/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  ArrowFunction,
  AST,
  AstVisitor,
  ASTWithSource,
  Binary,
  BindingPipe,
  Call,
  Chain,
  Conditional,
  ImplicitReceiver,
  Interpolation,
  KeyedRead,
  LiteralArray,
  LiteralMap,
  LiteralPrimitive,
  NonNullAssert,
  ParenthesizedExpression,
  PrefixNot,
  PropertyRead,
  RecursiveAstVisitor,
  RegularExpressionLiteral,
  SafeCall,
  SafeKeyedRead,
  SafePropertyRead,
  SpreadElement,
  TaggedTemplateLiteral,
  TemplateLiteral,
  TemplateLiteralElement,
  ThisReceiver,
  TypeofExpression,
  Unary,
  VoidExpression,
} from '../../../src/expression_parser/ast';

class Unparser implements AstVisitor {
  private static _quoteRegExp = /"/g;
  // using non-null assertion because they're both re(set) by unparse()
  private _expression!: string;

  unparse(ast: AST) {
    this._expression = '';
    this._visit(ast);
    return this._expression;
  }

  visitPropertyRead(ast: PropertyRead, context: any) {
    this._visit(ast.receiver);
    this._expression +=
      ast.receiver instanceof ImplicitReceiver || ast.receiver instanceof ThisReceiver
        ? `${ast.name}`
        : `.${ast.name}`;
  }

  visitUnary(ast: Unary, context: any) {
    this._expression += ast.operator;
    this._visit(ast.expr);
  }

  visitBinary(ast: Binary, context: any) {
    this._visit(ast.left);
    this._expression += ` ${ast.operation} `;
    this._visit(ast.right);
  }

  visitChain(ast: Chain, context: any) {
    const len = ast.expressions.length;
    for (let i = 0; i < len; i++) {
      this._visit(ast.expressions[i]);
      this._expression += i == len - 1 ? ';' : '; ';
    }
  }

  visitConditional(ast: Conditional, context: any) {
    this._visit(ast.condition);
    this._expression += ' ? ';
    this._visit(ast.trueExp);
    this._expression += ' : ';
    this._visit(ast.falseExp);
  }

  visitPipe(ast: BindingPipe, context: any) {
    this._expression += '(';
    this._visit(ast.exp);
    this._expression += ` | ${ast.name}`;
    ast.args.forEach((arg) => {
      this._expression += ':';
      this._visit(arg);
    });
    this._expression += ')';
  }

  visitCall(ast: Call, context: any) {
    this._visit(ast.receiver);
    this._expression += '(';
    let isFirst = true;
    ast.args.forEach((arg) => {
      if (!isFirst) this._expression += ', ';
      isFirst = false;
      this._visit(arg);
    });
    this._expression += ')';
  }

  visitSafeCall(ast: SafeCall, context: any) {
    this._visit(ast.receiver);
    this._expression += '?.(';
    let isFirst = true;
    ast.args.forEach((arg) => {
      if (!isFirst) this._expression += ', ';
      isFirst = false;
      this._visit(arg);
    });
    this._expression += ')';
  }

  visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}

  visitThisReceiver(ast: ThisReceiver, context: any) {}

  visitInterpolation(ast: Interpolation, context: any) {
    for (let i = 0; i < ast.strings.length; i++) {
      this._expression += ast.strings[i];
      if (i < ast.expressions.length) {
        this._expression += `{{ `;
        this._visit(ast.expressions[i]);
        this._expression += ` }}`;
      }
    }
  }

  visitKeyedRead(ast: KeyedRead, context: any) {
    this._visit(ast.receiver);
    this._expression += '[';
    this._visit(ast.key);
    this._expression += ']';
  }

  visitLiteralArray(ast: LiteralArray, context: any) {
    this._expression += '[';
    let isFirst = true;
    ast.expressions.forEach((expression) => {
      if (!isFirst) this._expression += ', ';
      isFirst = false;
      this._visit(expression);
    });

    this._expression += ']';
  }

  visitLiteralMap(ast: LiteralMap, context: any) {
    this._expression += '{';
    let isFirst = true;
    for (let i = 0; i < ast.keys.length; i++) {
      if (!isFirst) this._expression += ', ';
      isFirst = false;
      const key = ast.keys[i];

      if (key.kind === 'spread') {
        this._expression += '...';
      } else {
        this._expression += key.quoted ? JSON.stringify(key.key) : key.key;
        this._expression += ': ';
      }

      this._visit(ast.values[i]);
    }

    this._expression += '}';
  }

  visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {
    if (typeof ast.value === 'string') {
      this._expression += `"${ast.value.replace(Unparser._quoteRegExp, '"')}"`;
    } else {
      this._expression += `${ast.value}`;
    }
  }

  visitPrefixNot(ast: PrefixNot, context: any) {
    this._expression += '!';
    this._visit(ast.expression);
  }

  visitTypeofExpression(ast: TypeofExpression, context: any) {
    this._expression += 'typeof ';
    this._visit(ast.expression);
  }

  visitVoidExpression(ast: VoidExpression, context: any) {
    this._expression += 'void ';
    this._visit(ast.expression);
  }

  visitNonNullAssert(ast: NonNullAssert, context: any) {
    this._visit(ast.expression);
    this._expression += '!';
  }

  visitSafePropertyRead(ast: SafePropertyRead, context: any) {
    this._visit(ast.receiver);
    this._expression += `?.${ast.name}`;
  }

  visitSafeKeyedRead(ast: SafeKeyedRead, context: any) {
    this._visit(ast.receiver);
    this._expression += '?.[';
    this._visit(ast.key);
    this._expression += ']';
  }

  visitTemplateLiteral(ast: TemplateLiteral, context: any) {
    this._expression += '`';
    for (let i = 0; i < ast.elements.length; i++) {
      this._visit(ast.elements[i]);
      const expression = i < ast.expressions.length ? ast.expressions[i] : null;
      if (expression !== null) {
        this._expression += '${';
        this._visit(expression);
        this._expression += '}';
      }
    }
    this._expression += '`';
  }

  visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
    this._expression += ast.text;
  }

  visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral, context: any) {
    this._visit(ast.tag);
    this._visit(ast.template);
  }

  visitParenthesizedExpression(ast: ParenthesizedExpression, context: any) {
    this._expression += '(';
    this._visit(ast.expression);
    this._expression += ')';
  }

  visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
    this._expression += `/${ast.body}/${ast.flags || ''}`;
  }

  visitSpreadElement(ast: SpreadElement, context: any) {
    this._expression += '...';
    this._visit(ast.expression);
  }

  visitArrowFunction(ast: ArrowFunction, context: any) {
    if (ast.parameters.length === 1) {
      this._expression += ast.parameters[0].name;
    } else {
      this._expression += `(${ast.parameters.map((e) => e.name).join(', ')})`;
    }
    this._expression += ' => ';
    this._visit(ast.body);
  }

  private _visit(ast: AST) {
    ast.visit(this);
  }
}

const sharedUnparser = new Unparser();

export function unparse(ast: AST): string {
  return sharedUnparser.unparse(ast);
}

// [unparsed AST, original source code of AST]
type UnparsedWithSpan = [string, string];

export function unparseWithSpan(ast: ASTWithSource): UnparsedWithSpan[] {
  const unparsed: UnparsedWithSpan[] = [];
  const source = ast.source!;
  const recursiveSpanUnparser = new (class extends RecursiveAstVisitor {
    private recordUnparsed(ast: any, spanKey: string, unparsedList: UnparsedWithSpan[]) {
      const span = ast[spanKey];
      const prefix = spanKey === 'span' ? '' : `[${spanKey}] `;
      const src = source.substring(span.start, span.end);
      unparsedList.push([unparse(ast), prefix + src]);
    }

    override visit(ast: AST, unparsedList: UnparsedWithSpan[]) {
      this.recordUnparsed(ast, 'span', unparsedList);
      if (ast.hasOwnProperty('nameSpan')) {
        this.recordUnparsed(ast, 'nameSpan', unparsedList);
      }
      if (ast.hasOwnProperty('argumentSpan')) {
        this.recordUnparsed(ast, 'argumentSpan', unparsedList);
      }
      ast.visit(this, unparsedList);
    }
  })();
  recursiveSpanUnparser.visitAll([ast.ast], unparsed);
  return unparsed;
}
