import { Evaluate, type ValueEvaluator } from '../evaluator.mts';
import { OutOfRange } from '../helpers.mts';
import { BigIntValue, NumberValue } from '../value.mts';
import { Q } from '../completion.mts';
import type { ParseNode } from '../parser/ParseNode.mts';
import {
  Assert,
  F,
  GetValue,
  PutValue,
  ToNumeric,
  Z,
} from '#self';

type AnyNumericValue = BigIntValue | NumberValue;
// UpdateExpression :
//   LeftHandSideExpression `++`
//   LeftHandSideExpression `--`
//   `++` UnaryExpression
//   `--` UnaryExpression
export function* Evaluate_UpdateExpression({ LeftHandSideExpression, operator, UnaryExpression }: ParseNode.UpdateExpression): ValueEvaluator {
  switch (true) {
    // UpdateExpression : LeftHandSideExpression `++`
    // https://tc39.es/ecma262/#sec-postfix-increment-operator-runtime-semantics-evaluation
    case operator === '++' && !!LeftHandSideExpression: {
      // 1. Let lhs be the result of evaluating LeftHandSideExpression.
      const lhs = Q(yield* Evaluate(LeftHandSideExpression));
      // 2. Let oldValue be ? ToNumeric(? GetValue(lhs)).
      const oldValue = Q(yield* ToNumeric(Q(yield* GetValue(lhs))));
      // 3. If oldValue is a Number, then
      //  a. Let newValue be Number::add(oldValue, 1𝔽).
      //  4. Else,
      //         a. Assert: oldValue is a BigInt.
      //         b. Let newValue be BigInt::add(oldValue, 1ℤ).
      let newValue: AnyNumericValue;
      if (oldValue instanceof NumberValue) {
        newValue = NumberValue.add(oldValue, F(1));
      } else {
        Assert(oldValue instanceof BigIntValue);
        newValue = BigIntValue.add(oldValue, Z(1n));
      }
      // 4. Perform ? PutValue(lhs, newValue).
      Q(yield* PutValue(lhs, newValue));
      // 5. Return oldValue.
      return oldValue;
    }

    // UpdateExpression : LeftHandSideExpression `--`
    // https://tc39.es/ecma262/#sec-postfix-decrement-operator-runtime-semantics-evaluation
    case operator === '--' && !!LeftHandSideExpression: {
      // 1. Let lhs be the result of evaluating LeftHandSideExpression.
      const lhs = Q(yield* Evaluate(LeftHandSideExpression));
      // 2. Let oldValue be ? ToNumeric(? GetValue(lhs)).
      const oldValue = Q(yield* ToNumeric(Q(yield* GetValue(lhs))));
      // 3. If oldValue is a Number, then
      //  a. Let newValue be Number::subtract(oldValue, 1𝔽).
      //  4. Else,
      //         a. Assert: oldValue is a BigInt.
      //         b. Let newValue be BigInt::subtract(oldValue, 1ℤ).
      let newValue: AnyNumericValue;
      if (oldValue instanceof NumberValue) {
        newValue = NumberValue.subtract(oldValue, F(1));
      } else {
        Assert(oldValue instanceof BigIntValue);
        newValue = BigIntValue.subtract(oldValue, Z(1n));
      }
      // 4. Perform ? PutValue(lhs, newValue).
      Q(yield* PutValue(lhs, newValue));
      // 5. Return oldValue.
      return oldValue;
    }

    // UpdateExpression : `++` UnaryExpression
    // https://tc39.es/ecma262/#sec-prefix-increment-operator-runtime-semantics-evaluation
    case operator === '++' && !!UnaryExpression: {
      // 1. Let expr be the result of evaluating UnaryExpression.
      const expr = Q(yield* Evaluate(UnaryExpression));
      // 2. Let oldValue be ? ToNumeric(? GetValue(expr)).
      const oldValue = Q(yield* ToNumeric(Q(yield* GetValue(expr))));
      // 3. If oldValue is a Number, then
      //  a. Let newValue be Number::add(oldValue, 1𝔽).
      //  4. Else,
      //         a. Assert: oldValue is a BigInt.
      //         b. Let newValue be BigInt::add(oldValue, 1ℤ).
      let newValue: AnyNumericValue;
      if (oldValue instanceof NumberValue) {
        newValue = NumberValue.add(oldValue, F(1));
      } else {
        Assert(oldValue instanceof BigIntValue);
        newValue = BigIntValue.add(oldValue, Z(1n));
      }
      // 4. Perform ? PutValue(expr, newValue).
      Q(yield* PutValue(expr, newValue));
      // 5. Return newValue.
      return newValue;
    }

    // UpdateExpression : `--` UnaryExpression
    // https://tc39.es/ecma262/#sec-prefix-decrement-operator-runtime-semantics-evaluation
    case operator === '--' && !!UnaryExpression: {
      // 1. Let expr be the result of evaluating UnaryExpression.
      const expr = Q(yield* Evaluate(UnaryExpression));
      // 2. Let oldValue be ? ToNumeric(? GetValue(expr)).
      const oldValue = Q(yield* ToNumeric(Q(yield* GetValue(expr))));
      // 3. If oldValue is a Number, then
      //   a. Let newValue be Number::subtract(oldValue, 1𝔽).
      // 4. Else,
      //   a. Assert: oldValue is a BigInt.
      //   b. Let newValue be BigInt::subtract(oldValue, 1ℤ).
      let newValue: AnyNumericValue;
      if (oldValue instanceof NumberValue) {
        newValue = NumberValue.subtract(oldValue, F(1));
      } else {
        Assert(oldValue instanceof BigIntValue);
        newValue = BigIntValue.subtract(oldValue, Z(1n));
      }
      // 4. Perform ? PutValue(expr, newValue).
      Q(yield* PutValue(expr, newValue));
      // 5. Return newValue.
      return newValue;
    }

    default:
      throw new OutOfRange('Evaluate_UpdateExpression', operator);
  }
}
