// 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.

import 'annotated_code_helper.dart';

enum IdKind {
  /// Id used for top level or class members. This is used in [MemberId].
  member,

  /// Id used for classes. This is used in [ClassId].
  cls,

  /// Id used for libraries. This is used in [LibraryId].
  library,

  /// Id used for a code point at certain offset. The id represents the default
  /// use of the code point, often a read access. This is used in [NodeId].
  node,

  /// Id used for an invocation at certain offset. This is used in [NodeId].
  invoke,

  /// Id used for an assignment at certain offset. This is used in [NodeId].
  update,

  /// Id used for the iterator expression of a for-in at certain offset. This is
  /// used in [NodeId].
  iterator,

  /// Id used for the implicit call to `Iterator.current` in a for-in at certain
  /// offset. This is used in [NodeId].
  current,

  /// Id used for the implicit call to `Iterator.moveNext` in a for-in at
  /// certain offset. This is used in [NodeId].
  moveNext,

  /// Id used for the implicit as expression inserted by the compiler.
  implicitAs,

  /// Id used for the statement at certain offset. This is used in [NodeId].
  stmt,

  /// Id used for the error reported at certain offset. This is used in
  /// [NodeId].
  error,
}

/// Id for a code point or element.
abstract class Id {
  IdKind get kind;

  /// Indicates whether the id refers to an element defined outside of the test
  /// case itself (e.g. some tests may need to refer to properties of elements
  /// in `dart:core`).
  bool get isGlobal;

  /// Display name for this id.
  String get descriptor;
}

class IdValue {
  final Id id;
  final Annotation annotation;
  final String value;

  const IdValue(this.id, this.annotation, this.value);

  @override
  int get hashCode => id.hashCode * 13 + value.hashCode * 17;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! IdValue) return false;
    return id == other.id && value == other.value;
  }

  @override
  String toString() => idToString(id, value);

  static String idToString(Id id, String value) {
    switch (id.kind) {
      case IdKind.member:
        MemberId elementId = id as MemberId;
        return '$memberPrefix${elementId.name}:$value';
      case IdKind.cls:
        ClassId classId = id as ClassId;
        return '$classPrefix${classId.name}:$value';
      case IdKind.library:
        return '$libraryPrefix$value';
      case IdKind.node:
        return value;
      case IdKind.invoke:
        return '$invokePrefix$value';
      case IdKind.update:
        return '$updatePrefix$value';
      case IdKind.iterator:
        return '$iteratorPrefix$value';
      case IdKind.current:
        return '$currentPrefix$value';
      case IdKind.moveNext:
        return '$moveNextPrefix$value';
      case IdKind.implicitAs:
        return '$implicitAsPrefix$value';
      case IdKind.stmt:
        return '$stmtPrefix$value';
      case IdKind.error:
        return '$errorPrefix$value';
    }
  }

  static const String globalPrefix = "global#";
  static const String memberPrefix = "member: ";
  static const String classPrefix = "class: ";
  static const String libraryPrefix = "library: ";
  static const String invokePrefix = "invoke: ";
  static const String updatePrefix = "update: ";
  static const String iteratorPrefix = "iterator: ";
  static const String currentPrefix = "current: ";
  static const String moveNextPrefix = "moveNext: ";
  static const String implicitAsPrefix = "as: ";
  static const String stmtPrefix = "stmt: ";
  static const String errorPrefix = "error: ";

  static IdValue decode(
    Uri sourceUri,
    Annotation annotation,
    String text, {
    bool preserveWhitespaceInAnnotations = false,
    bool preserveInfixWhitespace = false,
  }) {
    int offset = annotation.offset;
    Id id;
    String expected;
    if (text.startsWith(memberPrefix)) {
      text = text.substring(memberPrefix.length);
      int colonPos = text.indexOf(':');
      if (colonPos == -1) throw "Invalid member id: '$text'";
      String name = text.substring(0, colonPos);
      bool isGlobal = name.startsWith(globalPrefix);
      if (isGlobal) {
        name = name.substring(globalPrefix.length);
      }
      id = new MemberId(name, isGlobal: isGlobal);
      expected = text.substring(colonPos + 1);
    } else if (text.startsWith(classPrefix)) {
      text = text.substring(classPrefix.length);
      int colonPos = text.indexOf(':');
      if (colonPos == -1) throw "Invalid class id: '$text'";
      String name = text.substring(0, colonPos);
      bool isGlobal = name.startsWith(globalPrefix);
      if (isGlobal) {
        name = name.substring(globalPrefix.length);
      }
      id = new ClassId(name, isGlobal: isGlobal);
      expected = text.substring(colonPos + 1);
    } else if (text.startsWith(libraryPrefix)) {
      id = new LibraryId(sourceUri);
      expected = text.substring(libraryPrefix.length);
    } else if (text.startsWith(invokePrefix)) {
      id = new NodeId(offset, IdKind.invoke);
      expected = text.substring(invokePrefix.length);
    } else if (text.startsWith(updatePrefix)) {
      id = new NodeId(offset, IdKind.update);
      expected = text.substring(updatePrefix.length);
    } else if (text.startsWith(iteratorPrefix)) {
      id = new NodeId(offset, IdKind.iterator);
      expected = text.substring(iteratorPrefix.length);
    } else if (text.startsWith(currentPrefix)) {
      id = new NodeId(offset, IdKind.current);
      expected = text.substring(currentPrefix.length);
    } else if (text.startsWith(moveNextPrefix)) {
      id = new NodeId(offset, IdKind.moveNext);
      expected = text.substring(moveNextPrefix.length);
    } else if (text.startsWith(implicitAsPrefix)) {
      id = new NodeId(offset, IdKind.implicitAs);
      expected = text.substring(implicitAsPrefix.length);
    } else if (text.startsWith(stmtPrefix)) {
      id = new NodeId(offset, IdKind.stmt);
      expected = text.substring(stmtPrefix.length);
    } else if (text.startsWith(errorPrefix)) {
      id = new NodeId(offset, IdKind.error);
      expected = text.substring(errorPrefix.length);
    } else {
      id = new NodeId(offset, IdKind.node);
      expected = text;
    }
    if (preserveWhitespaceInAnnotations) {
      // Keep all whitespace.
    } else if (preserveInfixWhitespace) {
      // Remove heading and trailing whitespace.
      expected = expected.trim();
    } else {
      // Remove unneeded whitespace.
      expected = expected.replaceAll(new RegExp(r'\s*(\n\s*)+\s*'), '');
    }
    return new IdValue(id, annotation, expected);
  }
}

