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

library services.src.refactoring.sort_members;

import 'package:analysis_server/plugin/protocol/protocol.dart' hide Element;
import 'package:analysis_server/src/services/correction/strings.dart';
import 'package:analyzer/dart/ast/ast.dart';

/**
 * Sorter for unit/class members.
 */
class MemberSorter {
  static List<_PriorityItem> _PRIORITY_ITEMS = [
    new _PriorityItem(false, _MemberKind.UNIT_FUNCTION_MAIN, false),
    new _PriorityItem(false, _MemberKind.UNIT_VARIABLE_CONST, false),
    new _PriorityItem(false, _MemberKind.UNIT_VARIABLE_CONST, true),
    new _PriorityItem(false, _MemberKind.UNIT_VARIABLE, false),
    new _PriorityItem(false, _MemberKind.UNIT_VARIABLE, true),
    new _PriorityItem(false, _MemberKind.UNIT_ACCESSOR, false),
    new _PriorityItem(false, _MemberKind.UNIT_ACCESSOR, true),
    new _PriorityItem(false, _MemberKind.UNIT_FUNCTION, false),
    new _PriorityItem(false, _MemberKind.UNIT_FUNCTION, true),
    new _PriorityItem(false, _MemberKind.UNIT_FUNCTION_TYPE, false),
    new _PriorityItem(false, _MemberKind.UNIT_FUNCTION_TYPE, true),
    new _PriorityItem(false, _MemberKind.UNIT_CLASS, false),
    new _PriorityItem(false, _MemberKind.UNIT_CLASS, true),
    new _PriorityItem(true, _MemberKind.CLASS_FIELD, false),
    new _PriorityItem(true, _MemberKind.CLASS_ACCESSOR, false),
    new _PriorityItem(true, _MemberKind.CLASS_ACCESSOR, true),
    new _PriorityItem(false, _MemberKind.CLASS_FIELD, false),
    new _PriorityItem(false, _MemberKind.CLASS_CONSTRUCTOR, false),
    new _PriorityItem(false, _MemberKind.CLASS_CONSTRUCTOR, true),
    new _PriorityItem(false, _MemberKind.CLASS_ACCESSOR, false),
    new _PriorityItem(false, _MemberKind.CLASS_ACCESSOR, true),
    new _PriorityItem(false, _MemberKind.CLASS_METHOD, false),
    new _PriorityItem(false, _MemberKind.CLASS_METHOD, true),
    new _PriorityItem(true, _MemberKind.CLASS_METHOD, false),
    new _PriorityItem(true, _MemberKind.CLASS_METHOD, true)
  ];

  final String initialCode;
  final CompilationUnit unit;
  String code;
  String endOfLine;

  MemberSorter(this.initialCode, this.unit) {
    this.code = initialCode;
    this.endOfLine = getEOL(code);
  }

  /**
   * Return the [SourceEdit]s that sort [unit].
   */
  List<SourceEdit> sort() {
    _sortClassesMembers();
    _sortUnitMembers();
    // Must sort unit directives last because it may insert newlines, which
    // would confuse the offsets used by the other sort functions.
    _sortUnitDirectives();
    // prepare edits
    List<SourceEdit> edits = <SourceEdit>[];
    if (code != initialCode) {
      SimpleDiff diff = computeSimpleDiff(initialCode, code);
      SourceEdit edit =
          new SourceEdit(diff.offset, diff.length, diff.replacement);
      edits.add(edit);
    }
    return edits;
  }

  void _sortAndReorderMembers(List<_MemberInfo> members) {
    List<_MemberInfo> membersSorted = _getSortedMembers(members);
    int size = membersSorted.length;
    for (int i = 0; i < size; i++) {
      _MemberInfo newInfo = membersSorted[size - 1 - i];
      _MemberInfo oldInfo = members[size - 1 - i];
      if (newInfo != oldInfo) {
        String beforeCode = code.substring(0, oldInfo.offset);
        String afterCode = code.substring(oldInfo.end);
        code = beforeCode + newInfo.text + afterCode;
      }
    }
  }

