/* eslint-disable @typescript-eslint/no-unused-vars */
import { emptyArray } from '@aurelia/kernel';
import type { IVisitor } from './ast.visitor';

/** @internal */ export const ekAccessThis = 'AccessThis';
/** @internal */ export const ekAccessBoundary = 'AccessBoundary';
/** @internal */ export const ekAccessGlobal = 'AccessGlobal';
/** @internal */ export const ekAccessScope = 'AccessScope';
/** @internal */ export const ekArrayLiteral = 'ArrayLiteral';
/** @internal */ export const ekObjectLiteral = 'ObjectLiteral';
/** @internal */ export const ekPrimitiveLiteral = 'PrimitiveLiteral';
/** @internal */ export const ekNew = 'New';
/** @internal */ export const ekTemplate = 'Template';
/** @internal */ export const ekUnary = 'Unary';
/** @internal */ export const ekCallScope = 'CallScope';
/** @internal */ export const ekCallMember = 'CallMember';
/** @internal */ export const ekCallFunction = 'CallFunction';
/** @internal */ export const ekCallGlobal = 'CallGlobal';
/** @internal */ export const ekAccessMember = 'AccessMember';
/** @internal */ export const ekAccessKeyed = 'AccessKeyed';
/** @internal */ export const ekTaggedTemplate = 'TaggedTemplate';
/** @internal */ export const ekBinary = 'Binary';
/** @internal */ export const ekConditional = 'Conditional';
/** @internal */ export const ekAssign = 'Assign';
/** @internal */ export const ekArrowFunction = 'ArrowFunction';
/** @internal */ export const ekValueConverter = 'ValueConverter';
/** @internal */ export const ekBindingBehavior = 'BindingBehavior';
/** @internal */ export const ekArrayBindingPattern = 'ArrayBindingPattern';
/** @internal */ export const ekObjectBindingPattern = 'ObjectBindingPattern';
/** @internal */ export const ekBindingIdentifier = 'BindingIdentifier';
/** @internal */ export const ekForOfStatement = 'ForOfStatement';
/** @internal */ export const ekInterpolation = 'Interpolation';
/** @internal */ export const ekArrayDestructuring = 'ArrayDestructuring';
/** @internal */ export const ekObjectDestructuring = 'ObjectDestructuring';
/** @internal */ export const ekDestructuringAssignmentLeaf = 'DestructuringAssignmentLeaf';
/** @internal */ export const ekCustom = 'Custom';

export type ExpressionKind =
  | 'AccessThis'
  | 'AccessGlobal'
  | 'AccessBoundary'
  | 'AccessScope'
  | 'ArrayLiteral'
  | 'ObjectLiteral'
  | 'PrimitiveLiteral'
  | 'New'
  | 'Template'
  | 'Unary'
  | 'CallScope'
  | 'CallMember'
  | 'CallFunction'
  | 'CallGlobal'
  | 'AccessMember'
  | 'AccessKeyed'
  | 'TaggedTemplate'
  | 'Binary'
  | 'Conditional'
  | 'Assign'
  | 'ArrowFunction'
  | 'ValueConverter'
  | 'BindingBehavior'
  | 'ArrayBindingPattern'
  | 'ObjectBindingPattern'
  | 'BindingIdentifier'
  | 'ForOfStatement'
  | 'Interpolation'
  | 'ArrayDestructuring'
  | 'ObjectDestructuring'
  | 'DestructuringAssignmentLeaf'
  | 'Custom';

export type UnaryOperator = 'void' | 'typeof' | '!' | '-' | '+' | '++' | '--';

