// Copyright (c) 2019, 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 'dart:convert';

import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:meta/meta.dart';

/// Stripped down information derived from [AstNode] containing only information
/// needed to resurrect the source code of [_element].
class ModelNode {
  final Element _element;
  final AnalysisContext _analysisContext;
  final int _sourceEnd;
  final int _sourceOffset;

  /// Data about the doc comment of this node.
  final CommentData? commentData;

  factory ModelNode(
    AstNode? sourceNode,
    Element element,
    AnalysisContext analysisContext, {
    CommentData? commentData,
  }) {
    if (sourceNode == null) {
      return ModelNode._(element, analysisContext,
          sourceEnd: -1, sourceOffset: -1);
    } else {
      // Get a node higher up the syntax tree that includes the semicolon.
      // In this case, it is either a [FieldDeclaration] or
      // [TopLevelVariableDeclaration]. (#2401)
      if (sourceNode is VariableDeclaration) {
        sourceNode = sourceNode.parent!.parent!;
        assert(sourceNode is FieldDeclaration ||
            sourceNode is TopLevelVariableDeclaration);
      }
      return ModelNode._(
        element,
        analysisContext,
        sourceEnd: sourceNode.end,
        sourceOffset: sourceNode.offset,
        commentData: commentData,
      );
    }
  }

  ModelNode._(
    this._element,
    this._analysisContext, {
    required int sourceEnd,
    required int sourceOffset,
    this.commentData,
  })  : _sourceEnd = sourceEnd,
        _sourceOffset = sourceOffset;

  bool get _isSynthetic => _sourceEnd < 0 || _sourceOffset < 0;

  /// The text of the source code of this node, stripped of the leading
  /// indentation, and stripped of the doc comments.
  late final String sourceCode = () {
    if (_isSynthetic) return '';

    var path = _element.firstFragment.libraryFragment?.source.fullName;
    if (path == null) return '';

    var fileResult = _analysisContext.currentSession.getFile(path);
    if (fileResult is! FileResult) return '';

    return fileResult.content
        .substringFromLineStart(_sourceOffset, _sourceEnd)
        .stripIndent
        .stripDocComments
        .trim();
  }();
}

/// Comment data from the syntax tree.
///
/// Various comment data is not available on the analyzer's Element model, so we
/// store it in instances of this class after resolving libraries.
class CommentData {
  /// The offset of this comment in the source text.
  final int offset;
  final List<CommentDocImportData> docImports;
  final Map<String, CommentReferenceData> references;

  CommentData({
    required this.offset,
    required this.docImports,
    required this.references,
  });
}

/// doc-import data from the syntax tree.
///
/// Comment doc-import data is not available on the analyzer's Element model, so
/// we store it in instances of this class after resolving libraries.
class CommentDocImportData {
  /// The offset of the doc import in the source text.
  final int offset;

  /// The offset of the end of the doc import in the source text.
  final int end;

  CommentDocImportData({required this.offset, required this.end});
}

/// Comment reference data from the syntax tree.
///
/// Comment reference data is not available on the analyzer's Element model, so
/// we store it in instances of this class after resolving libraries.
class CommentReferenceData {
  final Element element;
  final String name;
  final int offset;
  final int length;

  CommentReferenceData(this.element, String? name, this.offset, this.length)
      : name = name ?? '';
}

@visibleForTesting
extension SourceStringExtensions on String {
  String substringFromLineStart(int offset, int endOffset) {
    var lineStartOffset = startOfLineWithOffset(offset);
    return substring(lineStartOffset, endOffset);
  }

  // Finds the start of the line which contains the character at [offset].
  int startOfLineWithOffset(int offset) {
    var i = offset;
    // Walk backwards until we find the previous line's EOL character.
    while (i > 0) {
      i -= 1;
      if (this[i] == '\n' || this[i] == '\r') {
        i += 1;
        break;
      }
    }
    return i;
  }

  /// Strips leading doc comments from the given source code.
  String get stripDocComments {
    var remainder = trimLeft();
    var lineComments = remainder.startsWith(_tripleSlash) ||
        remainder.startsWith(_escapedTripleSlash);
    var blockComments = remainder.startsWith(_slashStarStar) ||
        remainder.startsWith(_escapedSlashStarStar);

    return split('\n').where((String line) {
      if (lineComments) {
        if (line.startsWith(_tripleSlash) ||
            line.startsWith(_escapedTripleSlash)) {
          return false;
        }
        lineComments = false;
        return true;
      } else if (blockComments) {
        if (line.contains(_starSlash) || line.contains(_escapedStarSlash)) {
          blockComments = false;
          return false;
        }
        if (line.startsWith(_slashStarStar) ||
            line.startsWith(_escapedSlashStarStar)) {
          return false;
        }
        return false;
      }

      return true;
    }).join('\n');
  }

  /// Strips the common indent from the given source fragment.
  String get stripIndent {
    var remainder = trimLeft();
    var indent = substring(0, length - remainder.length);
    return split('\n').map((line) {
      line = line.trimRight();
      return line.startsWith(indent) ? line.substring(indent.length) : line;
    }).join('\n');
  }
}

const HtmlEscape _escape = HtmlEscape();

const String _tripleSlash = '///';

final String _escapedTripleSlash = _escape.convert(_tripleSlash);

const String _slashStarStar = '/**';

final String _escapedSlashStarStar = _escape.convert(_slashStarStar);

const String _starSlash = '*/';

final String _escapedStarSlash = _escape.convert(_starSlash);
