// 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 'dart:collection';
import 'dart:convert' hide JsonDecoder;

import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
    show JsonDecoder;
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';

export 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
    show JsonDecoder;

// Ignored for legacy code reasons; this variable is used across a few packages.
// ignore: non_constant_identifier_names
final Map<String, RefactoringKind> REQUEST_ID_REFACTORING_KINDS =
    HashMap<String, RefactoringKind>();

/// Compare the lists [listA] and [listB], using [itemEqual] to compare
/// list elements.
bool listEqual<T>(
  List<T>? listA,
  List<T>? listB,
  bool Function(T a, T b) itemEqual,
) {
  if (listA == null) {
    return listB == null;
  }
  if (listB == null) {
    return false;
  }
  if (listA.length != listB.length) {
    return false;
  }
  for (var i = 0; i < listA.length; i++) {
    if (!itemEqual(listA[i], listB[i])) {
      return false;
    }
  }
  return true;
}

/// Compare the maps [mapA] and [mapB], using [valueEqual] to compare map
/// values.
bool mapEqual<K, V>(
  Map<K, V>? mapA,
  Map<K, V>? mapB,
  bool Function(V a, V b) valueEqual,
) {
  if (mapA == null) {
    return mapB == null;
  }
  if (mapB == null) {
    return false;
  }
  if (mapA.length != mapB.length) {
    return false;
  }
  for (var entryA in mapA.entries) {
    var key = entryA.key;
    var valueA = entryA.value;
    var valueB = mapB[key];
    if (valueB == null || !valueEqual(valueA, valueB)) {
      return false;
    }
  }
  return true;
}

/// Translate the input [map], applying [keyCallback] to all its keys, and
/// [valueCallback] to all its values.
Map<KR, VR> mapMap<KP, VP, KR, VR>(
  Map<KP, VP> map, {
  KR Function(KP key)? keyCallback,
  VR Function(VP value)? valueCallback,
}) {
  var result = <KR, VR>{};
  map.forEach((key, value) {
    KR resultKey;
    VR resultValue;
    if (keyCallback != null) {
      resultKey = keyCallback(key);
    } else {
      resultKey = key as KR;
    }
    if (valueCallback != null) {
      resultValue = valueCallback(value);
    } else {
      resultValue = value as VR;
    }
    result[resultKey] = resultValue;
  });
  return result;
}

/// Create a [RefactoringFeedback] corresponding the given [kind].
RefactoringFeedback? refactoringFeedbackFromJson(
  JsonDecoder jsonDecoder,
  String jsonPath,
  Object? json,
  Map<Object?, Object?> feedbackJson, {
  required ClientUriConverter? clientUriConverter,
}) {
  var kind = jsonDecoder.refactoringKind;
  if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) {
    return ExtractLocalVariableFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.EXTRACT_METHOD) {
    return ExtractMethodFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.EXTRACT_WIDGET) {
    return ExtractWidgetFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.INLINE_LOCAL_VARIABLE) {
    return InlineLocalVariableFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.INLINE_METHOD) {
    return InlineMethodFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.RENAME) {
    return RenameFeedback.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  return null;
}

/// Create a [RefactoringOptions] corresponding the given [kind].
RefactoringOptions? refactoringOptionsFromJson(
  JsonDecoder jsonDecoder,
  String jsonPath,
  Object? json,
  RefactoringKind kind, {
  required ClientUriConverter? clientUriConverter,
}) {
  if (kind == RefactoringKind.EXTRACT_LOCAL_VARIABLE) {
    return ExtractLocalVariableOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.EXTRACT_METHOD) {
    return ExtractMethodOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.EXTRACT_WIDGET) {
    return ExtractWidgetOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.INLINE_METHOD) {
    return InlineMethodOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.MOVE_FILE) {
    return MoveFileOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  if (kind == RefactoringKind.RENAME) {
    return RenameOptions.fromJson(
      jsonDecoder,
      jsonPath,
      json,
      clientUriConverter: clientUriConverter,
    );
  }
  return null;
}

/// Instances of the class [HasToJson] implement [toJson] method that returns
/// a JSON presentation.
abstract class HasToJson {
  /// Returns a JSON presentation of the object.
  Map<String, Object> toJson({required ClientUriConverter? clientUriConverter});
}

/// JsonDecoder for decoding requests.  Errors are reporting by throwing a
/// [RequestFailure].
class RequestDecoder extends JsonDecoder {
  /// The request being deserialized.
  final Request _request;

  new(this._request);

  @override
  RefactoringKind? get refactoringKind {
    // Refactoring feedback objects should never appear in requests.
    return null;
  }

  @override
  Object mismatch(String jsonPath, String expected, [Object? actual]) {
    var buffer = StringBuffer();
    buffer.write('Expected to be ');
    buffer.write(expected);
    if (actual != null) {
      buffer.write('; found "');
      buffer.write(json.encode(actual));
      buffer.write('"');
    }
    return RequestFailure(
      Response.invalidParameter(_request, jsonPath, buffer.toString()),
    );
  }

  @override
  Object missingKey(String jsonPath, String key) {
    return RequestFailure(
      Response.invalidParameter(
        _request,
        jsonPath,
        'Expected to contain key ${json.encode(key)}',
      ),
    );
  }
}

abstract class RequestParams implements HasToJson {
  /// Return a request whose parameters are taken from this object and that has
  /// the given [id].
  Request toRequest(
    String id, {
    required ClientUriConverter? clientUriConverter,
  });
}

/// JsonDecoder for decoding responses from the server.  This is intended to be
/// used only for testing.  Errors are reported using bare [Exception] objects.
class ResponseDecoder extends JsonDecoder {
  @override
  final RefactoringKind? refactoringKind;

  new(this.refactoringKind);

  @override
  Object mismatch(String jsonPath, String expected, [Object? actual]) {
    var buffer = StringBuffer();
    buffer.write('Expected ');
    buffer.write(expected);
    if (actual != null) {
      buffer.write(' found "');
      buffer.write(json.encode(actual));
      buffer.write('"');
    }
    buffer.write(' at ');
    buffer.write(jsonPath);
    return Exception(buffer.toString());
  }

  @override
  Object missingKey(String jsonPath, String key) {
    return Exception('Missing key $key at $jsonPath');
  }
}

/// The result data associated with a response.
abstract class ResponseResult implements HasToJson {
  /// Return a response whose result data is this object for the request with
  /// the given [id].
  Response toResponse(
    String id, {
    required ClientUriConverter? clientUriConverter,
  });
}
