"use strict";

class Expression {
  evaluate() {
    throw new Error("Not implemented");
  }
  toString() {
    throw new Error("Not implemented");
  }
  diff() {
    throw new Error("Not implemented");
  }
  simplify() {
    return this;
  }
}

class Const extends Expression {
  constructor(value) {
    super();
    this._value = value;
  }

  evaluate(_x, _y, _z) {
    return this._value;
  }
  toString() {
    return String(this._value);
  }
  diff(_varName) {
    return ZERO;
  }
  simplify() {
    return this;
  }
}

const ZERO = new Const(0);
const ONE = new Const(1);

const VAR_INDEX = { x: 0, y: 1, z: 2 };

class Variable extends Expression {
  constructor(name) {
    super();
    this._name = name;
    this._index = VAR_INDEX[name];
  }

  evaluate(...args) {
    return args[this._index];
  }
  toString() {
    return this._name;
  }

  diff(varName) {
    return varName === this._name ? ONE : ZERO;
  }

  simplify() {
    return this;
  }
}

function exprEquals(a, b) {
  if (a === b) return true;
  if (a.constructor !== b.constructor) return false;
  if (a instanceof Const) return a._value === b._value;
  if (a instanceof Variable) return a._name === b._name;
  if (a instanceof BinaryOp)
    return exprEquals(a._left, b._left) && exprEquals(a._right, b._right);
  if (a instanceof UnaryOp) return exprEquals(a._operand, b._operand);
  if (a instanceof NaryOp) {
    if (a._args.length !== b._args.length) return false;
    return a._args.every((arg, i) => exprEquals(arg, b._args[i]));
  }
  return false;
}

class BinaryOp extends Expression {
  constructor(left, right) {
    super();
    this._left = left;
    this._right = right;
  }

  evaluate(x, y, z) {
    return this._op(
      this._left.evaluate(x, y, z),
      this._right.evaluate(x, y, z),
    );
  }

  toString() {
    return `${this._left.toString()} ${this._right.toString()} ${this._symbol}`;
  }

  simplify() {
    const l = this._left.simplify();
    const r = this._right.simplify();
    const lConst = l instanceof Const;
    const rConst = r instanceof Const;

    const special = this._simplifySpecial(l, r, lConst, rConst);
    if (special !== null) return special;

    if (lConst && rConst) {
      return new Const(this._op(l._value, r._value));
    }

    return this._rebuild(l, r);
  }

  _rebuild(l, r) {
    return new this.constructor(l, r);
  }

  _simplifySpecial(l, r, lConst, rConst) {
    return null;
  }
}

class UnaryOp extends Expression {
  constructor(operand) {
    super();
    this._operand = operand;
  }

  evaluate(x, y, z) {
    return this._op(this._operand.evaluate(x, y, z));
  }

  toString() {
    return `${this._operand.toString()} ${this._symbol}`;
  }

  simplify() {
    const inner = this._operand.simplify();
    if (inner instanceof Const) {
      return new Const(this._op(inner._value));
    }
    return new this.constructor(inner);
  }
}

// Base class for N-ary operations (N = 1..5)
class NaryOp extends Expression {
  constructor(...args) {
    super();
    this._args = args;
  }

  evaluate(x, y, z) {
    return this._op(...this._args.map((a) => a.evaluate(x, y, z)));
  }

  toString() {
    return `${this._args.map((a) => a.toString()).join(" ")} ${this._symbol}`;
  }

  simplify() {
    const simplified = this._args.map((a) => a.simplify());
    if (simplified.every((a) => a instanceof Const)) {
      return new Const(this._op(...simplified.map((a) => a._value)));
    }
    return new this.constructor(...simplified);
  }
}

class Add extends BinaryOp {
  get _symbol() {
    return "+";
  }
  _op(a, b) {
    return a + b;
  }

  diff(varName) {
    return new Add(this._left.diff(varName), this._right.diff(varName));
  }

  _simplifySpecial(l, r, lConst, rConst) {
    if (lConst && l._value === 0) return r;
    if (rConst && r._value === 0) return l;
    return null;
  }
}

class Subtract extends BinaryOp {
  get _symbol() {
    return "-";
  }
  _op(a, b) {
    return a - b;
  }

  diff(varName) {
    return new Subtract(this._left.diff(varName), this._right.diff(varName));
  }

  _simplifySpecial(l, r, lConst, rConst) {
    if (rConst && r._value === 0) return l;
    if (lConst && l._value === 0) return new Negate(r).simplify();
    if (exprEquals(l, r)) return ZERO;
    return null;
  }
}

