// Copyright (c) 2017, 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.
//
// This transformations outputs a Coq definitions to resemble the Dart syntax
// tree.
//
// Classes that are supposed to be converted are marked with the `@coq`
// annotation. Some fields within these classes will be converted according to
// the following rules:
//
// 1. A field labeled `@nocoq` will not be converted.
//
// 2. A field labeled `@coq` or `@coqopt` will be converted unless it's type is
// unuspported, in which case an exception will be raised.
//
// 3. An unannotated field will be converted if it has type `T` or `List<T>`,
// where `T` is a class marked for conversion.
//
// Classes marked `@coqref` are referenced by natural numbered IDs wherever
// fields of their type appear, and we have a finite map resolving these IDs.
// This breaks cycles in the AST graph and provides a way to identify nodes for
// substitution.
//
// All classes with data members are given an induction type definition with a
// single constructor, holding the data members of that class. Classes with
// subclasses are also given an inductive definition enumerating all (converted)
// subclasses. Their data-member type definition will appear inline in the data
// member definitions for their leaf subclasses. Due to this representation,
// converted classes with subclasses must be abstract.
//
// Since the whole syntax tree is mutually recursive, all the types are dumped
// into one big "Inductive ... with ... with ... " definition.

library kernel.transformations.coq;

import 'dart:io';
import '../ast.dart';
import '../coq_annot.dart' as coq_annot;
import '../core_types.dart' show CoreTypes;

enum RefStyle { direct, identified }

enum FieldStyle { list, optional, normal }

class CoqFieldInfo {
  // Only one of these two may be non-null.
  final CoqClassInfo type;
  final String primitiveCoqType;

  final String dartName;
  final FieldStyle style;
  bool definitional = false;

  String get innerRefType => type == null ? primitiveCoqType : type.refType;

  String get refType {
    if (type != null) {
      var rt = definitional ? type.coqType : type.refType;
      if (style == FieldStyle.list) {
        return "list $rt";
      } else if (style == FieldStyle.optional) {
        return "option $rt";
      } else {
        return rt;
      }
    } else {
      if (style == FieldStyle.list) {
        return "list " + primitiveCoqType;
      } else if (style == FieldStyle.optional) {
        return "option " + primitiveCoqType;
      } else {
        return primitiveCoqType;
      }
    }
  }

  CoqFieldInfo(this.dartName, this.type, this.primitiveCoqType, this.style);
}

class CoqClassInfo {
  final Class cls;
  final RefStyle refStyle;

  List<CoqClassInfo> subs = <CoqClassInfo>[];
  List<CoqFieldInfo> fields = <CoqFieldInfo>[];

  bool needsOption = false;
  bool needsList = false;

  String get coqType => coqifyName(cls.name);
  String get coqTypeCaps => coqifyName(cls.name, capitalize: true);
  String get abbrevName => abbrev(cls.name);
  String get abbrevNameCaps => abbrev(cls.name, capitalize: true);
  String get refType => refStyle == RefStyle.direct ? coqType : "nat";

  CoqClassInfo(this.cls, this.refStyle);

  Iterable<CoqClassInfo> supersWithData(CoqLibInfo info) sync* {
    for (Supertype st = cls.supertype;
        st != null;
        st = st.classNode.supertype) {
      Class spr = st.classNode;
      var sprInfo = info.classes[spr];
      if (sprInfo == null) break;
      if (sprInfo.fields.length == 0) continue;
      yield sprInfo;
    }
  }
}

class CoqLibInfo {
  final Map<Class, CoqClassInfo> classes = <Class, CoqClassInfo>{};
  CoqLibInfo();
}

