/**
 * @license
 * Copyright Google Inc. 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.io/license
 */

import {InjectFlags} from './core';
import {Identifiers} from './identifiers';
import * as o from './output/output_ast';
import {R3DependencyMetadata, compileFactoryFunction} from './render3/r3_factory';
import {mapToMapExpression} from './render3/util';

export interface InjectableDef {
  expression: o.Expression;
  type: o.Type;
}

export interface R3InjectableMetadata {
  name: string;
  type: o.Expression;
  providedIn: o.Expression;
  useClass?: o.Expression;
  useFactory?: o.Expression;
  useExisting?: o.Expression;
  useValue?: o.Expression;
  deps?: R3DependencyMetadata[];
}

export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
  let factory: o.Expression = o.NULL_EXPR;

  function makeFn(ret: o.Expression): o.Expression {
    return o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`);
  }

  if (meta.useClass !== undefined || meta.useFactory !== undefined) {
    // First, handle useClass and useFactory together, since both involve a similar call to
    // `compileFactoryFunction`. Either dependencies are explicitly specified, in which case
    // a factory function call is generated, or they're not specified and the calls are special-
    // cased.
    if (meta.deps !== undefined) {
      // Either call `new meta.useClass(...)` or `meta.useFactory(...)`.
      const fnOrClass: o.Expression = meta.useClass || meta.useFactory !;

      // useNew: true if meta.useClass, false for meta.useFactory.
      const useNew = meta.useClass !== undefined;

      factory = compileFactoryFunction({
        name: meta.name,
        fnOrClass,
        useNew,
        injectFn: Identifiers.inject,
        deps: meta.deps,
      });
    } else if (meta.useClass !== undefined) {
      // Special case for useClass where the factory from the class's ngInjectableDef is used.
      if (meta.useClass.isEquivalent(meta.type)) {
        // For the injectable compiler, useClass represents a foreign type that should be
        // instantiated to satisfy construction of the given type. It's not valid to specify
        // useClass === type, since the useClass type is expected to already be compiled.
        throw new Error(
            `useClass is the same as the type, but no deps specified, which is invalid.`);
      }
      factory =
          makeFn(new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory')
                     .callFn([]));
    } else if (meta.useFactory !== undefined) {
      // Special case for useFactory where no arguments are passed.
      factory = meta.useFactory.callFn([]);
    } else {
      // Can't happen - outer conditional guards against both useClass and useFactory being
      // undefined.
      throw new Error('Reached unreachable block in injectable compiler.');
    }
  } else if (meta.useValue !== undefined) {
    // Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for
    // client code because meta.useValue is an Expression which will be defined even if the actual
    // value is undefined.
    factory = makeFn(meta.useValue);
  } else if (meta.useExisting !== undefined) {
    // useExisting is an `inject` call on the existing token.
    factory = makeFn(o.importExpr(Identifiers.inject).callFn([meta.useExisting]));
  } else {
    // A strict type is compiled according to useClass semantics, except the dependencies are
    // required.
    if (meta.deps === undefined) {
      throw new Error(`Type compilation of an injectable requires dependencies.`);
    }
    factory = compileFactoryFunction({
      name: meta.name,
      fnOrClass: meta.type,
      useNew: true,
      injectFn: Identifiers.inject,
      deps: meta.deps,
    });
  }

  const token = meta.type;
  const providedIn = meta.providedIn;

  const expression = o.importExpr(Identifiers.defineInjectable).callFn([mapToMapExpression(
      {token, factory, providedIn})]);
  const type = new o.ExpressionType(
      o.importExpr(Identifiers.InjectableDef, [new o.ExpressionType(meta.type)]));

  return {
      expression, type,
  };
}
