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

const int _LF = 0x0A;
const int _CR = 0x0D;

const Pattern atBraceStart = '@{';
const Pattern braceEnd = '}';

final Pattern commentStart = new RegExp(r'/\*');
final Pattern commentEnd = new RegExp(r'\*/\s*');

class Annotation {
  /// The index of the (corresponding) annotation in the annotated code test, or
  /// `null` if the annotation doesn't correspond to an annotation in the
  /// annotated code.
  final int? index;

  /// 1-based line number of the annotation.
  final int lineNo;

  /// 1-based column number of the annotation.
  final int columnNo;

  /// 0-based character offset  of the annotation within the source text.
  final int offset;

  /// The annotation start text.
  final String prefix;

  /// The text in the annotation.
  final String text;

  /// The annotation end text.
  final String suffix;

  Annotation(this.index, this.lineNo, this.columnNo, this.offset, this.prefix,
      this.text, this.suffix)
      : assert(offset >= 0);

  @override
  String toString() =>
      'Annotation(index=$index,lineNo=$lineNo,columnNo=$columnNo,'
      'offset=$offset,prefix=$prefix,text=$text,suffix=$suffix)';
}

/// A source code text with annotated positions.
///
/// An [AnnotatedCode] can be created from a [String] of source code where
/// annotated positions are embedded, by default using the syntax `@{text}`.
/// For instance
///
///     main() {
///       @{foo-call}foo();
///       bar@{bar-args}();
///     }
///
///  the position of `foo` call will hold an annotation with text 'foo-call' and
///  the position of `bar` arguments will hold an annotation with text
///  'bar-args'.
///
///  Annotation text cannot span multiple lines and cannot contain '}'.
class AnnotatedCode {
  /// The original code with annotations.
  final String annotatedCode;

  /// The source code without annotations.
  final String sourceCode;

  /// The annotations for the source code.
  final List<Annotation> annotations;

  List<int>? _lineStarts;

  AnnotatedCode(this.annotatedCode, this.sourceCode, this.annotations);

  AnnotatedCode.internal(
      this.annotatedCode, this.sourceCode, this.annotations, this._lineStarts);

  /// Creates an [AnnotatedCode] by processing [annotatedCode]. Annotation
  /// delimited by [start] and [end] are converted into [Annotation]s and
  /// removed from the [annotatedCode] to produce the source code.
  factory AnnotatedCode.fromText(String annotatedCode,
      [Pattern start = atBraceStart, Pattern end = braceEnd]) {
    StringBuffer codeBuffer = new StringBuffer();
    List<Annotation> annotations = <Annotation>[];
    int index = 0;
    int offset = 0;
    int lineNo = 1;
    int columnNo = 1;
    List<int> lineStarts = <int>[];
    lineStarts.add(offset);
    while (index < annotatedCode.length) {
      Match? startMatch = start.matchAsPrefix(annotatedCode, index);
      if (startMatch != null) {
        int startIndex = startMatch.end;
        Iterable<Match> endMatches =
            end.allMatches(annotatedCode, startMatch.end);
        if (!endMatches.isEmpty) {
          Match endMatch = endMatches.first;
          annotatedCode.indexOf(end, startIndex);
          String prefix =
              annotatedCode.substring(startMatch.start, startMatch.end);
          String text = annotatedCode.substring(startMatch.end, endMatch.start);
          String suffix = annotatedCode.substring(endMatch.start, endMatch.end);
          annotations.add(new Annotation(annotations.length, lineNo, columnNo,
              offset, prefix, text, suffix));
          index = endMatch.end;
          continue;
        }
      }

      int charCode = annotatedCode.codeUnitAt(index);
      switch (charCode) {
        case _LF:
          codeBuffer.write('\n');
          offset++;
          lineStarts.add(offset);
          lineNo++;
          columnNo = 1;
          break;
        case _CR:
          if (index + 1 < annotatedCode.length &&
              annotatedCode.codeUnitAt(index + 1) == _LF) {
            index++;
          }
          codeBuffer.write('\n');
          offset++;
          lineStarts.add(offset);
          lineNo++;
          columnNo = 1;
          break;
        default:
          codeBuffer.writeCharCode(charCode);
          offset++;
          columnNo++;
      }
      index++;
    }
    lineStarts.add(offset);
    return new AnnotatedCode.internal(
        annotatedCode, codeBuffer.toString(), annotations, lineStarts);
  }

