require 'symath/operation'
require 'symath/poly/dup'

module SyMath::Operation::DistributiveLaw
  # The expand() method expands a product using the distributive law over
  # products of sums:
  #   a*(b + c) -> a*b + a*c
  #   a*(b - c) -> a*b - a*c
  # The transformation iterates until no changes occur. Thus, the expression
  #   (a + b)*(c + d) transforms to a*c + a*d + b*c + b*d
  def expand()
    return iterate('expand_single_pass')
  end

  def expand_single_pass
    if is_a?(SyMath::Minus)
      return -argument.expand_single_pass
    end

    if is_a?(SyMath::Power) or is_a?(SyMath::Product)
      ret = 1.to_m

      factors.each do |f|
        if f.is_a?(SyMath::Power)
          if f.exponent.is_number?
            f.exponent.value.times { ret = expand_product(ret, f.base) }
          else
            ret = expand_product(ret, f)
          end
        else
          ret = expand_product(ret, f)
        end
      end

      return ret
    end

    if is_sum_exp?
      ret = 0.to_m
      
      terms.each do |t|
        ret += t.expand_single_pass
      end

      return ret
    end
    
    return self
  end

  def expand_product(exp1, exp2)
    sign = 1.to_m

    if exp1.is_a?(SyMath::Minus)
      exp1 = exp1.argument
      sign = -sign
    end

    if exp2.is_a?(SyMath::Minus)
      exp2 = exp2.argument
      sign = -sign
    end

    ret = 0.to_m
      
    exp1.terms.each do |t1|
      exp2.terms.each do |t2|
        ret += sign*t1*t2
      end
    end

    return ret
  end

  def has_fractional_terms?()
    terms.each do |t|
      t.factors.each do |f|
        if f.is_divisor_factor?
          return true
        end
      end
    end

    return false
  end

  # Collect factors which occur in each term.
  def factorize_simple()
    return self if !self.is_sum_exp?

    sfactors = {}
    vfactors = {}
    coeffs = []
    dcoeffs = []
    vectors = []
    
    terms.each_with_index do |t, i|
      c = 1
      dc = 1
      vf = 1.to_m
      
      t.factors.each do |f|
        # Sign
        if f == -1
          c *= -1
          next
        end

        # Constant
        if f.is_number?
          c *= f.value
          next
        end

        if f.is_divisor_factor?
          # Divisor constant
          if f.base.is_number?
            dc *= f.base.value**f.exponent.argument.value
            next
          end

          # Divisor factor
          ex = f.base

          if !sfactors.key?(ex)
            sfactors[ex] = []
          end

          if sfactors[ex][i].nil?
            sfactors[ex][i] = - f.exponent.argument.value
          else
            sfactors[ex][i] -= f.exponent.argument.value
          end
          next
        end

        # Vector factor
        if f.type.is_subtype?(:tensor)
          vf *= f
          next
        end

        # Scalar factor
        if f.exponent.is_number?
          ex = f.base
          n = f.exponent.value
        else
          ex = f
          n = 1
        end
      
        if !sfactors.key?(ex)
          sfactors[ex] = []
        end
      
        sfactors[ex][i] = n.value        
      end
      
      coeffs.push(c)
      dcoeffs.push(dc)
      vectors.push(vf)
    end

    # If there is only one term, there is nothing to factorize
    if coeffs.length == 1
      return self
    end

    # Try to factorize the scalar part
    spart = 1.to_m
    dpart = 1.to_m
    sfactors.each do |ex, pow|
      # Replace nil with 0 and extend array to full length
      pow.map! { |i| i || 0 }
      (coeffs.length - pow.length).times { pow << 0 }

      if pow.max > 0 and pow.min > 0
        f = pow.min
        pow.map! { |i| i - f }
        spart = spart*ex**f
      end
      
      if pow.max < 0 and pow.min < 0
        f = pow.max
        pow.map! { |i| i - f }
        dpart = dpart*ex**(-f)
      end
    end
    
    # Return self if there were no common factors.
    if spart == 1 and dpart == 1
      return self
    end

    # Extract gcd from coeffs and dcoeffs
    gcd_coeffs = coeffs.inject(:gcd)
    gcd_dcoeffs = dcoeffs.inject(:gcd)
    coeffs.map! { |i| i/gcd_coeffs }
    dcoeffs.map! { |i| i/gcd_dcoeffs }
    
    newsum = 0.to_m

    (0..coeffs.length-1).each do |i|
      t = coeffs[i].to_m
      
      sfactors.each do |ex, pow|
        next if pow[i].nil?
        if pow[i] > 0
          t *= ex**pow[i]
        elsif pow[i] < 0
          t /= ex**(-pow[i])
        end
      end

      t /= dcoeffs[i]
      t *= vectors[i]

      newsum += t
    end

    return gcd_coeffs*spart*newsum/(gcd_dcoeffs*dpart)
  end
  
  # The factorize() method factorizes a univariate polynomial expression
  # with integer coefficients.
  def factorize()
    if (has_fractional_terms?)
      e = combine_fractions
      if e.is_a?(SyMath::Fraction)
        return e.dividend.factorize_integer_poly.div(e.divisor)
      end
    else
      return factorize_integer_poly
    end
  end
    
  def factorize_integer_poly()
    dup = SyMath::Poly::DUP.new(self)
    factors = dup.factor
      
    ret = factors[1].map do |f|
      if f[1] != 1
        f[0].to_m.power(f[1])
      else
        f[0].to_m
      end
    end

    if factors[0] != 1
      ret.unshift(factors[0].to_m)
    end

    return ret.inject(:mul)
  end


  # The combine_fractions() method combines fractions by first determining
  # their least common denominator, then applying the distributive law.
  # Examples:
  #   a/c + b/c   -> (a + b)/c
  #   2/3 + 3/4   -> 17/12
  #   a/2 + 2*a/3 -> 7*a/6
  #   2*a/b + 2*c/(3*b) -> (6*a + 2*c)/(3*b)
  def combine_fractions()
    if is_sum_exp?
      return combfrac_sum
    end

    return recurse('combine_fractions', nil)
  end

  def combfrac_add_term(sum, t)
    c = 1
    dc = 1
    fact = 1.to_m
    divf = 1.to_m

    t.factors.each do |f|
      if f.is_number?
        c *= f.value
        next
      end

      if f == -1
        fact *= -1
        next
      end

      if f.is_divisor_factor?
        if f.base.is_number?
          dc *= (f.base.value**f.exponent.argument.value)
        else
          divf *= f.base
        end
        next
      end

      fact *= f
    end

    if !sum.key?(divf)
      sum[divf] = {}
      sum[divf][:fact] = fact
      sum[divf][:c] = c
      sum[divf][:dc] = dc
      return
    end

    s = sum[divf]
    lcm = dc.lcm(s[:dc])

    if lcm > dc
      c *= lcm/dc
      dc = lcm
    end

    if lcm > s[:dc]
      s[:c] *= lcm/s[:dc]
      s[:dc] = lcm
    end

    if fact.nil?
      fact = c.to_m
    elsif c > 1
      fact = fact.mul(c.to_m)
    end

    if s[:fact].nil?
      fact = fact.add(s[:c].to_m) if s[:c] > 1
    else
      fact = fact.add(s[:c] > 1 ? s[:c].to_m*s[:fact] : s[:fact])
    end
    
    s[:fact] = fact
    s[:c] = 1
  end

  def combfrac_sum
    sum = {}

    terms.each do |t|
      combfrac_add_term(sum, t)
    end

    ret = 0.to_m

    sum.keys.each do |divf|
      s = sum[divf]
      if s[:c] > 1
        r = s[:c].to_m.mul(s[:fact])
      else
        r = s[:fact]
      end

      if divf.nil?
        r = r.div(s[:dc]) if s[:dc] > 1
      elsif s[:dc] > 1
        r = r.div(s[:dc].to_m*divf)
      else
        r = r.div(divf)
      end

      ret += r
      return ret
    end
  end
end
