//
//  RatPoly.swift
//  RatNum
//

import Foundation

/**

 RatPoly represents an immutable single-variate polynomial expression,
 as in the examples below.
 
 **AbstractState**: 
 RatPolys are sums of RatTerms with non-negative exponents.
 
 Example RatPolys include "0", "x-10", and "x^3-2*x^2+5/3*x+3", and "NaN".
 
 - Note: See RatNum's documentation for a definition of "immutable".
 
 */
public class RatPoly : CustomStringConvertible, Equatable {
  
  /// Holds all the RatTerms in this RatPoly
  private let terms : [RatTerm]
  
  // Definitions:
  // For a RatPoly p, let:
  //   C(p,i) be "p.terms[i].coeff"
  //   E(p,i) be "p.terms[i].expt"
  //   length(p) be "p.terms.count"
  // (These are helper functions that will make it easier for us
  // to write the remainder of the specifications. They are not
  // executable code; they just represent complex expressions in a
  // concise manner, so that we can stress the important parts of
  // other expressions in the spec rather than get bogged down in
  // the details of how we extract the coefficient for the 2nd term
  // or the exponent for the 5th term. So when you see C(p,i),
  // think "coefficient for the ith term in p".)
  
  // Abstraction Function:
  // AF(self) is the polynomial equal to the sum of the
  // RatTerms contained in 'terms':
  // sum (0 <= i < length(self)): self.terms[i]
  // If there are no terms, then the RatPoly represents the zero
  // polynomial.
  
  // Representation Invariant RI(self):
  //  forall i such that (0 <= i < length(self)), C(self,i) != 0 &&
  //  forall i such that (0 <= i < length(self)), E(self,i) >= 0 &&
  //  forall i such that (0 <= i < length(self) - 1), E(self,i) > E(self, i+1)
  // In other words:
  // * The terms field always points to some usable object.
  // * No term in a RatPoly has a zero coefficient.
  // * No term in a RatPoly has a negative exponent.
  // * The terms in a RatPoly are sorted in descending exponent order.
  
  /// A constant holding a Not-a-Number (NaN) value of type RatPoly.
  public static let nan = RatPoly(term: RatTerm.nan)
  
  /// A constant holding a zero value of type RatPoly.
  public static let zero = RatPoly()
  
  /**

   **Effects**: Constructs a new Poly, "0".
   
   */
  public init() {
    terms = []
  }
  
  /**
   
   **Requires**: term.expt >= 0
   
   **Effects**: Constructs a new Poly equal to "term". If term.coeff is zero,
             constructs a "0" polynomial.

   - Parameter term: The single term which the new RatPoly equals.
   */
  public init(term : RatTerm) {
    // TODO: Write Me
    // preconditionFailure("RatPoly.init not implemented yet.")
    // Remove the following line:
    terms = []
  }
  
  
  /**
   Builds a new RatPoly, given a descriptive String.
   Valid inputs include "0", "x-10", and "x^3-2*x^2+5/3*x+3", and "NaN".

   **Requires**: 'polyStr' is an instance of a string with no spaces that
   expresses a poly in the form defined in the description computer property.

   - Parameter description: A string of the format described in
     the **Requires** clause.
   
   - Returns: a RatPoly p such that p.description = description if description
     is an instance of a string with no spaces that expresses a poly in the
     form defined in the description computed property.  if description is
     not valid in that regard, this initializer returns nil.
   */
  public convenience init?(_ description: String) {
    if (description == "NaN") {
      self.init(term: RatTerm.nan)
    } else if (description == "") {
      return nil
    } else {
      var terms = [RatTerm]()
      let scanner = Scanner(string: description)
      while (!scanner.isAtEnd) {
        let negative = scanner.match(token: "-")
        // at start of term...
        let termStr = scanner.scanUpToCharacters(from: CharacterSet(charactersIn: "+-"))!
        if let term = RatTerm(termStr) {
          if (term != RatTerm.zero) {
            terms = RatPoly.insertOrdered(lst: terms, term: (negative ? -term : term))
          }
        } else {
          return nil
        }
        let _ = scanner.match(token: "+")
      }
      self.init(terms: terms)
    }
  }
  
  /**
   Builds a new RatPoly, given a constant and exponent.

   **Requires**: e >= 0
   
   **Effects**: Constructs a new Poly equal to "c*x^e". If c is zero, constructs
             a "0" polynomial.

   - Parameter c: The constant in the term which the new RatPoly equals.
   - Parameter e: The exponent in the term which the new RatPoly equals.
   */
  convenience init(c : Int, e : Int) {
    // TODO: Write Me
    // preconditionFailure("RatPoly.init not implemented yet.")
    // Remove the following line:
    self.init()
  }
  
