// Copyright (c) 2022, 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/protocol_server.dart';
import 'package:analyzer/src/test_utilities/platform.dart';
import 'package:collection/collection.dart';

import 'completion_check.dart';

class CompletionResponsePrinter {
  final StringBuffer buffer;
  final Configuration configuration;
  final CompletionResponseForTesting response;

  String _indent = '';

  new({
    required this.buffer,
    required this.configuration,
    required this.response,
  });

  void writeResponse() {
    _writeLocation();
    _writeResponseReplacement();
    _writeSuggestions();
  }

  /// Compares suggestions according to the configuration sorting.
  int _compareSuggestions(CompletionSuggestion a, CompletionSuggestion b) {
    int completionThenKind() {
      var completionDiff = a.completion.compareTo(b.completion);
      if (completionDiff != 0) {
        return completionDiff;
      } else {
        return a.kind.name.compareTo(b.kind.name);
      }
    }

    switch (configuration.sorting) {
      case Sorting.asIs:
        return 0;
      case Sorting.completionThenKind:
        return completionThenKind();
      case Sorting.relevanceThenCompletionThenKind:
        var relevanceDiff = a.relevance - b.relevance;
        if (relevanceDiff != 0) {
          return -relevanceDiff;
        } else {
          return completionThenKind();
        }
    }
  }

  String _escapeMultiLine(String text) {
    return text.replaceAll('\n', r'\n');
  }

  String _getElementKindName(ElementKind kind) {
    if (kind == ElementKind.CLASS) {
      return 'class';
    } else if (kind == ElementKind.CONSTRUCTOR) {
      return 'constructor';
    } else if (kind == ElementKind.ENUM) {
      return 'enum';
    } else if (kind == ElementKind.EXTENSION) {
      return 'extension';
    } else if (kind == ElementKind.FIELD) {
      return 'field';
    } else if (kind == ElementKind.FUNCTION) {
      return 'function';
    } else if (kind == ElementKind.PARAMETER) {
      return 'parameter';
    } else if (kind == ElementKind.TOP_LEVEL_VARIABLE) {
      return 'topLevelVariable';
    } else if (kind == ElementKind.TYPE_ALIAS) {
      return 'typeAlias';
    }
    throw UnimplementedError('kind: $kind');
  }

  String _getSuggestionKindName(CompletionSuggestion suggestion) {
    var kind = suggestion.kind;
    if (kind == CompletionSuggestionKind.KEYWORD) {
      return 'keyword';
    } else if (kind == CompletionSuggestionKind.IDENTIFIER) {
      var elementKind = suggestion.element?.kind;
      if (elementKind == null) {
        return 'identifier';
      } else if (elementKind == ElementKind.CLASS) {
        return 'class';
      } else if (elementKind == ElementKind.CONSTRUCTOR) {
        return 'constructor';
      } else if (elementKind == ElementKind.ENUM) {
        return 'enum';
      } else if (elementKind == ElementKind.ENUM_CONSTANT) {
        return 'enumConstant';
      } else if (elementKind == ElementKind.EXTENSION) {
        return 'extension';
      } else if (elementKind == ElementKind.EXTENSION_TYPE) {
        return 'extensionType';
      } else if (elementKind == ElementKind.FIELD) {
        return 'field';
      } else if (elementKind == ElementKind.FUNCTION) {
        return 'function';
      } else if (elementKind == ElementKind.GETTER) {
        return 'getter';
      } else if (elementKind == ElementKind.LABEL) {
        return 'label';
      } else if (elementKind == ElementKind.LIBRARY) {
        return 'library';
      } else if (elementKind == ElementKind.LOCAL_VARIABLE) {
        return 'localVariable';
      } else if (elementKind == ElementKind.METHOD) {
        return 'method';
      } else if (elementKind == ElementKind.MIXIN) {
        return 'mixin';
      } else if (elementKind == ElementKind.PARAMETER) {
        return 'parameter';
      } else if (elementKind == ElementKind.PREFIX) {
        return 'prefix';
      } else if (elementKind == ElementKind.SETTER) {
        return 'setter';
      } else if (elementKind == ElementKind.TOP_LEVEL_VARIABLE) {
        return 'topLevelVariable';
      } else if (elementKind == ElementKind.TYPE_ALIAS) {
        return 'typeAlias';
      } else if (elementKind == ElementKind.TYPE_PARAMETER) {
        return 'typeParameter';
      }
      throw UnimplementedError('elementKind: $elementKind');
    } else if (kind == CompletionSuggestionKind.INVOCATION) {
      var elementKind = suggestion.element?.kind;
      if (elementKind == null) {
        return 'invocation';
      } else if (elementKind == ElementKind.CONSTRUCTOR) {
        return 'constructorInvocation';
      } else if (elementKind == ElementKind.EXTENSION) {
        return 'extensionInvocation';
      } else if (elementKind == ElementKind.FUNCTION) {
        return 'functionInvocation';
      } else if (elementKind == ElementKind.METHOD) {
        return 'methodInvocation';
      }
      throw UnimplementedError('elementKind: $elementKind');
    } else if (kind == CompletionSuggestionKind.IMPORT) {
      return 'import';
    } else if (kind == CompletionSuggestionKind.NAMED_ARGUMENT) {
      return 'namedArgument';
    } else if (kind == CompletionSuggestionKind.OVERRIDE) {
      return 'override';
    }
    throw UnimplementedError('kind: $kind');
  }