// Get the number associated with the annotation from `coq_annot.dart` on a
// [Field] or [Class] if one exists, and 0 otherwise. Throws an exception if
// invalid or multiple annotations are discoverted.
int getCoqAnnot(NamedNode N, List<Expression> annotations) {
  if (coq_annot.coqEnums.contains("$N")) {
    return coq_annot.coq;
  }

  int annot = 0;
  for (var A in annotations) {
    if (A is StaticGet) {
      var target = A.targetReference.node;
      var parent = target.parent;
      if (parent is Library) {
        if (parent is NamedNode && parent.name == "kernel.coq_annot") {
          if (target is Field) {
            if (annot != 0) {
              throw new Exception("ERROR: Multiple Coq annotations on ${N}!");
            }
            switch ("${target.name}") {
              case "coq":
                annot = coq_annot.coq;
                break;
              case "coqref":
                annot = coq_annot.coqref;
                break;
              case "nocoq":
                annot = coq_annot.nocoq;
                break;
              case "coqopt":
                annot = coq_annot.coqopt;
                break;
              case "coqsingle":
                annot = coq_annot.coqsingle;
                break;
              case "coqdef":
                annot = coq_annot.coqdef;
                break;
              case "coqsingledef":
                annot = coq_annot.coqsingledef;
                break;
              default:
                throw new Exception("ERROR: Invalid Coq annotation on ${N}!");
            }
          } else {
            throw new Exception("ERROR: Invalid Coq annotation on ${N}!");
          }
        }
      }
    }
  }
  return annot;
}

// Determine which classes we're going to convert.
class CoqPass1 extends RecursiveVisitor {
  CoqLibInfo info;
  CoqPass1(this.info);

  visitClass(Class C) {
    int annot = getCoqAnnot(C, C.annotations);
    if (annot == 0) return;

    if (annot != coq_annot.coq && annot != coq_annot.coqref) {
      throw new Exception("ERROR: Invalid Coq annotation on ${C.name}!");
    }

    info.classes[C] = new CoqClassInfo(
        C, annot == coq_annot.coq ? RefStyle.direct : RefStyle.identified);
  }
}

// Determine which fields we're going to convert and which classes have
// converted subclasses.
class CoqPass2 extends RecursiveVisitor {
  CoqLibInfo info;
  CoreTypes coreTypes;
  CoqPass2(this.info, this.coreTypes);

  CoqClassInfo currentClass = null;

  String getCoqPrimitiveType(Class cls) {
    if (cls == coreTypes.stringClass) {
      return "string";
    } else if (cls == coreTypes.boolClass) {
      return "bool";
    } else if (cls == coreTypes.intClass) {
      return "nat";
    } else {
      return null;
    }
  }

  visitClass(Class C) {
    var classInfo = info.classes[C];
    if (classInfo == null) return;

    if (C.supertype != null) {
      Class spr = C.supertype.classNode;
      var sprInfo = info.classes[spr];
      if (sprInfo != null) {
        sprInfo.subs.add(classInfo);
      }
    }

    currentClass = classInfo;
    C.visitChildren(this);
    currentClass = null;
  }

  visitField(Field F) {
    if (currentClass == null) return;

    int annot = getCoqAnnot(F, F.annotations);
    if (annot == coq_annot.nocoq) return;

    var type = F.type;
    if (type is! InterfaceType) return;

    var interfaceType = type as InterfaceType;

    var cls = null;
    bool isList = false;

    if (interfaceType.classNode == coreTypes.listClass) {
      isList = annot != coq_annot.coqsingle && annot != coq_annot.coqsingledef;
      if (interfaceType.typeArguments.length != 1) return;
      var elemType = interfaceType.typeArguments[0];
      if (elemType is InterfaceType) {
        cls = elemType.classNode;
      } else if (annot == coq_annot.coqopt) {
        throw new Exception("ERROR: Field $F may not be optional.");
      }
    } else {
      cls = interfaceType.classNode;
    }

    FieldStyle style = isList
        ? FieldStyle.list
        : (annot == coq_annot.coqopt ? FieldStyle.optional : FieldStyle.normal);

    CoqFieldInfo fieldInfo = null;
    var primitive = getCoqPrimitiveType(cls);
    var fieldName = F.name.name;

    if (primitive != null) {
      if (annot == 0) return;
      fieldInfo = new CoqFieldInfo(fieldName, null, primitive, style);
    } else {
      var fieldClassInfo = info.classes[cls];
      if (fieldClassInfo == null) {
        return;
      }
      fieldInfo = new CoqFieldInfo(fieldName, fieldClassInfo, null, style);

      if (style == FieldStyle.optional) {
        fieldClassInfo.needsOption = true;
      } else if (style == FieldStyle.list) {
        fieldClassInfo.needsList = true;
      }

      if (annot == coq_annot.coqdef || annot == coq_annot.coqsingledef) {
        fieldInfo.definitional = true;
      }
    }

    currentClass.fields.add(fieldInfo);
  }
}

