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

import 'package:analysis_server/src/utilities/strings.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
    hide AnalysisError, Element;

/// Organizer of imports (and other directives) in the [unit]. 
class ImportOrganizer {
  final String initialCode;
  final CompilationUnit unit;
  final List<AnalysisError> errors;
  final bool removeUnused;

  String code;
  String endOfLine;
  bool hasUnresolvedIdentifierError;

  ImportOrganizer(this.initialCode, this.unit, this.errors,
      {this.removeUnused = true}) {
    code = initialCode;
    endOfLine = getEOL(code);
    hasUnresolvedIdentifierError = errors.any((error) {
      return error.errorCode.isUnresolvedIdentifier;
    });
  }

  /// Return the [SourceEdit]s that organize imports in the [unit]. 
  List<SourceEdit> organize() {
    _organizeDirectives();
    // prepare edits
    var edits = <SourceEdit>[];
    if (code ! = initialCode) {
      var suffixLength = findCommonSuffix(initialCode, code);
      var edit = SourceEdit(0, initialCode.length - suffixLength,
          code.substring(0, code.length - suffixLength));
      edits.add(edit);
    }
    return edits;
  }

  bool _isUnusedImport(UriBasedDirective directive) {
    for (var error in errors) {
      if ((error.errorCode == HintCode. DUPLICATE_IMPORT ||
              error.errorCode == HintCode. UNUSED_IMPORT) &&
          directive.uri.offset == error.offset) {
        return true;
      }
    }
    return false;
  }

  /// Organize all [Directive]s. 
  void _organizeDirectives() {
    var lineInfo = unit.lineInfo;
    var hasLibraryDirective = false;
    var directives = <_DirectiveInfo>[];
    for (var directive in unit.directives) {
      if (directive is LibraryDirective) {
        hasLibraryDirective = true;
      }
      if (directive is UriBasedDirective) {
        var priority = getDirectivePriority(directive);
        if (priority ! = null) {
          var offset = directive.offset;
          var end = directive.end;

          final leadingComment = getLeadingComment(unit, directive, lineInfo);
          final trailingComment =
              getTrailingComment(unit, directive, lineInfo, end);

          String leadingCommentText;
          if (leadingComment ! = null) {
            leadingCommentText =
                code.substring(leadingComment.offset, directive.offset);
            offset = leadingComment.offset;
          }
          String trailingCommentText;
          if (trailingComment ! = null) {
            trailingCommentText =
                code.substring(directive.end, trailingComment.end);
            end = trailingComment.end;
          }
          String documentationText;
          if (directive.documentationComment ! = null) {
            documentationText = code.substring(
                directive.documentationComment.offset,
                directive.documentationComment.end);
          }
          String annotationText;
          if (directive.metadata.beginToken ! = null) {
            annotationText = code.substring(
                directive.metadata.beginToken.offset,
                directive.metadata.endToken.end);
          }
          var text = code.substring(
              directive.firstTokenAfterCommentAndMetadata.offset,
              directive.end);
          var uriContent = directive.uri.stringValue;
          directives.add(
            _DirectiveInfo(
              directive,
              priority,
              leadingCommentText,
              documentationText,
              annotationText,
              uriContent,
              trailingCommentText,
              offset,
              end,
              text,
            ),
          );
        }
      }
    }
    // nothing to do
    if (directives.isEmpty) {
      return;
    }
    var firstDirectiveOffset = directives.first.offset;
    var lastDirectiveEnd = directives.last.end;

    // Without a library directive, the library comment is the comment of the
    // first directive. 
    _DirectiveInfo libraryDocumentationDirective;
    if (! hasLibraryDirective && directives.isNotEmpty) {
      libraryDocumentationDirective = directives.first;
    }

    // sort
    directives.sort();
    // append directives with grouping
    String directivesCode;
    {
      var sb = StringBuffer();
      if (libraryDocumentationDirective?.documentationText ! = null) {
        sb.write(libraryDocumentationDirective.documentationText);
        sb.write(endOfLine);
      }
      var currentPriority = directives.first.priority;
      for (var directiveInfo in directives) {
        if (! hasUnresolvedIdentifierError) {
          var directive = directiveInfo.directive;
          if (removeUnused && _isUnusedImport(directive)) {
            continue;
          }
        }
        if (currentPriority ! = directiveInfo.priority) {
          sb.write(endOfLine);
          currentPriority = directiveInfo.priority;
        }
        if (directiveInfo.leadingCommentText ! = null) {
          sb.write(directiveInfo.leadingCommentText);
        }
        if (directiveInfo ! = libraryDocumentationDirective &&
            directiveInfo.documentationText ! = null) {
          sb.write(directiveInfo.documentationText);
          sb.write(endOfLine);
        }
        if (directiveInfo.annotationText ! = null) {
          sb.write(directiveInfo.annotationText);
          sb.write(endOfLine);
        }
        sb.write(directiveInfo.text);
        if (directiveInfo.trailingCommentText ! = null) {
          sb.write(directiveInfo.trailingCommentText);
        }
        sb.write(endOfLine);
      }
      directivesCode = sb.toString();
      directivesCode = directivesCode.trimRight();
    }
    // prepare code
    var beforeDirectives = code.substring(0, firstDirectiveOffset);
    var afterDirectives = code.substring(lastDirectiveEnd);
    code = beforeDirectives + directivesCode + afterDirectives;
  }