  void _withIndent(void Function() f) {
    var indent = _indent;
    _indent = '$_indent  ';
    f();
    _indent = indent;
  }

  /// Writes the completion text, marked with a `[!range!]` or `^position`
  /// indicating the selection.
  void _writeCompletion(CompletionSuggestion suggestion) {
    var completion = suggestion.completion;

    var annotatedCompletion = completion;

    // Show the selection in the completion text using `[!range!]` or
    // `^position` markers.
    if (configuration.withSelection) {
      annotatedCompletion = switch ((
        suggestion.selectionOffset,
        suggestion.selectionLength,
      )) {
        // We don't annotate positions at the end of the string
        (int offset, 0) when offset == suggestion.completion.length =>
          completion,
        // Position
        (int offset, 0) => [
          completion.substring(0, offset),
          '^',
          completion.substring(offset),
        ].join(),
        // Range
        (int offset, int length) => [
          completion.substring(0, offset),
          '[!',
          completion.substring(offset, offset + length),
          '!]',
          completion.substring(offset + length),
        ].join(),
      };
    }

    if (RegExp(r'^\s').hasMatch(completion) ||
        RegExp(r'\s$').hasMatch(completion)) {
      _writelnWithIndent('|$annotatedCompletion|');
    } else {
      _writelnWithIndent(annotatedCompletion);
    }
  }

  void _writeDeclaringType(CompletionSuggestion suggestion) {
    if (configuration.withDeclaringType) {
      _writelnWithIndent('declaringType: ${suggestion.declaringType}');
    }
  }

  void _writeDefaultArgumentList(CompletionSuggestion suggestion) {
    if (configuration.withDefaultArgumentList) {
      _writelnWithIndent(
        'defaultArgumentList: ${suggestion.defaultArgumentListString}',
      );
      _writelnWithIndent(
        'defaultArgumentListRanges: ${suggestion.defaultArgumentListTextRanges}',
      );
    }
  }

  void _writeDeprecated(CompletionSuggestion suggestion) {
    if (suggestion.isDeprecated) {
      _writelnWithIndent('deprecated: true');
    }
  }

  void _writeDisplayText(CompletionSuggestion suggestion) {
    if (configuration.withDisplayText) {
      _writelnWithIndent('displayText: ${suggestion.displayText}');
    }
  }

  void _writeDocumentation(CompletionSuggestion suggestion) {
    if (configuration.withDocumentation) {
      var docComplete = suggestion.docComplete;
      if (docComplete != null) {
        var text = _escapeMultiLine(docComplete);
        _writelnWithIndent('docComplete: $text');
      }

      var docSummary = suggestion.docSummary;
      if (docSummary != null) {
        var text = _escapeMultiLine(docSummary);
        _writelnWithIndent('docSummary: $text');
      }
    }
  }

  void _writeElement(CompletionSuggestion suggestion) {
    if (configuration.withElement) {
      var element = suggestion.element;
      if (element != null) {
        _writelnWithIndent('element');
        _withIndent(() {
          var kindStr = _getElementKindName(element.kind);
          _writelnWithIndent('name: ${element.name}');
          _writelnWithIndent('kind: $kindStr');
        });
      }
    }
  }

  void _writeElementLocation(CompletionSuggestion suggestion) {
    if (configuration.withElementLocation) {
      var element = suggestion.element;
      if (element != null) {
        _writelnWithIndent('line: ${element.location?.startLine}');
        _writelnWithIndent('column: ${element.location?.startColumn}');
      }
    }
  }

  void _writeIsNotImported(CompletionSuggestion suggestion) {
    if (configuration.withIsNotImported) {
      _writelnWithIndent('isNotImported: ${suggestion.isNotImported}');
    }
  }

  void _writeLibraryUri(CompletionSuggestion suggestion) {
    if (configuration.withLibraryUri) {
      _writelnWithIndent('libraryUri: ${suggestion.libraryUri}');
    }
  }