// Conventional Coq code uses underscores instead of camelCase as Dart code
// does. This function converts the Dart convention to the Coq convention.
final coqReserved = <String>["let"];
String coqifyName(String S, {bool capitalize: false}) {
  List<int> codes = <int>[];
  bool skipUnderscore = false;
  for (int i = 0; i < S.length; ++i) {
    var c = S.codeUnitAt(i);
    if (c >= "A".codeUnits[0] && c <= "Z".codeUnits[0]) {
      if (i > 0 && !skipUnderscore) {
        codes.add("_".codeUnitAt(0));
      }
      if (!capitalize) c += ("a".codeUnits[0] - "A".codeUnits[0]);
      codes.add(c);
    } else {
      codes.add(c);
    }
    skipUnderscore = c == "_".codeUnits[0];
  }
  var name = new String.fromCharCodes(codes);
  if (coqReserved.contains(name)) {
    name = "dart_" + name;
  }
  return name;
}

// Give an abbreviation of a identifying by combining the capital letters of the
// identifier. For example, "ProcedureKind" becomes "pk" or "PK".
String abbrev(String S, {bool capitalize: false}) {
  List<int> codes = <int>[];
  for (var c in S.codeUnits)
    if (c >= 65 && c <= 90) codes.add(capitalize ? c : c + 32);
  return new String.fromCharCodes(codes);
}

void outputCoqImports() {
  print("""
Require Import Common.
""");
}

void outputCoqSyntax(CoqLibInfo info) {
  int defN = 0;
  defkw() => defN++ > 0 ? "with" : "Inductive";
  for (var classInfo in info.classes.values) {
    bool isAbstract = classInfo.subs.length > 0;

    Class cls = classInfo.cls;
    var coqName = classInfo.coqType;

    if (classInfo.cls.isEnum) {
      var enums = [];
      for (var fld in classInfo.fields) {
        if (fld.dartName == "values") continue;
        enums.add(coqifyName(fld.dartName, capitalize: true));
      }
      print("${defkw()} $coqName : Set := ${enums.join(" | ")}\n");
      continue;
    }

    if (!isAbstract || classInfo.fields.length > 0) {
      var suffix = isAbstract ? "_data" : "";
      var dataTypeName = coqName + suffix;
      var dataCtorName = coqifyName(cls.name, capitalize: true);

      print("${defkw()} ${dataTypeName} : Set :=");
      print("  | ${dataCtorName} : ");

      // Insert fields for superclasses.
      int arw = 0;
      arrow() => arw++ == 0 ? "" : "-> ";

      if (classInfo.refStyle == RefStyle.identified) {
        print("      ${arrow()}nat");
      }

      for (var sprInfo in classInfo.supersWithData(info)) {
        print("      ${arrow()}${sprInfo.coqType}_data");
      }

      for (CoqFieldInfo fld in classInfo.fields) {
        print("      ${arrow()}${fld.refType} (* ${fld.dartName} *)");
      }

      print("      ${arrow()}$dataTypeName\n");
    }

    if (classInfo.subs.length > 0) {
      print("${defkw()} $coqName : Set :=");

      var abbrevName = abbrev(cls.name, capitalize: true);
      for (var sub in classInfo.subs) {
        var subTypeName = coqifyName(sub.cls.name);
        var ctorName = coqifyName(sub.cls.name, capitalize: true);
        print("  | ${abbrevName}_${ctorName} : ${subTypeName} -> $coqName");
      }

      print("\n");
    }
  }
  print(".\n");
}

void outputCoqStore(info) {
  print("Record ast_store : Type := Ast_Store {");
  for (var classInfo in info.classes.values) {
    if (classInfo.refStyle != RefStyle.identified) continue;
    print("  ${classInfo.abbrevName}_refs : NatMap.t ${classInfo.coqType};");
  }
  print("}.\n");
}

