/*!
 * @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 {DirectiveOwner, MatchSource, ParseSourceSpan, TmplAstHostElement} from '@angular/compiler';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {declareVariable, TcbExpr} from './codegen';
import {TcbDirectiveMetadata} from '../../api';
import {ExpressionIdentifier} from '../comments';

/**
 * A `TcbOp` which constructs an instance of a directive. For generic directives, generic
 * parameters are set to `any` type.
 */
export abstract class TcbDirectiveTypeOpBase extends TcbOp {
  constructor(
    protected tcb: Context,
    protected scope: Scope,
    protected node: DirectiveOwner,
    protected dir: TcbDirectiveMetadata,
    protected directiveIndex?: number,
  ) {
    super();
  }

  override get optional() {
    // The statement generated by this operation is only used to declare the directive's type and
    // won't report diagnostics by itself, so the operation is marked as optional to avoid
    // generating declarations for directives that don't have any inputs/outputs.
    return true;
  }

  override execute(): TcbExpr {
    const rawType = this.tcb.env.referenceTcbValue(this.dir.ref);

    let type: TcbExpr;
    let span: ParseSourceSpan;
    if (
      this.dir.isGeneric === false ||
      this.dir.typeParameters === null ||
      this.dir.typeParameters.length === 0
    ) {
      type = rawType;
    } else {
      const typeArguments = Array(this.dir.typeParameters?.length ?? 0)
        .fill('any')
        .join(', ');
      type = new TcbExpr(`${rawType.print()}<${typeArguments}>`);
    }

    if (this.node instanceof TmplAstHostElement) {
      span = this.node.sourceSpan;
    } else {
      span = this.node.startSourceSpan || this.node.sourceSpan;
    }

    const identifier =
      this.dir.matchSource === MatchSource.HostDirective
        ? ExpressionIdentifier.HOST_DIRECTIVE
        : ExpressionIdentifier.DIRECTIVE;
    const id = new TcbExpr(this.tcb.allocateId())
      .addExpressionIdentifier(identifier, this.directiveIndex)
      .addParseSpanInfo(span);
    this.scope.addStatement(declareVariable(id, type));
    return id;
  }
}

/**
 * A `TcbOp` which constructs an instance of a non-generic directive _without_ setting any of its
 * inputs. Inputs are later set in the `TcbDirectiveInputsOp`. Type checking was found to be
 * faster when done in this way as opposed to `TcbDirectiveCtorOp` which is only necessary when the
 * directive is generic.
 *
 * Executing this operation returns a reference to the directive instance variable with its inferred
 * type.
 */
export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
  /**
   * Creates a variable declaration for this op's directive of the argument type. Returns the id of
   * the newly created variable.
   */
  override execute(): TcbExpr {
    if (this.dir.isGeneric) {
      throw new Error(`Assertion Error: expected ${this.dir.ref.name} not to be generic.`);
    }
    return super.execute();
  }
}

/**
 * A `TcbOp` which constructs an instance of a generic directive with its generic parameters set
 * to `any` type. This op is like `TcbDirectiveTypeOp`, except that generic parameters are set to
 * `any` type. This is used for situations where we want to avoid inlining.
 *
 * Executing this operation returns a reference to the directive instance variable with its generic
 * type parameters set to `any`.
 */
export class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase {
  override execute(): TcbExpr {
    if (this.dir.typeParameters === null || this.dir.typeParameters.length === 0) {
      throw new Error(
        `Assertion Error: expected typeParameters when creating a declaration for ${this.dir.ref.name}`,
      );
    }

    return super.execute();
  }
}