  /**
   * Sorts all members of all [ClassDeclaration]s.
   */
  void _sortClassesMembers() {
    for (CompilationUnitMember unitMember in unit.declarations) {
      if (unitMember is ClassDeclaration) {
        ClassDeclaration classDeclaration = unitMember;
        _sortClassMembers(classDeclaration);
      }
    }
  }

  /**
   * Sorts all members of the given [ClassDeclaration].
   */
  void _sortClassMembers(ClassDeclaration classDeclaration) {
    List<_MemberInfo> members = <_MemberInfo>[];
    for (ClassMember member in classDeclaration.members) {
      _MemberKind kind = null;
      bool isStatic = false;
      String name = null;
      if (member is ConstructorDeclaration) {
        kind = _MemberKind.CLASS_CONSTRUCTOR;
        SimpleIdentifier nameNode = member.name;
        if (nameNode == null) {
          name = "";
        } else {
          name = nameNode.name;
        }
      }
      if (member is FieldDeclaration) {
        FieldDeclaration fieldDeclaration = member;
        List<VariableDeclaration> fields = fieldDeclaration.fields.variables;
        if (!fields.isEmpty) {
          kind = _MemberKind.CLASS_FIELD;
          isStatic = fieldDeclaration.isStatic;
          name = fields[0].name.name;
        }
      }
      if (member is MethodDeclaration) {
        MethodDeclaration method = member;
        isStatic = method.isStatic;
        name = method.name.name;
        if (method.isGetter) {
          kind = _MemberKind.CLASS_ACCESSOR;
          name += " getter";
        } else if (method.isSetter) {
          kind = _MemberKind.CLASS_ACCESSOR;
          name += " setter";
        } else {
          kind = _MemberKind.CLASS_METHOD;
        }
      }
      if (name != null) {
        _PriorityItem item = new _PriorityItem.forName(isStatic, name, kind);
        int offset = member.offset;
        int length = member.length;
        String text = code.substring(offset, offset + length);
        members.add(new _MemberInfo(item, name, offset, length, text));
      }
    }
    // do sort
    _sortAndReorderMembers(members);
  }