function cancelCommonFactor(num, den) {
  if (exprEquals(num, den)) return [ONE, ONE];

  if (den instanceof Multiply) {
    const [dA, dB] = [den._left, den._right];
    if (exprEquals(num, dA)) return [ONE, dB];
    if (exprEquals(num, dB)) return [ONE, dA];
    if (num instanceof Multiply) {
      const [nA, nB] = [num._left, num._right];
      if (exprEquals(nA, dA)) return [nB, dB];
      if (exprEquals(nA, dB)) return [nB, dA];
      if (exprEquals(nB, dA)) return [nA, dB];
      if (exprEquals(nB, dB)) return [nA, dA];
    }
  }

  if (num instanceof Multiply) {
    const [nA, nB] = [num._left, num._right];
    if (exprEquals(den, nA)) return [nB, ONE];
    if (exprEquals(den, nB)) return [nA, ONE];
  }

  return null;
}

class Multiply extends BinaryOp {
  get _symbol() {
    return "*";
  }
  _op(a, b) {
    return a * b;
  }

  diff(varName) {
    return new Add(
      new Multiply(this._left.diff(varName), this._right),
      new Multiply(this._left, this._right.diff(varName)),
    );
  }

  _simplifySpecial(l, r, lConst, rConst) {
    if ((lConst && l._value === 0) || (rConst && r._value === 0)) return ZERO;
    if (lConst && l._value === 1) return r;
    if (rConst && r._value === 1) return l;
    return null;
  }
}

class Divide extends BinaryOp {
  get _symbol() {
    return "/";
  }
  _op(a, b) {
    return a / b;
  }

  diff(varName) {
    return new Divide(
      new Subtract(
        new Multiply(this._left.diff(varName), this._right),
        new Multiply(this._left, this._right.diff(varName)),
      ),
      new Multiply(this._right, this._right),
    );
  }

  _simplifySpecial(l, r, lConst, rConst) {
    if (lConst && l._value === 0) return ZERO;
    if (rConst && r._value === 1) return l;

    const cancelled = cancelCommonFactor(l, r);
    if (cancelled !== null) {
      const [newNum, newDen] = cancelled;
      return new Divide(newNum, newDen).simplify();
    }

    return null;
  }
}

class Negate extends UnaryOp {
  get _symbol() {
    return "negate";
  }
  _op(a) {
    return -a;
  }

  diff(varName) {
    return new Negate(this._operand.diff(varName));
  }

  simplify() {
    const inner = this._operand.simplify();
    if (inner instanceof Const) return new Const(-inner._value);
    return new Negate(inner);
  }
}

// ── Power ──────────────────────────────────────────────────────────────────
// pow(f, g)  =  f ^ g
// (f ^ g)' = f ^ g * (g' * ln|f| + g * f' / f)
class Power extends BinaryOp {
  get _symbol() {
    return "pow";
  }
  _op(a, b) {
    return Math.pow(a, b);
  }

  diff(varName) {
    const f = this._left;
    const g = this._right;

    // Simplify f and g first so that e.g. Negate(Const(493)) is treated as Const(-493)
    const gs = g.simplify();
    const fs = f.simplify();

    // Also compute derivatives and simplify them to detect zero
    const fdiff = f.diff(varName).simplify();
    const gdiff = g.diff(varName).simplify();
    const fdiffZero = fdiff instanceof Const && fdiff._value === 0;
    const gdiffZero = gdiff instanceof Const && gdiff._value === 0;

    if (gs instanceof Const || gdiffZero) {
      // Power rule: (f^n)' = n * f^(n-1) * f'
      // Works when exponent is constant OR doesn't depend on varName
      const n = gs instanceof Const ? gs._value : g;
      const nExpr = gs instanceof Const ? new Const(gs._value) : g;
      const nMinus1 = gs instanceof Const ? new Const(gs._value - 1) : new Subtract(g, ONE);
      return new Multiply(
        new Multiply(nExpr, new Power(f, nMinus1)),
        fdiff,
      );
    }

    if (fs instanceof Const || fdiffZero) {
      // Exponential rule: (a^g)' = a^g * ln(|a|) * g'
      // Works when base is constant OR doesn't depend on varName
      return new Multiply(
        new Multiply(new Power(f, g), new Log(new Const(Math.E), f)),
        gdiff,
      );
    }

    // General case: both base and exponent truly depend on variable
    // (f^g)' = f^g * (g' * ln|f| + g * f'/f)
    return new Multiply(
      new Power(f, g),
      new Add(
        new Multiply(gdiff, new Log(new Const(Math.E), f)),
        new Multiply(g, new Divide(fdiff, f)),
      ),
    );
  }

  _simplifySpecial(l, r, lConst, rConst) {
    if (rConst && r._value === 0) return ONE;
    if (rConst && r._value === 1) return l;
    if (lConst && l._value === 1) return ONE;
    if (lConst && l._value === 0) return ZERO;
    return null;
  }
}

// ── Log ────────────────────────────────────────────────────────────────────
// log(base, x) = ln|x| / ln|base|
// Matches the postfix token "log": `base x log`
//
// Full quotient-rule derivative (base may depend on the variable):
//   log_b(f) = ln|f| / ln|b|
//   d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
//         = f'/(f * ln|b|)  -  b' * ln|f| / (b * (ln|b|)^2)
class Log extends BinaryOp {
  get _symbol() {
    return "log";
  }
  _op(base, x) {
    return Math.log(Math.abs(x)) / Math.log(Math.abs(base));
  }

