// 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 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/computer/computer_folding.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analyzer/source/line_info.dart';

typedef StaticOptions =
    Either3<bool, FoldingRangeOptions, FoldingRangeRegistrationOptions>;

class FoldingHandler
    extends LspMessageHandler<FoldingRangeParams, List<FoldingRange>> {
  new(super.server);
  @override
  Method get handlesMessage => Method.textDocument_foldingRange;

  @override
  LspJsonHandler<FoldingRangeParams> get jsonHandler =>
      FoldingRangeParams.jsonHandler;

  @override
  Future<ErrorOr<List<FoldingRange>>> handle(
    FoldingRangeParams params,
    MessageInfo message,
    CancellationToken token,
  ) async {
    var clientCapabilities = message.clientCapabilities;
    if (clientCapabilities == null) {
      // This should not happen unless a client misbehaves.
      return serverNotInitializedError;
    }

    var lineFoldingOnly = clientCapabilities.lineFoldingOnly;
    var path = pathOfDoc(params.textDocument);

    return path.mapResult((path) async {
      var partialResults = <List<FoldingRegion>>[];
      LineInfo? lineInfo;

      var unit = await server.getParsedUnit(path);
      if (unit != null) {
        lineInfo = unit.lineInfo;

        var regions = DartUnitFoldingComputer(lineInfo, unit.unit).compute();
        partialResults.insert(0, regions);
      }

      // Still try to obtain line info for invalid or non-Dart files, as plugins
      // could contribute to those.
      lineInfo ??= server.getLineInfo(path);

      if (lineInfo == null) {
        // Line information would be required to translate folding results to
        // LSP.
        return success(const []);
      }

      var notificationManager = server.notificationManager;
      var pluginResults = notificationManager.folding.getResults(path);
      partialResults.addAll(pluginResults);

      var regions = notificationManager.merger.mergeFoldingRegions(
        partialResults,
      );

      // Ensure sorted by offset for when looking for overlapping ranges in
      // line mode below.
      regions.sort((r1, r2) => r1.offset.compareTo(r2.offset));

      var foldingRanges = regions
          .map(
            (region) =>
                _toFoldingRange(lineInfo!, region, lineOnly: lineFoldingOnly),
          )
          .toList();

      // When in line-only mode, ranges that end on the same line that another
      // ranges starts should be truncated to be on the line before (and if this
      // leave them spanning only a single line, should be removed).
      if (lineFoldingOnly) {
        _compensateForLineFolding(foldingRanges);
      }

      return success(foldingRanges);
    });
  }

  /// Adjust [foldingRanges] taking into count additional rules for line
  /// folding.
  ///
  /// When character folding is supported, a range may start on the same line
  /// that another ends (as long as they don't overlap).
  ///
  /// When only line folding is supported, ranges must not end on the same line
  /// that another starts. In this case, we shrink the previous range (and if
  /// this makes it a single line, remove it).
  void _compensateForLineFolding(List<FoldingRange> foldingRanges) {
    // Loop over items except last (`-1`). We can skip the last item because
    // it has no next item.
    for (var i = 0; i < foldingRanges.length - 1; i++) {
      var range = foldingRanges[i];
      var next = foldingRanges[i + 1];
      // If this item runs into the next but does not enclose it...
      if (range.endLine >= next.startLine && range.endLine < next.endLine) {
        // Truncate it to end on the line before.
        var newEndLine = next.startLine - 1;

        // If it no longer needs to be a folding range at all, remove it.
        if (newEndLine <= range.startLine) {
          foldingRanges.removeAt(i);
          i--;
          continue;
        }

        foldingRanges[i] = FoldingRange(
          startLine: range.startLine,
          endLine: newEndLine,
          kind: range.kind,
        );
      }
    }
  }

  FoldingRange _toFoldingRange(
    LineInfo lineInfo,
    FoldingRegion region, {
    required bool lineOnly,
  }) {
    var range = toRange(lineInfo, region.offset, region.length);
    return FoldingRange(
      startLine: range.start.line,
      startCharacter: lineOnly ? null : range.start.character,
      endLine: range.end.line,
      endCharacter: lineOnly ? null : range.end.character,
      kind: toFoldingRangeKind(region.kind),
    );
  }
}

class FoldingRegistrations extends FeatureRegistration
    with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
  new(super.info);

  @override
  ToJsonable? get options =>
      TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes);

  @override
  Method get registrationMethod => Method.textDocument_foldingRange;

  @override
  StaticOptions get staticOptions => Either3.t1(true);

  @override
  bool get supportsDynamic => clientDynamic.folding;
}
