import { Assert, Parser } from '../index.mts';
import { isArray, OutOfRange } from '../helpers.mts';
import type { TokenData } from './Lexer.mts';
import type { ParseNode } from './ParseNode.mts';

export enum Flag {
  return = 1 << 0,
  await = 1 << 1,
  yield = 1 << 2,
  parameters = 1 << 3,
  newTarget = 1 << 4,
  importMeta = 1 << 5,
  superCall = 1 << 6,
  superProperty = 1 << 7,
  in = 1 << 8,
  default = 1 << 9,
  module = 1 << 10,
  classStaticBlock = 1 << 11,
}

export interface DeclarationInfo {
  readonly name: string;
  readonly node: ParseNode;
}

export function getDeclarations(node: ParseNode | readonly ParseNode[]): DeclarationInfo[] {
  if (isArray(node)) {
    return node.flatMap((n) => getDeclarations(n));
  }
  switch (node.type) {
    case 'LexicalBinding':
    case 'VariableDeclaration':
    case 'BindingRestElement':
    case 'ForBinding':
      if (node.BindingIdentifier) {
        return getDeclarations(node.BindingIdentifier);
      }
      if (node.BindingPattern) {
        return getDeclarations(node.BindingPattern);
      }
      return [];
    case 'BindingRestProperty':
      if (node.BindingIdentifier) {
        return getDeclarations(node.BindingIdentifier);
      }
      return [];
    case 'SingleNameBinding':
      return getDeclarations(node.BindingIdentifier);
    case 'ImportClause': {
      const d = [];
      if (node.ImportedDefaultBinding) {
        d.push(...getDeclarations(node.ImportedDefaultBinding));
      }
      if (node.NameSpaceImport) {
        d.push(...getDeclarations(node.NameSpaceImport));
      }
      if (node.NamedImports) {
        d.push(...getDeclarations(node.NamedImports));
      }
      return d;
    }
    case 'ImportSpecifier':
      return getDeclarations(node.ImportedBinding);
    case 'ImportedDefaultBinding':
    case 'NameSpaceImport':
      return getDeclarations(node.ImportedBinding);
    case 'NamedImports':
      return getDeclarations(node.ImportsList);
    case 'ObjectBindingPattern': {
      const declarations = getDeclarations(node.BindingPropertyList);
      if (node.BindingRestProperty) {
        declarations.push(...getDeclarations(node.BindingRestProperty));
      }
      return declarations;
    }
    case 'ArrayBindingPattern': {
      const declarations = getDeclarations(node.BindingElementList);
      if (node.BindingRestElement) {
        declarations.push(...getDeclarations(node.BindingRestElement));
      }
      return declarations;
    }
    case 'BindingElement':
      return getDeclarations(node.BindingPattern);
    case 'BindingProperty':
      return getDeclarations(node.BindingElement);
    case 'BindingIdentifier':
    case 'IdentifierName':
    case 'LabelIdentifier':
      return [{ name: node.name, node }];
    case 'PrivateIdentifier':
      return [{ name: `#${node.name}`, node }];
    case 'StringLiteral':
      return [{ name: node.value, node }];
    case 'Elision':
      return [];
    case 'ForDeclaration':
      return getDeclarations(node.ForBinding);
    case 'ExportSpecifier':
      return getDeclarations(node.exportName);
    case 'FunctionDeclaration':
    case 'GeneratorDeclaration':
    case 'AsyncFunctionDeclaration':
    case 'AsyncGeneratorDeclaration':
      Assert(!!node.BindingIdentifier);
      return getDeclarations(node.BindingIdentifier);
    case 'LexicalDeclaration':
      return getDeclarations(node.BindingList);
    case 'VariableStatement':
      return getDeclarations(node.VariableDeclarationList);
    case 'ClassDeclaration':
      Assert(!!node.BindingIdentifier);
      return getDeclarations(node.BindingIdentifier);
    default:
      throw new OutOfRange('getDeclarations', node);
  }
}

export type ScopeFlagSetters =
  & { readonly [P in (keyof typeof Flag) & string]?: boolean; }
  & {
    readonly lexical?: boolean;
    readonly variable?: boolean;
    readonly variableFunctions?: boolean;
    readonly private?: boolean;
    readonly label?: LabelType | 'boundary';
    readonly strict?: boolean;
  };

export interface ScopeInfo {
  readonly flags: ScopeFlagSetters;
  readonly lexicals: Set<string>;
  readonly variables: Set<string>;
  readonly functions: Set<string>;
  readonly parameters: Set<string>;
}

export interface PrivateScopeInfo {
  readonly outer: PrivateScopeInfo | undefined;
  readonly names: Map<string, Set<'field' | 'method' | 'get' | 'set'>>;
}

export interface UndefinedPrivateAccessInfo {
  readonly node: ParseNode;
  readonly name: string;
  readonly scope: PrivateScopeInfo | undefined;
}

export interface ArrowInfo {
  readonly isAsync: boolean;
  hasTrailingComma: boolean;
  readonly yieldExpressions: ParseNode[];
  readonly awaitExpressions: ParseNode[];
  readonly awaitIdentifiers: ParseNode[];
  merge(other: ArrowInfo): void;
}