  /**
   * Sorts all [Directive]s.
   */
  void _sortUnitDirectives() {
    bool hasLibraryDirective = false;
    List<_DirectiveInfo> directives = [];
    for (Directive directive in unit.directives) {
      if (directive is LibraryDirective) {
        hasLibraryDirective = true;
      }
      if (directive is! UriBasedDirective) {
        continue;
      }
      UriBasedDirective uriDirective = directive as UriBasedDirective;
      String uriContent = uriDirective.uri.stringValue;
      _DirectivePriority kind = null;
      if (directive is ImportDirective) {
        if (uriContent.startsWith("dart:")) {
          kind = _DirectivePriority.IMPORT_SDK;
        } else if (uriContent.startsWith("package:")) {
          kind = _DirectivePriority.IMPORT_PKG;
        } else if (uriContent.contains('://')) {
          kind = _DirectivePriority.IMPORT_OTHER;
        } else {
          kind = _DirectivePriority.IMPORT_REL;
        }
      }
      if (directive is ExportDirective) {
        if (uriContent.startsWith("dart:")) {
          kind = _DirectivePriority.EXPORT_SDK;
        } else if (uriContent.startsWith("package:")) {
          kind = _DirectivePriority.EXPORT_PKG;
        } else if (uriContent.contains('://')) {
          kind = _DirectivePriority.EXPORT_OTHER;
        } else {
          kind = _DirectivePriority.EXPORT_REL;
        }
      }
      if (directive is PartDirective) {
        kind = _DirectivePriority.PART;
      }
      if (kind != null) {
        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);
        }
        int offset = directive.firstTokenAfterCommentAndMetadata.offset;
        int length = directive.end - offset;
        String text = code.substring(offset, offset + length);
        directives.add(new _DirectiveInfo(directive, kind, uriContent,
            documentationText, annotationText, text));
      }
    }
    // nothing to do
    if (directives.isEmpty) {
      return;
    }
    int firstDirectiveOffset = directives[0].directive.offset;
    int lastDirectiveEnd = directives[directives.length - 1].directive.end;
    // Without a library directive, the library comment is the comment of the
    // first directive.
    _DirectiveInfo libraryDocumentationDirective;
    if (!hasLibraryDirective && directives.isNotEmpty) {
      libraryDocumentationDirective = directives.first;
    }
    // do sort
    directives.sort();
    // append directives with grouping
    String directivesCode;
    {
      StringBuffer sb = new StringBuffer();
      String endOfLine = this.endOfLine;
      _DirectivePriority currentPriority = null;
      bool firstOutputDirective = true;
      for (_DirectiveInfo directive in directives) {
        if (currentPriority != directive.priority) {
          if (sb.length != 0) {
            sb.write(endOfLine);
          }
          currentPriority = directive.priority;
        }
        if (directive != libraryDocumentationDirective &&
            directive.documentationText != null) {
          sb.write(directive.documentationText);
          sb.write(endOfLine);
        }
        if (firstOutputDirective) {
          firstOutputDirective = false;
          if (libraryDocumentationDirective != null &&
              libraryDocumentationDirective.documentationText != null) {
            sb.write(libraryDocumentationDirective.documentationText);
            sb.write(endOfLine);
          }
        }
        if (directive.annotationText != null) {
          sb.write(directive.annotationText);
          sb.write(endOfLine);
        }
        sb.write(directive.text);
        sb.write(endOfLine);
      }
      directivesCode = sb.toString();
      directivesCode = directivesCode.trimRight();
    }
    // prepare code
    String beforeDirectives = code.substring(0, firstDirectiveOffset);
    String afterDirectives = code.substring(lastDirectiveEnd);
    code = beforeDirectives + directivesCode + afterDirectives;
  }

  /**
   * Sorts all [CompilationUnitMember]s.
   */
  void _sortUnitMembers() {
    List<_MemberInfo> members = [];
    for (CompilationUnitMember member in unit.declarations) {
      _MemberKind kind = null;
      String name = null;
      if (member is ClassDeclaration) {
        kind = _MemberKind.UNIT_CLASS;
        name = member.name.name;
      }
      if (member is ClassTypeAlias) {
        kind = _MemberKind.UNIT_CLASS;
        name = member.name.name;
      }
      if (member is EnumDeclaration) {
        kind = _MemberKind.UNIT_CLASS;
        name = member.name.name;
      }
      if (member is FunctionDeclaration) {
        FunctionDeclaration function = member;
        name = function.name.name;
        if (function.isGetter) {
          kind = _MemberKind.UNIT_ACCESSOR;
          name += " getter";
        } else if (function.isSetter) {
          kind = _MemberKind.UNIT_ACCESSOR;
          name += " setter";
        } else {
          if (name == 'main') {
            kind = _MemberKind.UNIT_FUNCTION_MAIN;
          } else {
            kind = _MemberKind.UNIT_FUNCTION;
          }
        }
      }
      if (member is FunctionTypeAlias) {
        kind = _MemberKind.UNIT_FUNCTION_TYPE;
        name = member.name.name;
      }
      if (member is TopLevelVariableDeclaration) {
        TopLevelVariableDeclaration variableDeclaration = member;
        List<VariableDeclaration> variables =
            variableDeclaration.variables.variables;
        if (!variables.isEmpty) {
          if (variableDeclaration.variables.isConst) {
            kind = _MemberKind.UNIT_VARIABLE_CONST;
          } else {
            kind = _MemberKind.UNIT_VARIABLE;
          }
          name = variables[0].name.name;
        }
      }
      if (name != null) {
        _PriorityItem item = new _PriorityItem.forName(false, name, kind);
        int offset = member.offset;
        int length = member.length;
        String text = code.substring(offset, offset + length);
        members.add(new _MemberInfo(item, name, offset, length, text));
      }
    }
    // do sort
    _sortAndReorderMembers(members);
  }

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

  static int _getPriority(_PriorityItem item) {
    for (int i = 0; i < _PRIORITY_ITEMS.length; i++) {
      if (_PRIORITY_ITEMS[i] == item) {
        return i;
      }
    }
    return 0;
  }

  static List<_MemberInfo> _getSortedMembers(List<_MemberInfo> members) {
    List<_MemberInfo> membersSorted = new List<_MemberInfo>.from(members);
    membersSorted.sort((_MemberInfo o1, _MemberInfo o2) {
      int priority1 = _getPriority(o1.item);
      int priority2 = _getPriority(o2.item);
      if (priority1 == priority2) {
        // don't reorder class fields
        if (o1.item.kind == _MemberKind.CLASS_FIELD) {
          return o1.offset - o2.offset;
        }
        // sort all other members by name
        String name1 = o1.name.toLowerCase();
        String name2 = o2.name.toLowerCase();
        return name1.compareTo(name2);
      }
      return priority1 - priority2;
    });
    return membersSorted;
  }
}