export type BinaryOperator = '??' | '&&' | '||' | '==' | '===' | '!=' | '!==' | 'instanceof' | 'in' | '+' | '-' | '*' | '/' | '%' | '**' | '<' | '>' | '<=' | '>=';
export type AssignmentOperator = '=' | '/=' | '*=' | '+=' | '-=';
export type IsPrimary = AccessThisExpression | AccessBoundaryExpression | AccessScopeExpression | AccessGlobalExpression | ArrayLiteralExpression | ObjectLiteralExpression | PrimitiveLiteralExpression | TemplateExpression | NewExpression;
export type IsLiteral = ArrayLiteralExpression | ObjectLiteralExpression | PrimitiveLiteralExpression | TemplateExpression;
export type IsLeftHandSide = IsPrimary | CallGlobalExpression | CallFunctionExpression | CallMemberExpression | CallScopeExpression | AccessMemberExpression | AccessKeyedExpression | TaggedTemplateExpression;
export type IsUnary = IsLeftHandSide | UnaryExpression;
export type IsBinary = IsUnary | BinaryExpression;
export type IsConditional = IsBinary | ConditionalExpression;
export type IsAssign = IsConditional | AssignExpression | ArrowFunction;
export type IsValueConverter = IsAssign | ValueConverterExpression;
export type IsBindingBehavior = IsValueConverter | BindingBehaviorExpression;
export type IsAssignable = AccessScopeExpression | AccessKeyedExpression | AccessMemberExpression | AssignExpression;
export type IsExpression = IsBindingBehavior | Interpolation;
export type BindingIdentifierOrPattern = BindingIdentifier | ArrayBindingPattern | ObjectBindingPattern;
export type IsExpressionOrStatement = IsExpression | ForOfStatement | BindingIdentifierOrPattern | DestructuringAssignmentExpression | DestructuringAssignmentSingleExpression | DestructuringAssignmentRestExpression;
export type AnyBindingExpression<TCustom extends CustomExpression = CustomExpression> = Interpolation | ForOfStatement | TCustom | IsBindingBehavior;

export class CustomExpression {
  public readonly $kind = ekCustom;
  public constructor(
    public readonly value: unknown,
  ) {}

  public evaluate(...params: unknown[]): unknown {
    return this.value;
  }

  public assign(...params: unknown[]): unknown {
    return params;
  }

  public bind(...params: unknown[]): void {
    // empty
  }

  public unbind(...params: unknown[]): void {
    // empty
  }

  public accept<T>(_visitor: IVisitor<T>): T {
    return (void 0)!;
  }
}

export class BindingBehaviorExpression {
  public readonly $kind = ekBindingBehavior;
  /**
   * The name of the property to store a binding behavior on the binding when binding
   */
  public readonly key: string;

  public constructor(
    public readonly expression: IsBindingBehavior,
    public readonly name: string,
    public readonly args: readonly IsAssign[],
  ) {
    this.key = `_bb_${name}`;
  }
}

export class ValueConverterExpression {
  public readonly $kind = ekValueConverter;
  public constructor(
    public readonly expression: IsValueConverter,
    public readonly name: string,
    public readonly args: readonly IsAssign[],
  ) {
  }
}

export class AssignExpression {
  public readonly $kind = ekAssign;

  public constructor(
    public readonly target: IsAssignable,
    public readonly value: IsAssign,
    public readonly op: AssignmentOperator = '=',
  ) {}
}

export class ConditionalExpression {
  public readonly $kind = ekConditional;
  public constructor(
    public readonly condition: IsBinary,
    public readonly yes: IsAssign,
    public readonly no: IsAssign,
  ) {}
}

export class AccessGlobalExpression {
  public readonly $kind: 'AccessGlobal' = ekAccessGlobal;

  public constructor(
    public readonly name: string,
  ) {}
}

export class AccessThisExpression {
  public readonly $kind: 'AccessThis' = ekAccessThis;

  public constructor(
    public readonly ancestor: number = 0,
  ) {}
}

export class AccessBoundaryExpression {
  public readonly $kind: 'AccessBoundary' = ekAccessBoundary;
}

export class AccessScopeExpression {
  public readonly $kind = ekAccessScope;
  public constructor(
    public readonly name: string,
    public readonly ancestor: number = 0,
  ) {}
}