export interface AssignmentInfo {
  readonly type: 'assign' | 'arrow' | 'for';
  readonly earlyErrors: SyntaxError[];
  clear(): void;
}

export type LabelType = 'switch' | 'loop';

export interface Label {
  type: LabelType | null;
  readonly name?: string;
  readonly nextToken?: TokenData | null;
}

export class Scope {
  private readonly parser: Parser;

  private readonly scopeStack: ScopeInfo[] = [];

  labels: Label[] = [];

  readonly arrowInfoStack: (ArrowInfo | null)[] = [];

  readonly assignmentInfoStack: AssignmentInfo[] = [];

  readonly exports = new Set<string>();

  readonly undefinedExports = new Map<string, ParseNode.ModuleExportName>();

  privateScope: PrivateScopeInfo | undefined;

  private readonly undefinedPrivateAccesses: UndefinedPrivateAccessInfo[] = [];

  private flags: Flag = 0 as Flag;

  constructor(parser: Parser) {
    this.parser = parser;
  }

  hasReturn() {
    return (this.flags & Flag.return) !== 0;
  }

  hasAwait() {
    return (this.flags & Flag.await) !== 0;
  }

  hasYield() {
    return (this.flags & Flag.yield) !== 0;
  }

  hasNewTarget() {
    return (this.flags & Flag.newTarget) !== 0;
  }

  hasSuperCall() {
    return (this.flags & Flag.superCall) !== 0;
  }

  hasSuperProperty() {
    return (this.flags & Flag.superProperty) !== 0;
  }

  hasImportMeta() {
    return (this.flags & Flag.importMeta) !== 0;
  }

  hasIn() {
    return (this.flags & Flag.in) !== 0;
  }

  inParameters() {
    return (this.flags & Flag.parameters) !== 0;
  }

  inClassStaticBlock() {
    return (this.flags & Flag.classStaticBlock) !== 0;
  }

  isDefault() {
    return (this.flags & Flag.default) !== 0;
  }

  isModule() {
    return (this.flags & Flag.module) !== 0;
  }

  with<R>(flags: ScopeFlagSetters, f: () => R) {
    const oldFlags = this.flags;

    Object.entries(flags)
      .forEach(([k, v]) => {
        if (k in Flag && typeof Flag[k as keyof typeof Flag] === 'number') {
          if (v === true) {
            this.flags |= Flag[k as keyof typeof Flag];
          } else if (v === false) {
            this.flags &= ~Flag[k as keyof typeof Flag];
          }
        }
      });

    if (flags.lexical || flags.variable) {
      this.scopeStack.push({
        flags,
        lexicals: new Set(),
        variables: new Set(),
        functions: new Set(),
        parameters: new Set(),
      });
    }

    if (flags.private) {
      this.privateScope = {
        outer: this.privateScope,
        names: new Map(),
      };
    }

    const oldLabels = this.labels;
    if (flags.label === 'boundary') {
      this.labels = [];
    } else if (flags.label) {
      this.labels.push({ type: flags.label });
    }

    const oldStrict = this.parser.state.strict;
    if (flags.strict === true) {
      this.parser.state.strict = true;
    } else if (flags.strict === false) {
      this.parser.state.strict = false;
    }

    const r = f();

    if (flags.label === 'boundary') {
      this.labels = oldLabels;
    } else if (flags.label) {
      this.labels.pop();
    }

    if (flags.private) {
      this.privateScope = this.privateScope!.outer;

      if (this.privateScope === undefined) {
        this.undefinedPrivateAccesses.forEach(({ node, name, scope }) => {
          while (scope) {
            if (scope.names.has(name)) {
              return;
            }
            scope = scope.outer;
          }
          this.parser.raiseEarly('NotDefined', node, name);
        });
      }
    }

    if (flags.lexical || flags.variable) {
      this.scopeStack.pop();
    }

    this.parser.state.strict = oldStrict;
    this.flags = oldFlags;

    return r;
  }

  pushArrowInfo(isAsync = false) {
    this.arrowInfoStack.push({
      isAsync,
      hasTrailingComma: false,
      yieldExpressions: [],
      awaitExpressions: [],
      awaitIdentifiers: [],
      merge(other) {
        this.yieldExpressions.push(...other.yieldExpressions);
        this.awaitExpressions.push(...other.awaitExpressions);
        this.awaitIdentifiers.push(...other.awaitIdentifiers);
      },
    });
  }

  popArrowInfo() {
    const arrowInfo = this.arrowInfoStack.pop();
    Assert(!!arrowInfo);
    return arrowInfo;
  }

  get arrowInfo() {
    if (this.arrowInfoStack.length > 0) {
      return this.arrowInfoStack[this.arrowInfoStack.length - 1];
    }
    return undefined;
  }

  pushAssignmentInfo(type: 'assign' | 'arrow' | 'for') {
    const parser = this.parser;
    this.assignmentInfoStack.push({
      type,
      earlyErrors: [],
      clear() {
        this.earlyErrors.forEach((e) => {
          parser.earlyErrors.delete(e);
        });
      },
    });
  }