/// Id for an member element.
class MemberId implements Id {
  final String? className;
  final String memberName;
  @override
  final bool isGlobal;

  factory MemberId(String text, {bool isGlobal = false}) {
    int dotPos = text.indexOf('.');
    if (dotPos != -1) {
      return new MemberId.internal(
        text.substring(dotPos + 1),
        className: text.substring(0, dotPos),
        isGlobal: isGlobal,
      );
    } else {
      return new MemberId.internal(text, isGlobal: isGlobal);
    }
  }

  MemberId.internal(this.memberName, {this.className, this.isGlobal = false});

  @override
  int get hashCode => className.hashCode * 13 + memberName.hashCode * 17;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! MemberId) return false;
    return className == other.className && memberName == other.memberName;
  }

  @override
  IdKind get kind => IdKind.member;

  String get name => className != null ? '$className.$memberName' : memberName;

  @override
  String get descriptor => 'member $name';

  @override
  String toString() => 'member:$name';
}

/// Id for a class.
class ClassId implements Id {
  final String className;
  @override
  final bool isGlobal;

  ClassId(this.className, {this.isGlobal = false});

  @override
  int get hashCode => className.hashCode * 13;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! ClassId) return false;
    return className == other.className;
  }

  @override
  IdKind get kind => IdKind.cls;

  String get name => className;

  @override
  String get descriptor => 'class $name';

  @override
  String toString() => 'class:$name';
}

/// Id for a library.
class LibraryId implements Id {
  final Uri uri;

  LibraryId(this.uri);

  // TODO(johnniwinther): Support global library annotations.
  @override
  bool get isGlobal => false;

  @override
  int get hashCode => uri.hashCode * 13;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! LibraryId) return false;
    return uri == other.uri;
  }

  @override
  IdKind get kind => IdKind.library;

  String get name => uri.toString();

  @override
  String get descriptor => 'library $name';

  @override
  String toString() => 'library:$name';
}

/// Id for a code point defined by a kind and a code offset.
class NodeId implements Id {
  final int value;
  @override
  final IdKind kind;

  const NodeId(this.value, this.kind) : assert(value >= 0);

  @override
  bool get isGlobal => false;

  @override
  int get hashCode => value.hashCode * 13 + kind.hashCode * 17;

  @override
  bool operator ==(other) {
    if (identical(this, other)) return true;
    if (other is! NodeId) return false;
    return value == other.value && kind == other.kind;
  }

  @override
  String get descriptor => 'offset $value ($kind)';

  @override
  String toString() => '$kind:$value';
}

class ActualData<T> {
  final Id id;
  final T value;
  final Uri uri;
  final int _offset;
  final Object object;

  ActualData(this.id, this.value, this.uri, this._offset, this.object);

  int get offset {
    if (id is NodeId) {
      NodeId nodeId = id as NodeId;
      return nodeId.value;
    } else {
      return _offset;
    }
  }

  String get objectText {
    return 'object `${'$object'.replaceAll('\n', '')}` (${object.runtimeType})';
  }

  @override
  String toString() =>
      'ActualData(id=$id,value=$value,uri=$uri,'
      'offset=$offset,object=$objectText)';
}

mixin DataRegistry<T> {
  Map<Id, ActualData<T>> get actualMap;

  /// Registers [value] with [id] in [actualMap].
  ///
  /// Checks for duplicate data for [id].
  void registerValue(Uri uri, int offset, Id id, T? value, Object object) {
    if (value != null) {
      ActualData<T> newData = new ActualData<T>(id, value, uri, offset, object);
      if (actualMap.containsKey(id)) {
        ActualData<T> existingData = actualMap[id]!;
        ActualData<T>? mergedData = mergeData(existingData, newData);
        if (mergedData != null) {
          actualMap[id] = mergedData;
        } else {
          report(
            uri,
            offset,
            "Duplicate id ${id}, value=$value, object=$object "
            "(${object.runtimeType})",
          );
          report(
            uri,
            offset,
            "Duplicate id ${id}, value=${existingData.value}, "
            "object=${existingData.object} "
            "(${existingData.object.runtimeType})",
          );
          fail("Duplicate id $id.");
        }
      } else {
        actualMap[id] = newData;
      }
    }
  }

  ActualData<T>? mergeData(ActualData<T> value1, ActualData<T> value2) => null;

  /// Called to report duplicate errors.
  void report(Uri uri, int offset, String message);

  /// Called to raise an exception on duplicate errors.
  void fail(String message);
}