class _DirectiveInfo implements Comparable<_DirectiveInfo> {
  final Directive directive;
  final _DirectivePriority priority;
  final String uri;
  final String documentationText;
  final String annotationText;
  final String text;

  _DirectiveInfo(this.directive, this.priority, this.uri,
      this.documentationText, this.annotationText, 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) {
    List<String> aList = _splitUri(a);
    List<String> 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) {
    int 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 = const _DirectivePriority('IMPORT_SDK', 0);
  static const IMPORT_PKG = const _DirectivePriority('IMPORT_PKG', 1);
  static const IMPORT_OTHER = const _DirectivePriority('IMPORT_OTHER', 2);
  static const IMPORT_REL = const _DirectivePriority('IMPORT_REL', 3);
  static const EXPORT_SDK = const _DirectivePriority('EXPORT_SDK', 4);
  static const EXPORT_PKG = const _DirectivePriority('EXPORT_PKG', 5);
  static const EXPORT_OTHER = const _DirectivePriority('EXPORT_OTHER', 6);
  static const EXPORT_REL = const _DirectivePriority('EXPORT_REL', 7);
  static const PART = const _DirectivePriority('PART', 8);

  final String name;
  final int ordinal;

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

  @override
  String toString() => name;
}

class _MemberInfo {
  final _PriorityItem item;
  final String name;
  final int offset;
  final int length;
  final int end;
  final String text;

  _MemberInfo(this.item, this.name, int offset, int length, this.text)
      : offset = offset,
        length = length,
        end = offset + length;

  @override
  String toString() {
    return '(priority=$item; name=$name; offset=$offset; length=$length)';
  }
}

class _MemberKind {
  static const UNIT_FUNCTION_MAIN = const _MemberKind('UNIT_FUNCTION_MAIN', 0);
  static const UNIT_ACCESSOR = const _MemberKind('UNIT_ACCESSOR', 1);
  static const UNIT_FUNCTION = const _MemberKind('UNIT_FUNCTION', 2);
  static const UNIT_FUNCTION_TYPE = const _MemberKind('UNIT_FUNCTION_TYPE', 3);
  static const UNIT_CLASS = const _MemberKind('UNIT_CLASS', 4);
  static const UNIT_VARIABLE_CONST = const _MemberKind('UNIT_VARIABLE', 5);
  static const UNIT_VARIABLE = const _MemberKind('UNIT_VARIABLE', 6);
  static const CLASS_ACCESSOR = const _MemberKind('CLASS_ACCESSOR', 7);
  static const CLASS_CONSTRUCTOR = const _MemberKind('CLASS_CONSTRUCTOR', 8);
  static const CLASS_FIELD = const _MemberKind('CLASS_FIELD', 9);
  static const CLASS_METHOD = const _MemberKind('CLASS_METHOD', 10);

  final String name;
  final int ordinal;

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

  @override
  String toString() => name;
}

class _PriorityItem {
  final _MemberKind kind;
  final bool isPrivate;
  final bool isStatic;

  _PriorityItem(this.isStatic, this.kind, this.isPrivate);

  factory _PriorityItem.forName(bool isStatic, String name, _MemberKind kind) {
    bool isPrivate = Identifier.isPrivateName(name);
    return new _PriorityItem(isStatic, kind, isPrivate);
  }

  @override
  bool operator ==(Object obj) {
    _PriorityItem other = obj as _PriorityItem;
    if (kind == _MemberKind.CLASS_FIELD) {
      return other.kind == kind && other.isStatic == isStatic;
    }
    return other.kind == kind &&
        other.isPrivate == isPrivate &&
        other.isStatic == isStatic;
  }

  @override
  String toString() => kind.toString();
}