  popAssignmentInfo() {
    const assignmentInfo = this.assignmentInfoStack.pop();
    Assert(!!assignmentInfo);
    return assignmentInfo;
  }

  registerObjectLiteralEarlyError(error: SyntaxError) {
    for (let i = this.assignmentInfoStack.length - 1; i >= 0; i -= 1) {
      const info = this.assignmentInfoStack[i];
      info.earlyErrors.push(error);
      if (info.type !== 'assign') {
        break;
      }
    }
  }

  lexicalScope() {
    for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
      const scope = this.scopeStack[i];
      if (scope.flags.lexical) {
        return scope;
      }
    }
    /* node:coverage ignore next */
    throw new RangeError();
  }

  variableScope() {
    for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
      const scope = this.scopeStack[i];
      if (scope.flags.variable) {
        return scope;
      }
    }
    /* node:coverage ignore next */
    throw new RangeError();
  }

  declare(node: ParseNode | readonly ParseNode[], type: 'private', extraType?: 'field' | 'method' | 'get' | 'set'): void;

  declare(node: ParseNode | readonly ParseNode[], type: 'lexical' | 'import' | 'function' | 'parameter' | 'variable' | 'export'): void;

  declare(node: ParseNode | readonly ParseNode[], type: 'lexical' | 'import' | 'function' | 'parameter' | 'variable' | 'export' | 'private', extraType?: 'field' | 'method' | 'get' | 'set') {
    const declarations = getDeclarations(node);
    declarations.forEach((d) => {
      switch (type) {
        case 'lexical':
        case 'import': {
          if (type === 'lexical' && d.name === 'let') {
            this.parser.raiseEarly('LetInLexicalBinding', d.node);
          }
          const scope = this.lexicalScope();
          if (scope.lexicals.has(d.name)
              || scope.variables.has(d.name)
              || scope.functions.has(d.name)
              || scope.parameters.has(d.name)) {
            this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
          }
          scope.lexicals.add(d.name);
          if (scope === this.scopeStack[0] && this.undefinedExports.has(d.name)) {
            this.undefinedExports.delete(d.name);
          }
          break;
        }
        case 'function': {
          const scope = this.lexicalScope();
          if (scope.lexicals.has(d.name)) {
            this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
          }
          if (scope.flags.variableFunctions) {
            scope.functions.add(d.name);
          } else {
            if (scope.variables.has(d.name)) {
              this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
            }
            scope.lexicals.add(d.name);
          }
          if (scope === this.scopeStack[0] && this.undefinedExports.has(d.name)) {
            this.undefinedExports.delete(d.name);
          }
          break;
        }
        case 'parameter':
          this.variableScope().parameters.add(d.name);
          break;
        case 'variable':
          for (let i = this.scopeStack.length - 1; i >= 0; i -= 1) {
            const scope = this.scopeStack[i];
            scope.variables.add(d.name);
            if (scope.lexicals.has(d.name) || (!scope.flags.variableFunctions && scope.functions.has(d.name))) {
              this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
            }
            if (i === 0 && this.undefinedExports.has(d.name)) {
              this.undefinedExports.delete(d.name);
            }
            if (scope.flags.variable) {
              break;
            }
          }
          break;
        case 'export':
          if (this.exports.has(d.name)) {
            this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
          } else {
            this.exports.add(d.name);
          }
          break;
        case 'private': {
          const types = this.privateScope!.names.get(d.name);
          if (types) {
            let duplicate = true;
            switch (extraType) {
              case 'field':
              case 'method':
                break;
              case 'set':
              case 'get':
                duplicate = types.has(extraType) || types.has('field') || types.has('method');
                types.add(extraType);
                break;
              default:
                break;
            }
            if (duplicate) {
              this.parser.raiseEarly('AlreadyDeclared', d.node, d.name);
            }
          } else if (extraType) {
            this.privateScope!.names.set(d.name, new Set([extraType]));
          }
          break;
        }
        /* node:coverage ignore next 2 */
        default:
          throw new RangeError(type);
      }
    });
  }

  checkUndefinedExports(NamedExports: ParseNode.NamedExports) {
    const scope = this.variableScope();
    NamedExports.ExportsList.forEach((n) => {
      const name = n.localName.type === 'IdentifierName' ? n.localName.name : n.localName.value;
      if (!scope.lexicals.has(name) && !scope.variables.has(name)) {
        this.undefinedExports.set(name, n.localName);
      }
    });
  }

  checkUndefinedPrivate(PrivateIdentifier: ParseNode.PrivateIdentifier) {
    if (this.parser.state.allowAllPrivateNames) {
      return;
    }
    const [{ node, name }] = getDeclarations(PrivateIdentifier);

    if (!this.privateScope) {
      this.parser.raiseEarly('NotDefined', node, name);
      return;
    }

    let scope: PrivateScopeInfo | undefined = this.privateScope;
    while (scope) {
      if (scope.names.has(name)) {
        return;
      }
      scope = scope.outer;
    }

    this.undefinedPrivateAccesses.push({
      node,
      name,
      scope: this.privateScope,
    });
  }
}