  static _DirectivePriority getDirectivePriority(UriBasedDirective directive) {
    var uriContent = directive.uri.stringValue ?? ''; 
    if (directive is ImportDirective) {
      if (uriContent.startsWith('dart:')) {
        return _DirectivePriority. IMPORT_SDK;
      } else if (uriContent.startsWith('package:')) {
        return _DirectivePriority. IMPORT_PKG;
      } else if (uriContent.contains('://')) {
        return _DirectivePriority. IMPORT_OTHER;
      } else {
        return _DirectivePriority. IMPORT_REL;
      }
    }
    if (directive is ExportDirective) {
      if (uriContent.startsWith('dart:')) {
        return _DirectivePriority. EXPORT_SDK;
      } else if (uriContent.startsWith('package:')) {
        return _DirectivePriority. EXPORT_PKG;
      } else if (uriContent.contains('://')) {
        return _DirectivePriority. EXPORT_OTHER;
      } else {
        return _DirectivePriority. EXPORT_REL;
      }
    }
    if (directive is PartDirective) {
      return _DirectivePriority. PART;
    }
    return null;
  }

  /// Return the EOL to use for [code]. 
  static String getEOL(String code) {
    if (code.contains('\r\n')) {
      return '\r\n';
    } else {
      return '\n';
    }
  }

  /// Gets the first comment token considered to be the leading comment for this
  /// directive. 
  ///
  /// Leading comments for the first directive in a file are considered library
  /// comments and not returned unless they contain blank lines, in which case
  /// only the last part of the comment will be returned. 
  static Token getLeadingComment(
      CompilationUnit unit, UriBasedDirective directive, LineInfo lineInfo) {
    if (directive.beginToken.precedingComments == null) {
      return null;
    }

    var firstComment = directive.beginToken.precedingComments;
    var comment = firstComment;
    // Don't connect comments that have a blank line between them
    while (comment.next ! = null) {
      var currentLine = lineInfo.getLocation(comment.offset).lineNumber;
      var nextLine = lineInfo.getLocation(comment.next.offset).lineNumber;
      if (nextLine - currentLine > 1) {
        firstComment = comment.next;
      }
      comment = comment.next;
    }

    // Check if the comment is the first comment in the document
    if (firstComment ! = unit.beginToken.precedingComments) {
      var previousDirectiveLine =
          lineInfo.getLocation(directive.beginToken.previous.end).lineNumber;

      // Skip over any comments on the same line as the previous directive
      // as they will be attached to the end of it. 
      var comment = firstComment;
      while (comment ! = null &&
          previousDirectiveLine ==
              lineInfo.getLocation(comment.offset).lineNumber) {
        comment = comment.next;
      }
      return comment;
    }
    return null;
  }

  /// Gets the last comment token considered to be the trailing comment for this
  /// directive. 
  ///
  /// To be considered a trailing comment, the comment must be on the same line
  /// as the directive. 
  static Token getTrailingComment(CompilationUnit unit,
      UriBasedDirective directive, LineInfo lineInfo, int end) {
    var line = lineInfo.getLocation(end).lineNumber;
    Token comment = directive.endToken.next.precedingComments;
    while (comment ! = null) {
      if (lineInfo.getLocation(comment.offset).lineNumber == line) {
        return comment;
      }
      comment = comment.next;
    }
    return null;
  }
}

class _DirectiveInfo implements Comparable<_DirectiveInfo> {
  final UriBasedDirective directive;
  final _DirectivePriority priority;
  final String leadingCommentText;
  final String documentationText;
  final String annotationText;
  final String uri;
  final String trailingCommentText;

  /// The offset of the first token, usually the keyword but may include leading comments. 
  final int offset;

  /// The offset after the last token, including the end-of-line comment. 
  final int end;

  /// The text excluding comments, documentation and annotations. 
  final String text;

  _DirectiveInfo(
    this.directive,
    this.priority,
    this.leadingCommentText,
    this.documentationText,
    this.annotationText,
    this.uri,
    this.trailingCommentText,
    this.offset,
    this.end,
    this.text,
  );

  @override
  int compareTo(_DirectiveInfo other) {
    if (priority == other.priority) {
      return _compareUri(uri, other.uri);
    }
    return priority.ordinal - other.priority.ordinal;
  }

  @override
  String toString() => '(priority=$priority; text=$text)';

  static int _compareUri(String a, String b) {
    var aList = _splitUri(a);
    var bList = _splitUri(b);
    int result;
    if ((result = aList[0].compareTo(bList[0])) ! = 0) return result;
    if ((result = aList[1].compareTo(bList[1])) ! = 0) return result;
    return 0;
  }

  /// Split the given [uri] like `package:some.name/and/path.dart` into a list
  /// like `[package:some.name, and/path.dart]`. 
  static List<String> _splitUri(String uri) {
    var index = uri.indexOf('/');
    if (index == -1) {
      return <String>[uri, ''];
    }
    return <String>[uri.substring(0, index), uri.substring(index + 1)];
  }
}

class _DirectivePriority {
  static const IMPORT_SDK = _DirectivePriority('IMPORT_SDK', 0);
  static const IMPORT_PKG = _DirectivePriority('IMPORT_PKG', 1);
  static const IMPORT_OTHER = _DirectivePriority('IMPORT_OTHER', 2);
  static const IMPORT_REL = _DirectivePriority('IMPORT_REL', 3);
  static const EXPORT_SDK = _DirectivePriority('EXPORT_SDK', 4);
  static const EXPORT_PKG = _DirectivePriority('EXPORT_PKG', 5);
  static const EXPORT_OTHER = _DirectivePriority('EXPORT_OTHER', 6);
  static const EXPORT_REL = _DirectivePriority('EXPORT_REL', 7);
  static const PART = _DirectivePriority('PART', 8);

  final String name;
  final int ordinal;

  const _DirectivePriority(this.name, this.ordinal);

  @override
  String toString() => name;
}