void outputCoqSyntaxValidity(CoqLibInfo info) {
  int defN = 0;
  defkw() => defN++ > 0 ? "with" : "Fixpoint";

  validityPredicate(CoqClassInfo CI) {
    if (CI.refStyle == RefStyle.identified) {
      var mapName = "${CI.abbrevName}_refs";
      return (X) => "NatMap.In $X ($mapName ast)";
    } else {
      return (X) => "${CI.coqType}_validity ast $X";
    }
  }

  for (var CI in info.classes.values) {
    stdout.write(
        "${defkw()} ${CI.coqType}_validity (ast : ast_store) (T : ${CI.coqType}) {struct T} : Prop :=");
    if (CI.cls.isEnum) {
      stdout.write(" True\n");
      continue;
    } else {
      stdout.write("\n");
    }

    print("  match T with");
    for (var sub in CI.subs) {
      print(
          "    | ${CI.abbrevNameCaps}_${sub.coqTypeCaps} ST => ${sub.coqType}_validity ast ST");
    }

    if (CI.subs.length > 0) {
      print("end");
      if (CI.fields.length == 0) continue;
      print(
          "${defkw()} ${CI.coqType}_data_validity (ast : ast_store) (T : ${CI.coqType}_data) {struct T}: Prop :=");
      print("  match T with");
    }

    int i = 0;
    var fieldNames = [];
    var validityClauses = [];

    for (var SI in CI.supersWithData(info)) {
      var f = "f${i++}";
      fieldNames.add(f);
      validityClauses.add("${SI.coqType}_data_validity ast $f");
    }

    for (var fld in CI.fields) {
      if (fld.type == null) {
        fieldNames.add("_");
        continue;
      }

      var f = "f${i++}";
      fieldNames.add(f);

      var pred;
      if (fld.style == FieldStyle.normal) {
        pred = validityPredicate(fld.type)(f);
      } else if (fld.style == FieldStyle.list) {
        pred = "${fld.type.coqType}_list_validity ast $f";
      } else if (fld.style == FieldStyle.optional) {
        pred = "${fld.type.coqType}_option_validity ast $f";
      }

      validityClauses.add(pred);
    }

    var clause = "True";
    if (validityClauses.length > 0) {
      clause = validityClauses.join(" /\\\n        ");
    }

    print(
        "    | ${CI.coqTypeCaps} ${fieldNames.join(" ")} =>\n        $clause");
    print("  end");
  }

  for (var CI in info.classes.values) {
    var pred = validityPredicate(CI)("X");
    if (CI.needsList) {
      var def = """
with ${CI.coqType}_list_validity (ast : ast_store) (L : ${CI.coqType}_list) {struct L} : Prop :=
  match L with
    | ${CI.coqType}_nil => True
    | ${CI.coqType}_cons X XS => $pred /\\ ${CI.coqType}_list_validity ast XS
  end""";
      print(def);
    }
    if (CI.needsOption) {
      var def = """
with ${CI.coqType}_option_validity (ast : ast_store) (O : ${CI.coqType}_option) {struct O} : Prop :=
  match O with
    | ${CI.coqType}_none => True
    | ${CI.coqType}_some X => $pred
  end""";
      print(def);
    }
  }

  print(".\n");
}

void outputCoqStoreValidity(CoqLibInfo info) {
  var clauses = [];
  for (var CI in info.classes.values) {
    if (CI.refStyle != RefStyle.identified) continue;
    var mapName = "${CI.abbrevName}_refs";
    clauses.add(
        "  forall (n : nat), forall (X : ${CI.coqType}), NatMap.MapsTo n X ($mapName ast) -> ${CI.coqType}_validity ast X");
  }
  var clause = clauses.join(" /\\\n");
  print(
      "Definition ast_store_validity (ast : ast_store) : Prop := \n$clause\n.");
}

Component transformComponent(CoreTypes coreTypes, Component component) {
  for (Library lib in component.libraries) {
    // TODO(30610): Ideally we'd output to the file in the coq annotation on the
    // library name, but currently fasta throws away annotations on libraries.
    // Instead, we just special case "kernel.ast" and output to stdout.
    if ("$lib" != "kernel.ast") continue;
    var info = new CoqLibInfo();
    (new CoqPass1(info)).visitLibrary(lib);
    (new CoqPass2(info, coreTypes)).visitLibrary(lib);
    outputCoqImports();
    outputCoqSyntax(info);
  }
  return component;
}