  void _ensureLineStarts() {
    if (_lineStarts == null) {
      List<int> lineStarts = <int>[];
      _lineStarts = lineStarts;
      int index = 0;
      int offset = 0;
      lineStarts.add(offset);
      while (index < sourceCode.length) {
        int charCode = sourceCode.codeUnitAt(index);
        switch (charCode) {
          case _LF:
            offset++;
            lineStarts.add(offset);
            break;
          case _CR:
            if (index + 1 < sourceCode.length &&
                sourceCode.codeUnitAt(index + 1) == _LF) {
              index++;
            }
            offset++;
            lineStarts.add(offset);
            break;
          default:
            offset++;
        }
        index++;
      }
      lineStarts.add(offset);
    }
  }

  void addAnnotation(
      int lineNo, int columnNo, String prefix, String text, String suffix) {
    _ensureLineStarts();
    int offset = _lineStarts![lineNo - 1] + (columnNo - 1);
    annotations.add(new Annotation(
        annotations.length, lineNo, columnNo, offset, prefix, text, suffix));
  }

  int get lineCount {
    _ensureLineStarts();
    return _lineStarts!.length;
  }

  int getLineIndex(int offset) {
    _ensureLineStarts();
    int index = 0;
    while (index + 1 < _lineStarts!.length) {
      if (_lineStarts![index + 1] <= offset) {
        index++;
      } else {
        break;
      }
    }
    return index;
  }

  int getLineStart(int lineIndex) {
    _ensureLineStarts();
    if (lineIndex < 0) {
      return 0;
    } else if (lineIndex < _lineStarts!.length) {
      return _lineStarts![lineIndex];
    } else {
      return sourceCode.length;
    }
  }

  String getLine(int lineIndex) {
    int startIndex = getLineStart(lineIndex);
    int endIndex = getLineStart(lineIndex + 1);
    return sourceCode.substring(startIndex, endIndex);
  }

  String toText() {
    StringBuffer sb = new StringBuffer();
    List<Annotation> list = annotations.toList()
      ..sort((a, b) {
        int result = a.offset.compareTo(b.offset);
        if (result == 0) {
          if (a.index != null && b.index != null) {
            result = a.index!.compareTo(b.index!);
          } else if (a.index != null) {
            result = -1;
          } else if (b.index != null) {
            result = 1;
          }
        }
        if (result == 0) {
          result = annotations.indexOf(a).compareTo(annotations.indexOf(b));
        }
        return result;
      });
    int offset = 0;
    for (Annotation annotation in list) {
      sb.write(sourceCode.substring(offset, annotation.offset));
      sb.write(annotation.prefix);
      sb.write(annotation.text);
      sb.write(annotation.suffix);
      offset = annotation.offset;
    }
    sb.write(sourceCode.substring(offset));
    return sb.toString();
  }

  @override
  String toString() {
    return 'AnnotatedCode(sourceCode=$sourceCode,annotations=$annotations)';
  }
}

/// Split the annotations in [annotatedCode] by [prefixes].
///
/// Returns a map containing an [AnnotatedCode] object for each prefix,
/// containing only the annotations whose text started with the given prefix.
/// If no prefix match the annotation text, the annotation is added to all
/// [AnnotatedCode] objects.
///
/// The prefixes are removed from the annotation texts in the returned
/// [AnnotatedCode] objects.
Map<String, AnnotatedCode> splitByPrefixes(
    AnnotatedCode annotatedCode, Iterable<String> prefixes) {
  Set<String> prefixSet = prefixes.toSet();
  Map<String, List<Annotation>> map = <String, List<Annotation>>{};
  for (String prefix in prefixSet) {
    map[prefix] = <Annotation>[];
  }
  outer:
  for (Annotation annotation in annotatedCode.annotations) {
    int dotPos = annotation.text.indexOf('.');
    if (dotPos != -1) {
      String annotationPrefix = annotation.text.substring(0, dotPos);
      String annotationText = annotation.text.substring(dotPos + 1);
      List<String> markers = annotationPrefix.split('|').toList();
      if (prefixSet.containsAll(markers)) {
        for (String part in markers) {
          Annotation subAnnotation = new Annotation(
              annotation.index,
              annotation.lineNo,
              annotation.columnNo,
              annotation.offset,
              annotation.prefix,
              annotationText,
              annotation.suffix);
          map[part]!.add(subAnnotation);
        }
        continue outer;
      }
    }
    for (String prefix in prefixSet) {
      map[prefix]!.add(annotation);
    }
  }
  Map<String, AnnotatedCode> split = <String, AnnotatedCode>{};
  map.forEach((String prefix, List<Annotation> annotations) {
    split[prefix] = new AnnotatedCode(
        annotatedCode.annotatedCode, annotatedCode.sourceCode, annotations);
  });
  return split;
}
