// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library fasta.kernel_function_type_alias_builder;

import 'package:kernel/ast.dart'
    show
        DartType,
        DynamicType,
        FunctionType,
        InvalidType,
        TypeParameter,
        Typedef;

import 'package:kernel/type_algebra.dart' show substitute;

import '../fasta_codes.dart'
    show noLength, templateCyclicTypedef, templateTypeArgumentMismatch;

import '../problems.dart' show unhandled;

import 'kernel_builder.dart'
    show
        FunctionTypeAliasBuilder,
        KernelFunctionTypeBuilder,
        KernelTypeBuilder,
        KernelTypeVariableBuilder,
        LibraryBuilder,
        MetadataBuilder,
        TypeVariableBuilder;

class KernelFunctionTypeAliasBuilder
    extends FunctionTypeAliasBuilder<KernelFunctionTypeBuilder, DartType> {
  final Typedef target;

  DartType thisType;

  KernelFunctionTypeAliasBuilder(
      List<MetadataBuilder> metadata,
      String name,
      List<TypeVariableBuilder> typeVariables,
      KernelFunctionTypeBuilder type,
      LibraryBuilder parent,
      int charOffset,
      [Typedef target])
      : target = target ??
            (new Typedef(name, null, fileUri: parent.target.fileUri)
              ..fileOffset = charOffset),
        super(metadata, name, typeVariables, type, parent, charOffset);

  Typedef build(LibraryBuilder libraryBuilder) {
    return target..type ??= buildThisType(libraryBuilder);
  }

  DartType buildThisType(LibraryBuilder library) {
    if (thisType != null) {
      if (const InvalidType() == thisType) {
        library.addCompileTimeError(templateCyclicTypedef.withArguments(name),
            charOffset, noLength, fileUri);
        return const DynamicType();
      }
      return thisType;
    }
    thisType = const InvalidType();
    FunctionType builtType = type?.build(library);
    if (builtType != null) {
      builtType.typedefReference = target.reference;
      if (typeVariables != null) {
        for (KernelTypeVariableBuilder tv in typeVariables) {
          // Follow bound in order to find all cycles
          tv.bound?.build(library);
          target.typeParameters.add(tv.parameter..parent = target);
        }
      }
      return thisType = builtType;
    } else {
      return thisType = const DynamicType();
    }
  }

  /// [arguments] have already been built.
  DartType buildTypesWithBuiltArguments(
      LibraryBuilder library, List<DartType> arguments) {
    var thisType = buildThisType(library);
    if (const DynamicType() == thisType) return thisType;
    FunctionType result = thisType;
    if (target.typeParameters.isEmpty && arguments == null) return result;
    Map<TypeParameter, DartType> substitution = <TypeParameter, DartType>{};
    for (int i = 0; i < target.typeParameters.length; i++) {
      substitution[target.typeParameters[i]] = arguments[i];
    }
    return substitute(result, substitution);
  }

  List<DartType> buildTypeArguments(
      LibraryBuilder library, List<KernelTypeBuilder> arguments) {
    if (arguments == null && typeVariables == null) {
      return <DartType>[];
    }

    if (arguments == null && typeVariables != null) {
      List<DartType> result =
          new List<DartType>.filled(typeVariables.length, null, growable: true);
      for (int i = 0; i < result.length; ++i) {
        result[i] = typeVariables[i].defaultType.build(library);
      }
      return result;
    }

    if (arguments != null && arguments.length != (typeVariables?.length ?? 0)) {
      // That should be caught and reported as a compile-time error earlier.
      return unhandled(
          templateTypeArgumentMismatch
              .withArguments(name, typeVariables.length)
              .message,
          "buildTypeArguments",
          -1,
          null);
    }

    // arguments.length == typeVariables.length
    List<DartType> result =
        new List<DartType>.filled(arguments.length, null, growable: true);
    for (int i = 0; i < result.length; ++i) {
      result[i] = arguments[i].build(library);
    }
    return result;
  }

  /// If [arguments] are null, the default types for the variables are used.
  @override
  int get typeVariablesCount => typeVariables?.length ?? 0;

  @override
  DartType buildType(
      LibraryBuilder library, List<KernelTypeBuilder> arguments) {
    var thisType = buildThisType(library);
    if (thisType is DynamicType) return thisType;
    FunctionType result = thisType;
    if (target.typeParameters.isEmpty && arguments == null) return result;
    // Otherwise, substitute.
    return buildTypesWithBuiltArguments(
        library, buildTypeArguments(library, arguments));
  }
}