  /**
   **Requires**: 'rt' satisfies clauses given in rep. invariant
   
   **Effects**: Constructs a new Poly using 'terms' as part of the representation.
   The method does not make a copy of 'terms'.

   - Parameter terms: A list of terms to be contained in the new RatPoly.
   */
  init(terms: [RatTerm]) {
    self.terms = terms
  }
  
  /**
   The degree of this RatPoly, which must not be NaN.  In other words,
   the largest exponent with a non-zero coefficient, or 0 if self is
   "0".
   
   **Requires**: !self.isNan
   
   - Note: It's a little unusual for a computed property to have
   a precondition, but you can just assert it as usual at the
   beginning of the code below.
   */
  public var degree : Int {
    // TODO: Write Me
    // preconditionFailure("RatPoly.degree not implemented yet.")
    // Remove the following line:
    return 0
  }
  
  /// true if and only if self has some coefficient = "NaN".
  public var isNaN : Bool {
    // TODO: Write Me
    // preconditionFailure("RatPoly.isNaN not implemented yet.")
    // Remove the following line:
    return false
  }
  
  
  /**
   Gets the RatTerm associated with given degree, using Swift's subscripting
   features.  Example: if p is the RatPoly for "10 + x + 3*x^2", then
   p[0] is the RatTerm "10", and p[2] is RatTerm "3*x^2"
   
   **Requires**: !self.isNaN
   
   - Parameter degree: The degree for which to find the corresponding RatTerm.
   
   - Returns: the RatTerm of degree 'deg'. If there is no term of degree 'deg'
   in this poly, then returns the zero RatTerm.
   */
  subscript(_ degree: Int) -> RatTerm {
    // TODO: Write Me
    // preconditionFailure("RatPoly subscript not implemented yet.")
    // Remove the following line:
    return RatTerm.nan
  }
  
  
  /**
   Scales coefficients of 'lst' by 'scalar' (helper procedure).
   SeeAlso: `RatTerm` regarding (C . E) notation
   
   - Parameter lst: The RatTerms to be scaled.
   
   - Parameter scalar: The value by which to scale coefficients in lst.
   
   - Returns: The scaled RatTerms.  That is,
   Forall i s.t. 0 <= i < lst.count, if lst.get(i) = (C . E) then result.get(i) = (C*scalar . E)
   
   */
  private static func scaleCoefficients(lst: [RatTerm], scalar : RatNum) -> [RatTerm] {
    // TODO: Write Me if you wish to use this helper
    preconditionFailure("RatPoly.scaleCoefficients not implemented yet.")
  }
  
  /**
   Increments exponents of 'lst' by 'degree' (helper procedure).
   SeeAlso: `RatTerm` regarding (C . E) notation
   
   - Parameter lst: The RatTerms whose exponents are to be incremented.
   
   - Parameter degree: the value by which to increment exponents in lst.
   
   - Returns: A list of RatTerms such that:
   Forall i s.t. 0 <= i < lst.count,
   if (C . E) = lst.get(i) then result.get(i) = (C . E+degree)
   
   */
  private static func incrementExponents(lst: [RatTerm], degree : Int) -> [RatTerm] {
    // TODO: Write Me
    // preconditionFailure("RatPoly.incrementExponents not implemented yet.")
    // Remove the following line:
    return []
  }
  
  /**
   Helper procedure: Inserts a term into a sorted sequence of terms,
   preserving the sorted nature of the sequence. If a term with the given
   degree already exists, adds their coefficients.
   
   Definitions: Let a "Sorted [RatTerm]" be a [RatTerm] V such
   that:
   1. V is sorted in descending exponent order,
   2. there are no two RatTerms with the same exponent in V, and
   3. there is no RatTerm in V with a coefficient equal to zero.
   
   For a Sorted [RatTerm] V and integer e, let cofind(V, e) be either
   the coefficient for a RatTerm rt in V whose exponent is e, or zero if
   there does not exist any such RatTerm in V. (This is like the coeff
   function of RatPoly.) We will write sorted(lst) to denote that lst is a
   Sorted [RatTerm], as defined above.
   
   **Requires**: sorted(lst)
   
   - Parameter lst: The list into which newTerm should be inserted.
   - Parameter newTerm: The term to be inserted into the list.
   
   - Returns: A [RatTerm] result such that:
   - sorted(result)
   - (cofind(result,newTerm.expt) = cofind(lst,newTerm.expt) + newTerm.coeff)
   */
  private static func insertOrdered(lst: [RatTerm], term : RatTerm) -> [RatTerm] {
    // TODO: Write Me
    // preconditionFailure("RatPoly.insertOrdered not implemented yet.")
    // Remove the following line:
    return []
  }
  