const isAccessGlobal = (ast: IsLeftHandSide) => (
  ast.$kind === ekAccessGlobal ||
  (
    ast.$kind === ekAccessMember ||
    ast.$kind === ekAccessKeyed
  ) && ast.accessGlobal
);

export class AccessMemberExpression {
  public readonly $kind: 'AccessMember' = ekAccessMember;
  public readonly accessGlobal: boolean;
  public constructor(
    public readonly object: IsLeftHandSide,
    public readonly name: string,
    public readonly optional: boolean = false,
  ) {
    this.accessGlobal = isAccessGlobal(object);
  }
}

export class AccessKeyedExpression {
  public readonly $kind = ekAccessKeyed;
  public readonly accessGlobal: boolean;
  public constructor(
    public readonly object: IsLeftHandSide,
    public readonly key: IsAssign,
    public readonly optional: boolean = false,
  ) {
    this.accessGlobal = isAccessGlobal(object);
  }
}

export class NewExpression {
  public readonly $kind = ekNew;
  public constructor(
    public readonly func: IsLeftHandSide,
    public readonly args: readonly IsAssign[],
  ) {}
}

export class CallScopeExpression {
  public readonly $kind = ekCallScope;
  public constructor(
    public readonly name: string,
    public readonly args: readonly IsAssign[],
    public readonly ancestor: number = 0,
    public readonly optional: boolean = false,
  ) {}
}

export class CallMemberExpression {
  public readonly $kind = ekCallMember;
  public constructor(
    public readonly object: IsLeftHandSide,
    public readonly name: string,
    public readonly args: readonly IsAssign[],
    public readonly optionalMember: boolean = false,
    public readonly optionalCall: boolean = false,
  ) {}
}

export class CallFunctionExpression {
  public readonly $kind = ekCallFunction;
  public constructor(
    public readonly func: IsLeftHandSide,
    public readonly args: readonly IsAssign[],
    public readonly optional: boolean = false,
  ) {}
}

export class CallGlobalExpression {
  public readonly $kind = ekCallGlobal;
  public constructor(
    public readonly name: string,
    public readonly args: readonly IsAssign[]
  ) {}
}

export class BinaryExpression {
  public readonly $kind: 'Binary' = ekBinary;
  public constructor(
    public readonly operation: BinaryOperator,
    public readonly left: IsBinary,
    public readonly right: IsBinary,
  ) {}
}

export class UnaryExpression {
  public readonly $kind = ekUnary;
  public constructor(
    public readonly operation: UnaryOperator,
    public readonly expression: IsLeftHandSide,
    public readonly pos: 0 | 1 = 0,
  ) {}
}
export class PrimitiveLiteralExpression<TValue extends null | undefined | number | boolean | string = null | undefined | number | boolean | string> {
  public static readonly $undefined: PrimitiveLiteralExpression<undefined> = new PrimitiveLiteralExpression<undefined>(void 0);
  public static readonly $null: PrimitiveLiteralExpression<null> = new PrimitiveLiteralExpression<null>(null);
  public static readonly $true: PrimitiveLiteralExpression<true> = new PrimitiveLiteralExpression<true>(true);
  public static readonly $false: PrimitiveLiteralExpression<false> = new PrimitiveLiteralExpression<false>(false);
  public static readonly $empty: PrimitiveLiteralExpression<string> = new PrimitiveLiteralExpression<''>('');
  public readonly $kind = ekPrimitiveLiteral;

  public constructor(
    public readonly value: TValue,
  ) {}
}

export class ArrayLiteralExpression {
  public static readonly $empty: ArrayLiteralExpression = new ArrayLiteralExpression(emptyArray);
  public readonly $kind = ekArrayLiteral;
  public constructor(
    public readonly elements: readonly IsAssign[],
  ) {}
}

export class ObjectLiteralExpression {
  public static readonly $empty: ObjectLiteralExpression = new ObjectLiteralExpression(emptyArray, emptyArray);
  public readonly $kind = ekObjectLiteral;
  public constructor(
    public readonly keys: readonly (number | string)[],
    public readonly values: readonly IsAssign[],
  ) {}
}