  void _writelnWithIndent(String line) {
    buffer.write(_indent);
    buffer.write(line);
    // Always write the EOLs being assumed in this test run because any
    // multiline content will (if no bugs) be written with these, and we don't
    // want to perform any normalization on the actual results because that
    // could mask bugs.
    buffer.write(testEol);
  }

  void _writeLocation() {
    if (configuration.withLocationName) {
      if (response.requestLocationName case var location?) {
        _writelnWithIndent('location: $location');
      }
      // TODO(scheglov): will be removed
      if (response.opTypeLocationName case var location?) {
        _writelnWithIndent('locationOpType: $location');
      }
    }
  }

  void _writeParameterNames(CompletionSuggestion suggestion) {
    if (configuration.withParameterNames) {
      var parameterNames = suggestion.parameterNames?.join(',') ?? '';
      if (parameterNames.isNotEmpty) {
        parameterNames = ' $parameterNames';
      }
      _writelnWithIndent('parameterNames:$parameterNames');
      var parameterTypes = suggestion.parameterTypes?.join(',') ?? '';
      if (parameterTypes.isNotEmpty) {
        parameterTypes = ' $parameterTypes';
      }
      _writelnWithIndent('parameterTypes:$parameterTypes');
    }
  }

  void _writeRelevance(CompletionSuggestion suggestion) {
    if (configuration.withRelevance) {
      _writelnWithIndent('relevance: ${suggestion.relevance}');
    }
  }

  void _writeResponseReplacement() {
    if (configuration.withReplacement) {
      var offset = response.replacementOffset;
      var length = response.replacementLength;
      var left = response.requestOffset - offset;
      var right = (offset + length) - response.requestOffset;
      if (left > 0 || right > 0) {
        _writelnWithIndent('replacement');
        if (left > 0) {
          _withIndent(() {
            _writelnWithIndent('left: $left');
          });
        }
        if (right > 0) {
          _withIndent(() {
            _writelnWithIndent('right: $right');
          });
        }
      }
    }
  }

  void _writeReturnType(CompletionSuggestion suggestion) {
    if (configuration.withReturnType) {
      var returnType = suggestion.returnType;
      if (returnType != null) {
        _writelnWithIndent('returnType: $returnType');
      }
    }
  }

  void _writeSuggestion(CompletionSuggestion suggestion) {
    _writeCompletion(suggestion);
    _withIndent(() {
      _writeSuggestionKind(suggestion);
      _writeElementLocation(suggestion);
      _writeDeclaringType(suggestion);
      _writeDeprecated(suggestion);
      _writeDefaultArgumentList(suggestion);
      _writeDisplayText(suggestion);
      _writeDocumentation(suggestion);
      _writeElement(suggestion);
      _writeIsNotImported(suggestion);
      _writeLibraryUri(suggestion);
      _writeParameterNames(suggestion);
      _writeRelevance(suggestion);
      _writeReturnType(suggestion);
    });
  }

  void _writeSuggestionKind(CompletionSuggestion suggestion) {
    if (configuration.withKind) {
      var kind = _getSuggestionKindName(suggestion);
      _writelnWithIndent('kind: $kind');
    }
  }

  void _writeSuggestions() {
    var filtered = response.suggestions.where(configuration.filter);
    var sorted = filtered.sorted(_compareSuggestions);

    _writelnWithIndent('suggestions');

    _withIndent(() {
      for (var suggestion in sorted) {
        if (configuration.filter(suggestion)) {
          _writeSuggestion(suggestion);
        }
      }
    });
  }
}

class Configuration {
  Sorting sorting;
  bool withDeclaringType;
  bool withDefaultArgumentList;
  bool withDisplayText;
  bool withDocumentation;
  bool withElement;
  bool withElementLocation;
  bool withIsNotImported;
  bool withKind;
  bool withLibraryUri;
  bool withLocationName;
  bool withParameterNames;
  bool withRelevance;
  bool withReplacement;
  bool withReturnType;
  bool withSelection;
  bool Function(CompletionSuggestion suggestion) filter;

  new({
    this.sorting = Sorting.relevanceThenCompletionThenKind,
    this.withDeclaringType = false,
    this.withDefaultArgumentList = false,
    this.withDisplayText = false,
    this.withDocumentation = false,
    this.withElement = false,
    this.withElementLocation = false,
    this.withIsNotImported = false,
    this.withKind = true,
    this.withLibraryUri = false,
    this.withLocationName = false,
    this.withParameterNames = false,
    this.withReplacement = true,
    this.withRelevance = false,
    this.withReturnType = false,
    this.withSelection = true,
    required this.filter,
  });
}

enum Sorting { asIs, completionThenKind, relevanceThenCompletionThenKind }