  /**
   Return the additive inverse of this RatPoly.
   
   - Returns: a RatPoly equal to "0 - rhs"; if rhs.isNaN, returns some r
   such that r.isNaN
   */
  public static prefix func -(rhs: RatPoly) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly unary - not implemented yet.")
    // Remove the following line:
    return rhs
  }
  
  /**
   Addition operation.
   
   - Returns: a RatPoly, r, such that r = "lhs + rhs"; if lhs.isNaN or
   rhs.isNaN, returns some r such that r.isNaN
   */
  public static func +(lhs: RatPoly, rhs: RatPoly) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly + not implemented yet.")
    // Remove the following line:
    return lhs
  }
  
  /**
   Subtraction operation.
   
   - Returns: a RatPoly, r, such that r = "lhs - rhs"; if lhs.isNaN or
   rhs.isNaN, returns some r such that r.isNaN
   
   */
  public static func -(lhs: RatPoly, rhs: RatPoly) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly - not implemented yet.")
    // Remove the following line:
    return lhs
  }
  
  /**
   Multiplication operation.
   
   - Returns: a RatPoly, r, such that r = "lhs * rhs"; if lhs.isNaN or
   rhs.isNaN, returns some r such that r.isNaN
   */
  public static func *(lhs: RatPoly, rhs: RatPoly) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly * not implemented yet.")
    // Remove the following line:
    return lhs
  }
  
  /**
    Division operation (truncating).
   
    Division of polynomials is defined as follows: Given two polynomials u
    and v, with v != "0", we can divide u by v to obtain a quotient
    polynomial q and a remainder polynomial r satisfying the condition u = "q *
    v + r", where the degree of r is strictly less than the degree of v, the
    degree of q is no greater than the degree of u, and r and q have no
    negative exponents.
   
    For the purposes of this class, the operation "u / v" returns q as
    defined above.
   
    The following are examples of div's behavior:
      - "x^3-2*x+3" / "3*x^2" = "1/3*x" (with r = "-2*x+3")
      - "x^2+2*x+15 / 2*x^3" = "0" (with r = "x^2+2*x+15")
      - "x^3+x-1 / x+1 = x^2-x+2 (with r = "-3")
   
    Note that this truncating behavior is similar to the behavior of integer
    division on computers.

   - Returns: a RatPoly, r, such that r = "lhs / rhs"; if lhs.isNaN or
   rhs.isNaN or rhs == zero, returns some r such that r.isNaN
   
   */
  public static func /(lhs: RatPoly, rhs: RatPoly) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly / not implemented yet.")
    // Remove the following line:
    return lhs
  }
  
  
  
  /**
   Return the derivative of this RatPoly.
   
   The derivative of a polynomial is the sum of the derivative of each term.
   
   - Returns: a RatPoly, q, such that q = dy/dx, where self == y. In other
   words, q is the derivative of self. If self.isNaN, then return
   some q such that q.isNaN
   
   */
  public func differentiate() -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly.differentiate not implemented yet.")
    // Remove the following line:
    return self
  }
  
  /**
   Returns the antiderivative of this RatPoly.
   
   The antiderivative of a polynomial is the sum of the antiderivative of
   each term plus some constant.
   
   - Parameter integrationConstant: The constant of integration to use when
   computating the antiderivative.
   
   - Returns: a RatPoly, q, such that dq/dx = self and the constant of
   integration is "integrationConstant" In other words, q is the
   antiderivative of self. If self.isNaN or integrationConstant.isNaN,
   then return some q such that q.isNaN.
   */
  public func antiDifferentiate(withConstant integrationConstant : RatNum) -> RatPoly {
    // TODO: Write Me
    // preconditionFailure("RatPoly.antiDifferentiate not implemented yet.")
    // Remove the following line:
    return self
  }
  
  /**
   
   Returns the integral of this RatPoly, integrated from lowerBound to
   upperBound.
   
   The Fundamental Theorem of Calculus states that the definite integral of
   f(x) with bounds a to b is F(b) - F(a) where dF/dx = f(x) NOTE: Remember
   that the lowerBound can be higher than the upperBound.
   
   - Parameter lowerBound: The lower bound of integration.
   - Parameter upperBound: The upper bound of integration.
   
   - Returns: a double that is the definite integral of self with bounds of
   integration between lowerBound and upperBound. If self.isNaN,
   or either lowerBound or upperBound is Double.NaN, return
   Double.NaN.
   
   */
  public func integrate(from lowerBound: Double, to upperBound: Double) -> Double {
    // TODO: Write Me
    // preconditionFailure("RatPoly.integrate not implemented yet.")
    // Remove the following line:
    return 0.0
  }
  
  /**
   
   Returns the value of this RatPoly, evaluated at d.
   
   - Parameter d: The value at which to evaluate this polynomial.
   
   - Returns: the value of this polynomial when evaluated at 'd'. For example,
   "x+2" evaluated at 3 is 5, and "x^2-x" evaluated at 3 is 6. if
   (self.isNaN), return Double.NaN
   */
  public func eval(at d : Double) -> Double {
    // TODO: Write Me
    // preconditionFailure("RatPoly.eval not implemented yet.")
    // Remove the following line:
    return 0.0
  }
  
  /**
   
    A String representation of the expression represented by this,
    with the terms sorted in order of degree from highest to lowest.

    There is no whitespace in the returned string.

    If the polynomial is itself zero, the returned string will just
    be "0".

    If self.isNaN, then the returned string will be just "NaN"

    The string for a non-zero, non-NaN poly is in the form
    "(-)T(+|-)T(+|-)...", where "(-)" refers to a possible minus
    sign, if needed, and "(+|-)" refer to either a plus or minus
    sign, as needed. For each term, T takes the form "C*x^E" or "C*x"
    where C > 0, UNLESS: (1) the exponent E is zero, in which case T
    takes the form "C", or (2) the coefficient C is one, in which
    case T takes the form "x^E" or "x". In cases were both (1) and
    (2) apply, (1) is used.

    Valid example outputs include "x^17-3/2*x^2+1", "-x+1", "-1/2",
    and "0".
   
   */
  public var description: String {
    get {
      if (terms.count == 0) {
        return "0";
      } else if (isNaN) {
        return "NaN"
      } else {
        return terms.reduce("", {
          let termAsString = String(describing:$1)
          if $0 == "" {
            return termAsString
          } else if ($1.coeff.isNegative) {
            return $0 + termAsString
          } else {
            return $0 + "+" + termAsString
          }
        })
      }
    }
  }
  
  
  /**
   
   Standard equality operation.
   
   - Returns: true if and only if 'lhs' and 'rhs' represent the same
   rational polynomial. Note that all NaN RatPolys are equal.
   
   */
  public static func ==(lhs: RatPoly, rhs: RatPoly) -> Bool {
    return lhs.isNaN && rhs.isNaN || lhs.terms == rhs.terms
  }
  
  
  
  /**
   
   Checks that the representation invariant holds (if any).
   
   */
  private func checkRep() {
    
    for term in terms {
      assert(term.coeff != RatNum.zero, "zero coefficient")
      assert(term.exponent >= 0, "negative exponent")
    }
    if (terms.count > 0) {
      for i in terms.startIndex..<terms.endIndex-1 {
        assert(terms[i+1].exponent < terms[i].exponent, "terms out of order")
      }
    }
  }
}


/**
 
 Extensions to make `Scanner` more friendly to use in Swift code.  The
 provided methods are counter-parts to the built-in library class, except
 that they return their results rather than passing them via in/out parameters.
 
 */
extension Scanner {
  
  /// - Returns: A string read from the scanner up to one of the chars in set,
  ///            or nil if that cannot be done.
  func scanUpToCharacters(from set: CharacterSet) -> String? {
    var result: NSString?
    return scanUpToCharacters(from: set, into: &result) ? (result! as String) : nil
  }
  
  /// - Returns: A string read from the scanner up to the provided string,
  ///            or nil if that cannot be done.
  func scanUpTo(_ string: String) -> String? {
    var result: NSString?
    return self.scanUpTo(string, into: &result) ? (result! as String) : nil
  }
  
  /// - Returns: A double read from the scanner, or nil if the next token
  ///            is not a double.
  func scanDouble() -> Double? {
    var double: Double = 0
    return scanDouble(&double) ? double : nil
  }
  
  /// - Returns: Scan past a given string, returning whether it succeeds.
  func match(token : String) -> Bool {
    return scanString(token, into: nil)
  }
}