export class TemplateExpression {
  public static readonly $empty: TemplateExpression = new TemplateExpression(['']);
  public readonly $kind = ekTemplate;
  public constructor(
    public readonly cooked: readonly string[],
    public readonly expressions: readonly IsAssign[] = emptyArray,
  ) {}
}

export class TaggedTemplateExpression {
  public readonly $kind = ekTaggedTemplate;
  public constructor(
    public readonly cooked: readonly string[] & { raw?: readonly string[] },
    raw: readonly string[],
    public readonly func: IsLeftHandSide,
    public readonly expressions: readonly IsAssign[] = emptyArray,
  ) {
    cooked.raw = raw;
  }
}

export class ArrayBindingPattern {
  public readonly $kind = ekArrayBindingPattern;
  // We'll either have elements, or keys+values, but never all 3
  public constructor(
    public readonly elements: readonly IsAssign[],
  ) {}
}

export class ObjectBindingPattern {
  public readonly $kind = ekObjectBindingPattern;
  // We'll either have elements, or keys+values, but never all 3
  public constructor(
    public readonly keys: readonly (string | number)[],
    public readonly values: readonly IsAssign[],
  ) {}
}

export class BindingIdentifier {
  public readonly $kind = ekBindingIdentifier;
  public constructor(
    public readonly name: string,
  ) {}
}

// https://tc39.github.io/ecma262/#sec-iteration-statements
// https://tc39.github.io/ecma262/#sec-for-in-and-for-of-statements
export class ForOfStatement {
  public readonly $kind = ekForOfStatement;
  public constructor(
    public readonly declaration: BindingIdentifierOrPattern | DestructuringAssignmentExpression,
    public readonly iterable: IsBindingBehavior,
    public readonly semiIdx: number,
  ) {}
}

/*
* Note: this implementation is far simpler than the one in vCurrent and might be missing important stuff (not sure yet)
* so while this implementation is identical to Template and we could reuse that one, we don't want to lock outselves in to potentially the wrong abstraction
* but this class might be a candidate for removal if it turns out it does provide all we need
*/
export class Interpolation {
  public readonly $kind = ekInterpolation;
  public readonly isMulti: boolean;
  public readonly firstExpression: IsBindingBehavior;
  public constructor(
    public readonly parts: readonly string[],
    public readonly expressions: readonly IsBindingBehavior[] = emptyArray,
  ) {
    this.isMulti = expressions.length > 1;
    this.firstExpression = expressions[0];
  }
}

// spec: https://tc39.es/ecma262/#sec-destructuring-assignment
/** This is an internal API */
export class DestructuringAssignmentExpression {
  public constructor(
    public readonly $kind: 'ArrayDestructuring' | 'ObjectDestructuring',
    public readonly list: readonly (DestructuringAssignmentExpression | DestructuringAssignmentSingleExpression | DestructuringAssignmentRestExpression)[],
    public readonly source: AccessMemberExpression | AccessKeyedExpression | undefined,
    public readonly initializer: IsBindingBehavior | undefined,
  ) { }
}

/** This is an internal API */
export class DestructuringAssignmentSingleExpression {
  public readonly $kind = ekDestructuringAssignmentLeaf;
  public constructor(
    public readonly target: AccessMemberExpression,
    public readonly source: AccessMemberExpression | AccessKeyedExpression,
    public readonly initializer: IsBindingBehavior | undefined,
  ) { }
}

/** This is an internal API */
export class DestructuringAssignmentRestExpression {
  public readonly $kind = ekDestructuringAssignmentLeaf;
  public constructor(
    public readonly target: AccessMemberExpression,
    public readonly indexOrProperties: string[] | number,
  ) { }
}

export class ArrowFunction {
  public readonly $kind = ekArrowFunction;
  public constructor(
    public args: BindingIdentifier[],
    public body: IsAssign,
    public rest: boolean = false,
  ) {}
}
