import { ReferenceRecord, Value } from '../value.mts';
import { Evaluate, type ExpressionEvaluator } from '../evaluator.mts';
import { Q, X } from '../completion.mts';
import { IsInTailPosition, StringValue } from '../static-semantics/all.mts';
import { OutOfRange } from '../helpers.mts';
import type { ParseNode } from '../parser/ParseNode.mts';
import {
  EvaluateCall,
  EvaluatePropertyAccessWithExpressionKey,
  EvaluatePropertyAccessWithIdentifierKey,
} from './all.mts';
import { GetValue, MakePrivateReference } from '#self';

/** https://tc39.es/ecma262/#sec-optional-chaining-evaluation */
//   OptionalExpression :
//     MemberExpression OptionalChain
//     CallExpression OptionalChain
//     OptionalExpression OptionalChain
export function* Evaluate_OptionalExpression({ MemberExpression, OptionalChain }: ParseNode.OptionalExpression) {
  // 1. Let baseReference be the result of evaluating MemberExpression.
  const baseReference = Q(yield* Evaluate(MemberExpression));
  // 2. Let baseValue be ? GetValue(baseReference).
  const baseValue = Q(yield* GetValue(baseReference));
  // 3. If baseValue is undefined or null, then
  if (baseValue === Value.undefined || baseValue === Value.null) {
    // a. Return undefined.
    return Value.undefined;
  }
  // 4. Return the result of performing ChainEvaluation of OptionalChain with arguments baseValue and baseReference.
  return yield* ChainEvaluation(OptionalChain, baseValue, X(baseReference));
}

/** https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation */
//   OptionalChain :
//     `?.` Arguments
//     `?.` `[` Expression `]`
//     `?.` IdentifierName
//     `?.` PrivateIdentifier
//     OptionalChain Arguments
//     OptionalChain `[` Expression `]`
//     OptionalChain `.` IdentifierName
//     OptionalChain `.` PrivateIdentifier
function* ChainEvaluation(node: ParseNode.OptionalChain, baseValue: Value, baseReference: Value | ReferenceRecord): ExpressionEvaluator {
  const {
    OptionalChain,
    Arguments,
    Expression,
    IdentifierName,
    PrivateIdentifier,
  } = node;
  if (Arguments) {
    if (OptionalChain) {
      // 1. Let optionalChain be OptionalChain.
      const optionalChain = OptionalChain;
      // 2. Let newReference be ? ChainEvaluation of optionalChain with arguments baseValue and baseReference.
      const newReference = Q(yield* ChainEvaluation(optionalChain, baseValue, baseReference));
      // 3. Let newValue be ? GetValue(newReference).
      const newValue = Q(yield* GetValue(newReference));
      // 4. Let thisChain be this OptionalChain.
      const thisChain = node;
      // 5. Let tailCall be IsInTailPosition(thisChain).
      const tailCall = IsInTailPosition(thisChain);
      // 6. Return ? EvaluateCall(newValue, newReference, Arguments, tailCall).
      return Q(yield* EvaluateCall(newValue, newReference, Arguments, tailCall));
    }
    // 1. Let thisChain be this OptionalChain.
    const thisChain = node;
    // 2. Let tailCall be IsInTailPosition(thisChain).
    const tailCall = IsInTailPosition(thisChain);
    // 3. Return ? EvaluateCall(baseValue, baseReference, Arguments, tailCall).
    return Q(yield* EvaluateCall(baseValue, baseReference, Arguments, tailCall));
  }
  if (Expression) {
    if (OptionalChain) {
      // 1. Let optionalChain be OptionalChain.
      const optionalChain = OptionalChain;
      // 2. Let newReference be ? ChainEvaluation of optionalChain with arguments baseValue and baseReference.
      const newReference = Q(yield* ChainEvaluation(optionalChain, baseValue, baseReference));
      // 3. Let newValue be ? GetValue(newReference).
      const newValue = Q(yield* GetValue(newReference));
      // 4. If the code matched by this OptionalChain is strict mode code, let strict be true; else let strict be false.
      const strict = node.strict;
      // 5. Return ? EvaluatePropertyAccessWithExpressionKey(newValue, Expression, strict).
      return Q(yield* EvaluatePropertyAccessWithExpressionKey(newValue, Expression, strict));
    }
    // 1. If the code matched by this OptionalChain is strict mode code, let strict be true; else let strict be false.
    const strict = node.strict;
    // 2. Return ? EvaluatePropertyAccessWithExpressionKey(baseValue, Expression, strict).
    return Q(yield* EvaluatePropertyAccessWithExpressionKey(baseValue, Expression, strict));
  }
  if (IdentifierName) {
    if (OptionalChain) {
      // 1. Let optionalChain be OptionalChain.
      const optionalChain = OptionalChain;
      // 2. Let newReference be ? ChainEvaluation of optionalChain with arguments baseValue and baseReference.
      const newReference = Q(yield* ChainEvaluation(optionalChain, baseValue, baseReference));
      // 3. Let newValue be ? GetValue(newReference).
      const newValue = Q(yield* GetValue(newReference));
      // 4. If the code matched by this OptionalChain is strict mode code, let strict be true; else let strict be false.
      const strict = node.strict;
      // 5. Return ! EvaluatePropertyAccessWithIdentifierKey(newValue, IdentifierName, strict).
      return X(EvaluatePropertyAccessWithIdentifierKey(newValue, IdentifierName, strict));
    }
    // 1. If the code matched by this OptionalChain is strict mode code, let strict be true; else let strict be false.
    const strict = node.strict;
    // 2. Return ! EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict).
    return X(EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict));
  }
  if (PrivateIdentifier) {
    if (OptionalChain) {
      // 1. Let optionalChain be OptionalChain.
      const optionalChain = OptionalChain;
      // 2. Let newReference be ? ChainEvaluation of optionalChain with arguments baseValue and baseReference.
      const newReference = Q(yield* ChainEvaluation(optionalChain, baseValue, baseReference));
      // 3. Let newValue be ? GetValue(newReference).
      const newValue = Q(yield* GetValue(newReference));
      // 4. Let fieldNameString be the StringValue of PrivateIdentifier.
      const fieldNameString = StringValue(PrivateIdentifier);
      // 5. Return ! MakePrivateReference(nv, fieldNameString).
      return X(MakePrivateReference(newValue, fieldNameString));
    }
    // 1. Let fieldNameString be the StringValue of PrivateIdentifier.
    const fieldNameString = StringValue(PrivateIdentifier);
    // 2. Return ! MakePrivateReference(bv, fieldNameString).
    return X(MakePrivateReference(baseValue, fieldNameString));
  }
  throw new OutOfRange('ChainEvaluation', node);
}
