import { normalizeUnit } from "./conversions.js";
import { ZERO, numberNode } from "./parsers.js";
/* @license
 * Copyright 2019 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var _a, _b, _c;
const $evaluate = Symbol("evaluate");
const $lastValue = Symbol("lastValue");
class Evaluator {
  constructor() {
    this[_a] = null;
  }
  /**
   * An Evaluatable is a NumberNode or an Evaluator that evaluates a NumberNode
   * as the result of invoking its evaluate method. This is mainly used to
   * ensure that CSS function nodes are cast to the corresponding Evaluators
   * that will resolve the result of the function, but is also used to ensure
   * that a percentage nested at arbitrary depth in the expression will always
   * be evaluated against the correct basis.
   */
  static evaluatableFor(node, basis = ZERO) {
    if (node instanceof Evaluator) {
      return node;
    }
    if (node.type === "number") {
      if (node.unit === "%") {
        return new PercentageEvaluator(node, basis);
      }
      return node;
    }
    switch (node.name.value) {
      case "calc":
        return new CalcEvaluator(node, basis);
      case "env":
        return new EnvEvaluator(node);
    }
    return ZERO;
  }
  /**
   * If the input is an Evaluator, returns the result of evaluating it.
   * Otherwise, returns the input.
   *
   * This is a helper to aide in resolving a NumberNode without conditionally
   * checking if the Evaluatable is an Evaluator everywhere.
   */
  static evaluate(evaluatable) {
    if (evaluatable instanceof Evaluator) {
      return evaluatable.evaluate();
    }
    return evaluatable;
  }
  /**
   * If the input is an Evaluator, returns the value of its isConstant property.
   * Returns true for all other input values.
   */
  static isConstant(evaluatable) {
    if (evaluatable instanceof Evaluator) {
      return evaluatable.isConstant;
    }
    return true;
  }
  /**
   * This method applies a set of structured intrinsic metadata to an evaluated
   * result from a parsed CSS-like string of expressions. Intrinsics provide
   * sufficient metadata (e.g., basis values, analogs for keywords) such that
   * omitted values in the input string can be backfilled, and keywords can be
   * converted to concrete numbers.
   *
   * The result of applying intrinsics is a tuple of NumberNode values whose
   * units match the units used by the basis of the intrinsics.
   *
   * The following is a high-level description of how intrinsics are applied:
   *
   *  1. Determine the value of 'auto' for the current term
   *  2. If there is no corresponding input value for this term, substitute the
   *     'auto' value.
   *  3. If the term is an IdentNode, treat it as a keyword and perform the
   *     appropriate substitution.
   *  4. If the term is still null, fallback to the 'auto' value
   *  5. If the term is a percentage, apply it to the basis and return that
   *     value
   *  6. Normalize the unit of the term
   *  7. If the term's unit does not match the basis unit, return the basis
   *     value
   *  8. Return the term as is
   */
  static applyIntrinsics(evaluated, intrinsics) {
    const { basis, keywords } = intrinsics;
    const { auto } = keywords;
    return basis.map((basisNode, index) => {
      const autoSubstituteNode = auto[index] == null ? basisNode : auto[index];
      let evaluatedNode = evaluated[index] ? evaluated[index] : autoSubstituteNode;
      if (evaluatedNode.type === "ident") {
        const keyword = evaluatedNode.value;
        if (keyword in keywords) {
          evaluatedNode = keywords[keyword][index];
        }
      }
      if (evaluatedNode == null || evaluatedNode.type === "ident") {
        evaluatedNode = autoSubstituteNode;
      }
      if (evaluatedNode.unit === "%") {
        return numberNode(evaluatedNode.number / 100 * basisNode.number, basisNode.unit);
      }
      evaluatedNode = normalizeUnit(evaluatedNode, basisNode);
      if (evaluatedNode.unit !== basisNode.unit) {
        return basisNode;
      }
      return evaluatedNode;
    });
  }
  /**
   * If true, the Evaluator will only evaluate its AST one time. If false, the
   * Evaluator will re-evaluate the AST each time that the public evaluate
   * method is invoked.
   */
  get isConstant() {
    return false;
  }
  /**
   * Evaluate the Evaluator and return the result. If the Evaluator is constant,
   * the corresponding AST will only be evaluated once, and the result of
   * evaluating it the first time will be returned on all subsequent
   * evaluations.
   */
  evaluate() {
    if (!this.isConstant || this[$lastValue] == null) {
      this[$lastValue] = this[$evaluate]();
    }
    return this[$lastValue];
  }
}
_a = $lastValue;
const $percentage = Symbol("percentage");
const $basis = Symbol("basis");
class PercentageEvaluator extends Evaluator {
  constructor(percentage, basis) {
    super();
    this[$percentage] = percentage;
    this[$basis] = basis;
  }
  get isConstant() {
    return true;
  }
  [$evaluate]() {
    return numberNode(this[$percentage].number / 100 * this[$basis].number, this[$basis].unit);
  }
}
const $identNode = Symbol("identNode");
class EnvEvaluator extends Evaluator {
  constructor(envFunction) {
    super();
    this[_b] = null;
    const identNode = envFunction.arguments.length ? envFunction.arguments[0].terms[0] : null;
    if (identNode != null && identNode.type === "ident") {
      this[$identNode] = identNode;
    }
  }
  get isConstant() {
    return false;
  }
  [(_b = $identNode, $evaluate)]() {
    if (this[$identNode] != null) {
      switch (this[$identNode].value) {
        case "window-scroll-y":
          const verticalScrollPosition = window.pageYOffset;
          const verticalScrollMax = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
          const scrollY = verticalScrollPosition / (verticalScrollMax - window.innerHeight) || 0;
          return { type: "number", number: scrollY, unit: null };
      }
    }
    return ZERO;
  }
}
const IS_MULTIPLICATION_RE = /[\*\/]/;
const $evaluator = Symbol("evaluator");
class CalcEvaluator extends Evaluator {
  constructor(calcFunction, basis = ZERO) {
    super();
    this[_c] = null;
    if (calcFunction.arguments.length !== 1) {
      return;
    }
    const terms = calcFunction.arguments[0].terms.slice();
    const secondOrderTerms = [];
    while (terms.length) {
      const term = terms.shift();
      if (secondOrderTerms.length > 0) {
        const previousTerm = secondOrderTerms[secondOrderTerms.length - 1];
        if (previousTerm.type === "operator" && IS_MULTIPLICATION_RE.test(previousTerm.value)) {
          const operator = secondOrderTerms.pop();
          const leftValue = secondOrderTerms.pop();
          if (leftValue == null) {
            return;
          }
          secondOrderTerms.push(new OperatorEvaluator(operator, Evaluator.evaluatableFor(leftValue, basis), Evaluator.evaluatableFor(term, basis)));
          continue;
        }
      }
      secondOrderTerms.push(term.type === "operator" ? term : Evaluator.evaluatableFor(term, basis));
    }
    while (secondOrderTerms.length > 2) {
      const [left, operator, right] = secondOrderTerms.splice(0, 3);
      if (operator.type !== "operator") {
        return;
      }
      secondOrderTerms.unshift(new OperatorEvaluator(operator, Evaluator.evaluatableFor(left, basis), Evaluator.evaluatableFor(right, basis)));
    }
    if (secondOrderTerms.length === 1) {
      this[$evaluator] = secondOrderTerms[0];
    }
  }
  get isConstant() {
    return this[$evaluator] == null || Evaluator.isConstant(this[$evaluator]);
  }
  [(_c = $evaluator, $evaluate)]() {
    return this[$evaluator] != null ? Evaluator.evaluate(this[$evaluator]) : ZERO;
  }
}
const $operator = Symbol("operator");
const $left = Symbol("left");
const $right = Symbol("right");
class OperatorEvaluator extends Evaluator {
  constructor(operator, left, right) {
    super();
    this[$operator] = operator;
    this[$left] = left;
    this[$right] = right;
  }
  get isConstant() {
    return Evaluator.isConstant(this[$left]) && Evaluator.isConstant(this[$right]);
  }
  [$evaluate]() {
    const leftNode = normalizeUnit(Evaluator.evaluate(this[$left]));
    const rightNode = normalizeUnit(Evaluator.evaluate(this[$right]));
    const { number: leftValue, unit: leftUnit } = leftNode;
    const { number: rightValue, unit: rightUnit } = rightNode;
    if (rightUnit != null && leftUnit != null && rightUnit != leftUnit) {
      return ZERO;
    }
    const unit = leftUnit || rightUnit;
    let value;
    switch (this[$operator].value) {
      case "+":
        value = leftValue + rightValue;
        break;
      case "-":
        value = leftValue - rightValue;
        break;
      case "/":
        value = leftValue / rightValue;
        break;
      case "*":
        value = leftValue * rightValue;
        break;
      default:
        return ZERO;
    }
    return { type: "number", number: value, unit };
  }
}
const $evaluatables = Symbol("evaluatables");
const $intrinsics = Symbol("intrinsics");
class StyleEvaluator extends Evaluator {
  constructor(expressions, intrinsics) {
    super();
    this[$intrinsics] = intrinsics;
    const firstExpression = expressions[0];
    const terms = firstExpression != null ? firstExpression.terms : [];
    this[$evaluatables] = intrinsics.basis.map((basisNode, index) => {
      const term = terms[index];
      if (term == null) {
        return { type: "ident", value: "auto" };
      }
      if (term.type === "ident") {
        return term;
      }
      return Evaluator.evaluatableFor(term, basisNode);
    });
  }
  get isConstant() {
    for (const evaluatable of this[$evaluatables]) {
      if (!Evaluator.isConstant(evaluatable)) {
        return false;
      }
    }
    return true;
  }
  [$evaluate]() {
    const evaluated = this[$evaluatables].map((evaluatable) => Evaluator.evaluate(evaluatable));
    return Evaluator.applyIntrinsics(evaluated, this[$intrinsics]).map((numberNode2) => numberNode2.number);
  }
}
export {
  CalcEvaluator,
  EnvEvaluator,
  Evaluator,
  OperatorEvaluator,
  PercentageEvaluator,
  StyleEvaluator
};
//# sourceMappingURL=evaluators.js.map