  diff(varName) {
    const b = this._left;   // base expression
    const f = this._right;  // argument expression

    // Compute derivatives and simplify to detect zeros
    const bdiff = b.diff(varName).simplify();
    const fdiff = f.diff(varName).simplify();
    const bdiffZero = bdiff instanceof Const && bdiff._value === 0;
    const fdiffZero = fdiff instanceof Const && fdiff._value === 0;

    // log_b(f) = ln|f| / ln|b|
    // Quotient rule: d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
    //   = f'/(f * ln|b|)  -  b'*ln|f| / (b * (ln|b|)^2)

    // Special case: log_b(b) = 1 always => d/dx = 0
    if (exprEquals(b, f)) return ZERO;

    const lnB = new Log(new Const(Math.E), b);

    // Special case: base is constant (b' = 0) => d/dx = f'/(f * ln|b|)
    if (bdiffZero) {
      return new Divide(fdiff, new Multiply(f, lnB));
    }

    // Special case: argument is constant (f' = 0) => d/dx = -log_b(f) / (b * ln|b|)
    // derived from general formula with fdiff=0: -(b'/b * log_b(f)) / lnB
    // restructured to avoid 1/b factor: -Log(b,f) / (b * lnB)
    if (fdiffZero) {
      return new Negate(new Divide(new Log(b, f), new Multiply(b, lnB)));
    }

    // General case: use formula (f'/f - b'/b * log_b(f)) / ln|b|
    // This avoids introducing ln|f| separately (which would create structurally
    // different but numerically equal subexpressions that don't cancel).
    const inner = new Subtract(
      new Divide(fdiff, f),
      new Multiply(new Divide(bdiff, b), new Log(b, f)),
    );
    return new Divide(inner, lnB);
  }

  _simplifySpecial(l, r, lConst, rConst) {
    // log_b(1) = 0
    if (rConst && r._value === 1) return ZERO;
    // log_b(b) = 1  (when base and argument are structurally equal)
    if (exprEquals(l, r)) return ONE;
    return null;
  }
}

// ── SumN ───────────────────────────────────────────────────────────────────
// sum1..sum5 – sum of N arguments
// Derivative: sum of derivatives of each argument.
function makeSumN(n) {
  return class extends NaryOp {
    get _symbol() {
      return `sum${n}`;
    }
    _op(...vals) {
      return vals.reduce((acc, v) => acc + v, 0);
    }
    diff(varName) {
      return this._args
        .map((a) => a.diff(varName))
        .reduce((acc, d) => new Add(acc, d));
    }
    simplify() {
      const simplified = this._args.map((a) => a.simplify());
      // Drop zero summands
      const nonZero = simplified.filter(
        (a) => !(a instanceof Const && a._value === 0),
      );
      if (nonZero.length === 0) return ZERO;
      if (nonZero.length === 1) return nonZero[0];
      if (nonZero.every((a) => a instanceof Const)) {
        return new Const(nonZero.reduce((acc, a) => acc + a._value, 0));
      }
      return new this.constructor(...simplified);
    }
  };
}

const Sum1 = makeSumN(1);
const Sum2 = makeSumN(2);
const Sum3 = makeSumN(3);
const Sum4 = makeSumN(4);
const Sum5 = makeSumN(5);

// ── Operation registry & parser ────────────────────────────────────────────
const OPERATIONS = {
  "+": Add,
  "-": Subtract,
  "*": Multiply,
  "/": Divide,
  negate: Negate,
  pow: Power,
  log: Log,
  sum1: Sum1,
  sum2: Sum2,
  sum3: Sum3,
  sum4: Sum4,
  sum5: Sum5,
};

// Arity for each token (BinaryOp = 2, UnaryOp = 1, NaryOp = N)
const ARITY = {
  "+": 2,
  "-": 2,
  "*": 2,
  "/": 2,
  negate: 1,
  pow: 2,
  log: 2,
  sum1: 1,
  sum2: 2,
  sum3: 3,
  sum4: 4,
  sum5: 5,
};

function parse(expr) {
  const tokens = expr
    .trim()
    .split(/\s+/)
    .filter((t) => t.length > 0);
  const stack = [];

  for (const token of tokens) {
    if (token in OPERATIONS) {
      const Cls = OPERATIONS[token];
      const arity = ARITY[token];
      const operands = stack.splice(stack.length - arity, arity);
      stack.push(new Cls(...operands));
    } else if (token in VAR_INDEX) {
      stack.push(new Variable(token));
    } else {
      stack.push(new Const(Number(token)));
    }
  }

  return stack.pop();
}

if (typeof module !== "undefined") {
  module.exports = {
    Const,
    Variable,
    Add,
    Subtract,
    Multiply,
    Divide,
    Negate,
    Power,
    Log,
    Sum1,
    Sum2,
    Sum3,
    Sum4,
    Sum5,
    parse,
  };
}
