// Copyright (c) 2014, 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.

/**
 * Code generation for the file "matchers.dart".
 */
library codegen.matchers;

import 'dart:convert';

import 'package:analyzer/src/codegen/tools.dart';
import 'package:front_end/src/codegen/tools.dart';

import 'api.dart';
import 'from_html.dart';
import 'implied_types.dart';
import 'to_html.dart';

final GeneratedFile target = new GeneratedFile(
    'test/integration/protocol_matchers.dart', (String pkgPath) {
  CodegenMatchersVisitor visitor = new CodegenMatchersVisitor(readApi(pkgPath));
  return visitor.collectCode(visitor.visitApi);
});

class CodegenMatchersVisitor extends HierarchicalApiVisitor with CodeGenerator {
  /**
   * Visitor used to produce doc comments.
   */
  final ToHtmlVisitor toHtmlVisitor;

  /**
   * Short human-readable string describing the context of the matcher being
   * created.
   */
  String context;

  CodegenMatchersVisitor(Api api)
      : toHtmlVisitor = new ToHtmlVisitor(api),
        super(api) {
    codeGeneratorSettings.commentLineLength = 79;
    codeGeneratorSettings.languageName = 'dart';
  }

  /**
   * Create a matcher for the part of the API called [name], optionally
   * clarified by [nameSuffix].  The matcher should verify that its input
   * matches the given [type].
   */
  void makeMatcher(ImpliedType impliedType) {
    context = impliedType.humanReadableName;
    docComment(toHtmlVisitor.collectHtml(() {
      toHtmlVisitor.p(() {
        toHtmlVisitor.write(context);
      });
      if (impliedType.type != null) {
        toHtmlVisitor.showType(null, impliedType.type);
      }
    }));
    write('final Matcher ${camelJoin(['is', impliedType.camelName])} = ');
    if (impliedType.type == null) {
      write('isNull');
    } else {
      visitTypeDecl(impliedType.type);
    }
    writeln(';');
    writeln();
  }

  /**
   * Generate a map describing the given set of fields, for use as the
   * 'requiredFields' or 'optionalFields' argument to the [MatchesJsonObject]
   * constructor.
   */
  void outputObjectFields(Iterable<TypeObjectField> fields) {
    if (fields.isEmpty) {
      write('null');
      return;
    }
    writeln('{');
    indent(() {
      bool commaNeeded = false;
      for (TypeObjectField field in fields) {
        if (commaNeeded) {
          writeln(',');
        }
        write('${JSON.encode(field.name)}: ');
        if (field.value != null) {
          write('equals(${JSON.encode(field.value)})');
        } else {
          visitTypeDecl(field.type);
        }
        commaNeeded = true;
      }
      writeln();
    });
    write('}');
  }

  @override
  visitApi() {
    outputHeader();
    writeln();
    writeln('/**');
    writeln(' * Matchers for data types defined in the analysis server API');
    writeln(' */');
    writeln('library test.integration.protocol.matchers;');
    writeln();
    writeln("import 'package:test/test.dart';");
    writeln();
    writeln("import 'integration_tests.dart';");
    writeln();
    writeln();
    for (ImpliedType impliedType in computeImpliedTypes(api).values) {
      makeMatcher(impliedType);
    }
  }

  @override
  visitTypeEnum(TypeEnum typeEnum) {
    writeln('new MatchesEnum(${JSON.encode(context)}, [');
    indent(() {
      bool commaNeeded = false;
      for (TypeEnumValue value in typeEnum.values) {
        if (commaNeeded) {
          writeln(',');
        }
        write('${JSON.encode(value.value)}');
        commaNeeded = true;
      }
      writeln();
    });
    write('])');
  }

  @override
  visitTypeList(TypeList typeList) {
    write('isListOf(');
    visitTypeDecl(typeList.itemType);
    write(')');
  }

  @override
  visitTypeMap(TypeMap typeMap) {
    write('isMapOf(');
    visitTypeDecl(typeMap.keyType);
    write(', ');
    visitTypeDecl(typeMap.valueType);
    write(')');
  }

  @override
  void visitTypeObject(TypeObject typeObject) {
    writeln('new LazyMatcher(() => new MatchesJsonObject(');
    indent(() {
      write('${JSON.encode(context)}, ');
      Iterable<TypeObjectField> requiredFields =
          typeObject.fields.where((TypeObjectField field) => !field.optional);
      outputObjectFields(requiredFields);
      List<TypeObjectField> optionalFields = typeObject.fields
          .where((TypeObjectField field) => field.optional)
          .toList();
      if (optionalFields.isNotEmpty) {
        write(', optionalFields: ');
        outputObjectFields(optionalFields);
      }
    });
    write('))');
  }

  @override
  void visitTypeReference(TypeReference typeReference) {
    String typeName = typeReference.typeName;
    if (typeName == 'long') {
      typeName = 'int';
    }
    write(camelJoin(['is', typeName]));
  }

  @override
  void visitTypeUnion(TypeUnion typeUnion) {
    bool commaNeeded = false;
    write('isOneOf([');
    for (TypeDecl choice in typeUnion.choices) {
      if (commaNeeded) {
        write(', ');
      }
      visitTypeDecl(choice);
      commaNeeded = true;
    }
    write('])');
  }
}
