// 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/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/services/flutter/class_description.dart';
import 'package:analysis_server/src/services/flutter/property.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:dart_style/dart_style.dart';

/// The result of [WidgetDescriptions.setPropertyValue] invocation.
class SetPropertyValueResult {
  /// The error to report to the client, or `null` if OK.
  final protocol.RequestErrorCode? errorCode;

  /// The change to apply, or `null` if [errorCode] is not `null`.
  final protocol.SourceChange? change;

  new _({this.errorCode, this.change});
}

class WidgetDescriptions {
  final ClassDescriptionRegistry _classRegistry = ClassDescriptionRegistry();

  /// The mapping of identifiers of previously returned properties.
  final Map<int, PropertyDescription> _properties = {};

  /// Flush all data, because there was a change to a file.
  void flush() {
    _classRegistry.flush();
    _properties.clear();
  }

  /// Return the description of the widget with [InstanceCreationExpression] in
  /// the [resolvedUnit] at the [offset], or `null` if the location does not
  /// correspond to a widget.
  Future<protocol.FlutterGetWidgetDescriptionResult?> getDescription(
    ResolvedUnitResult resolvedUnit,
    int offset,
  ) async {
    var computer = _WidgetDescriptionComputer(
      _classRegistry,
      resolvedUnit,
      offset,
    );
    var widgetDescription = await computer.compute();

    if (widgetDescription == null) {
      return null;
    }

    var protocolProperties = _toProtocolProperties(
      widgetDescription.properties,
    );
    return protocol.FlutterGetWidgetDescriptionResult(protocolProperties);
  }

  Future<SetPropertyValueResult> setPropertyValue(
    int id,
    protocol.FlutterWidgetPropertyValue? value,
  ) async {
    var property = _properties[id];
    if (property == null) {
      return SetPropertyValueResult._(
        errorCode: protocol
            .RequestErrorCode
            .FLUTTER_SET_WIDGET_PROPERTY_VALUE_INVALID_ID,
      );
    }

    if (value == null) {
      if (property.protocolProperty.isRequired) {
        return SetPropertyValueResult._(
          errorCode: protocol
              .RequestErrorCode
              .FLUTTER_SET_WIDGET_PROPERTY_VALUE_IS_REQUIRED,
        );
      }
      var change = await property.removeValue();
      return SetPropertyValueResult._(change: change);
    } else {
      try {
        var change = await property.changeValue(value);
        return SetPropertyValueResult._(change: change);
      } on FormatterException {
        return SetPropertyValueResult._(
          errorCode: protocol
              .RequestErrorCode
              .FLUTTER_SET_WIDGET_PROPERTY_VALUE_INVALID_EXPRESSION,
        );
      }
    }
  }

  List<protocol.FlutterWidgetProperty> _toProtocolProperties(
    List<PropertyDescription> properties,
  ) {
    var protocolProperties = <protocol.FlutterWidgetProperty>[];
    for (var property in properties) {
      var protocolProperty = property.protocolProperty;

      _properties[protocolProperty.id] = property;

      protocolProperty.children = _toProtocolProperties(property.children);
      protocolProperties.add(protocolProperty);
    }
    return protocolProperties;
  }
}

class _WidgetDescription {
  final List<PropertyDescription> properties;

  new(this.properties);
}

class _WidgetDescriptionComputer {
  final ClassDescriptionRegistry classRegistry;

  /// The set of classes for which we are currently adding properties,
  /// used to prevent infinite recursion.
  final Set<InterfaceElement> elementsBeingProcessed = {};

  /// The resolved unit with the widget [InstanceCreationExpression].
  final ResolvedUnitResult resolvedUnit;

  /// The offset of the widget expression.
  final int widgetOffset;

  ClassElement? _classAlignment;
  ClassElement? _classAlignmentDirectional;
  ClassElement? _classContainer;
  ClassElement? _classEdgeInsets;

  new(this.classRegistry, this.resolvedUnit, this.widgetOffset);

  Future<_WidgetDescription?> compute() async {
    var node = resolvedUnit.unit.nodeCovering(offset: widgetOffset);
    if (node == null) {
      return null;
    }
    var instanceCreation = node.findInstanceCreationExpression;
    if (instanceCreation == null) {
      return null;
    }

    var constructorElement = instanceCreation.constructorName.element;
    if (constructorElement == null) {
      return null;
    }

    await _fetchClassElements();

    var properties = <PropertyDescription>[];
    _addProperties(properties: properties, instanceCreation: instanceCreation);
    _addContainerProperty(properties, instanceCreation);

    return _WidgetDescription(properties);
  }

  void _addContainerProperty(
    List<PropertyDescription> properties,
    InstanceCreationExpression widgetCreation,
  ) {
    if (!widgetCreation.isWidgetCreation) {
      return;
    }

    InstanceCreationExpression? parentCreation;
    var childArgument = widgetCreation.parent;
    if (childArgument is NamedArgument &&
        childArgument.name.lexeme == 'child') {
      var argumentList = childArgument.parent;
      var argumentListParent = argumentList?.parent;
      if (argumentList is ArgumentList &&
          argumentListParent is InstanceCreationExpression) {
        parentCreation = argumentListParent;
      }
    }

    PropertyDescription containerProperty;
    if (parentCreation?.isExactlyContainerCreation ?? false) {
      containerProperty = PropertyDescription(
        resolvedUnit: resolvedUnit,
        instanceCreation: parentCreation,
        protocolProperty: protocol.FlutterWidgetProperty(
          PropertyDescription.nextId(),
          true,
          false,
          'Container',
        ),
      );
      properties.add(containerProperty);

      _addProperties(
        properties: containerProperty.children,
        parent: containerProperty,
        instanceCreation: parentCreation,
      );

      containerProperty.children.removeWhere(
        (property) => property.name == 'child',
      );
    } else {
      var classContainer = _classContainer;
      if (classContainer == null) {
        return;
      }
      var containerDescription = classRegistry.get(classContainer);
      containerProperty = PropertyDescription(
        resolvedUnit: resolvedUnit,
        classDescription: containerDescription,
        protocolProperty: protocol.FlutterWidgetProperty(
          PropertyDescription.nextId(),
          true,
          false,
          'Container',
        ),
        virtualContainer: VirtualContainerProperty(
          classContainer,
          widgetCreation,
        ),
      );
      properties.add(containerProperty);

      _addProperties(
        properties: containerProperty.children,
        parent: containerProperty,
        classDescription: containerDescription,
      );

      if (parentCreation != null &&
          parentCreation.isExactlyAlignCreation &&
          parentCreation.argumentList.byName('widthFactor') == null &&
          parentCreation.argumentList.byName('heightFactor') == null) {
        _replaceNestedContainerProperty(
          containerProperty,
          parentCreation,
          'alignment',
        );
      }

      if (parentCreation != null && parentCreation.isExactlyPaddingCreation) {
        _replaceNestedContainerProperty(
          containerProperty,
          parentCreation,
          'padding',
        );
      }
    }

    containerProperty.children.removeWhere(
      (property) => property.name == 'child',
    );
  }

  void _addProperties({
    required List<PropertyDescription> properties,
    PropertyDescription? parent,
    ClassDescription? classDescription,
    InstanceCreationExpression? instanceCreation,
    ConstructorElement? constructorElement,
  }) {
    constructorElement ??= instanceCreation?.constructorName.element;
    constructorElement ??= classDescription?.constructor;
    if (constructorElement == null) return;

    var enclosingElement = constructorElement.enclosingElement;
    if (!elementsBeingProcessed.add(enclosingElement)) return;

    var existingNamed = <String>{};
    if (instanceCreation != null) {
      for (var argumentExpression in instanceCreation.argumentList.arguments) {
        var parameter = argumentExpression.correspondingParameter;
        if (parameter == null) continue;

        var valueExpression = argumentExpression.argumentExpression;
        if (argumentExpression is NamedArgument) {
          existingNamed.add(parameter.name!);
        }

        _addProperty(
          properties: properties,
          parent: parent,
          parameter: parameter,
          classDescription: classDescription,
          instanceCreation: instanceCreation,
          argumentExpression: argumentExpression,
          valueExpression: valueExpression,
        );
      }
    }

    for (var parameter in constructorElement.formalParameters) {
      if (!parameter.isNamed) continue;
      if (existingNamed.contains(parameter.name)) continue;

      _addProperty(
        properties: properties,
        parent: parent,
        parameter: parameter,
        classDescription: classDescription,
        instanceCreation: instanceCreation,
      );
    }

    elementsBeingProcessed.remove(enclosingElement);
  }

  void _addProperty({
    required List<PropertyDescription> properties,
    PropertyDescription? parent,
    required FormalParameterElement parameter,
    ClassDescription? classDescription,
    InstanceCreationExpression? instanceCreation,
    Argument? argumentExpression,
    Expression? valueExpression,
  }) {
    var documentation = getParameterDocumentation(parameter);

    String? valueExpressionCode;
    if (valueExpression != null) {
      valueExpressionCode = resolvedUnit.content.substring(
        valueExpression.offset,
        valueExpression.end,
      );
    }

    var isSafeToUpdate = false;
    protocol.FlutterWidgetPropertyValue? value;
    if (valueExpression != null) {
      value = _toValue(valueExpression);
      isSafeToUpdate = value != null;
    } else {
      isSafeToUpdate = true;
    }

    var propertyDescription = PropertyDescription(
      parent: parent,
      resolvedUnit: resolvedUnit,
      classDescription: classDescription,
      instanceCreation: instanceCreation,
      argumentExpression: argumentExpression,
      valueExpression: valueExpression,
      parameterElement: parameter,
      protocolProperty: protocol.FlutterWidgetProperty(
        PropertyDescription.nextId(),
        parameter.isRequiredPositional,
        isSafeToUpdate,
        parameter.name!,
        documentation: documentation,
        editor: _getEditor(parameter.type),
        expression: valueExpressionCode,
        value: value,
      ),
    );
    properties.add(propertyDescription);

    var classEdgeInsets = _classEdgeInsets;
    if (classEdgeInsets != null &&
        parameter.type.isExactEdgeInsetsGeometryType) {
      propertyDescription.addEdgeInsetsNestedProperties(classEdgeInsets);
    } else if (valueExpression is InstanceCreationExpression) {
      var type = valueExpression.staticType;
      if (type != null && classRegistry.hasNestedProperties(type)) {
        _addProperties(
          properties: propertyDescription.children,
          parent: propertyDescription,
          instanceCreation: valueExpression,
        );
      }
    } else if (valueExpression == null) {
      var type = parameter.type;
      if (type is InterfaceType) {
        var classDescription = classRegistry.get(type.element);
        if (classDescription != null) {
          _addProperties(
            properties: propertyDescription.children,
            parent: propertyDescription,
            classDescription: classDescription,
          );
        }
      }
    }
  }

  List<protocol.FlutterWidgetPropertyValueEnumItem> _enumItemsForEnum(
    EnumElement element,
  ) {
    return element.fields
        .where((field) => field.isStatic && field.isEnumConstant)
        .map(_toEnumItem)
        .toList();
  }

  List<protocol.FlutterWidgetPropertyValueEnumItem> _enumItemsForStaticFields(
    ClassElement classElement,
  ) {
    return classElement.fields
        .where((f) => f.isStatic)
        .map(_toEnumItem)
        .toList();
  }

  Future<void> _fetchClassElements() async {
    var sessionHelper = AnalysisSessionHelper(resolvedUnit.session);
    _classAlignment = await sessionHelper.getFlutterClass('Alignment');
    _classAlignmentDirectional = await sessionHelper.getFlutterClass(
      'AlignmentDirectional',
    );
    _classContainer = await sessionHelper.getFlutterClass('Container');
    _classEdgeInsets = await sessionHelper.getFlutterClass('EdgeInsets');
  }

  protocol.FlutterWidgetPropertyEditor? _getEditor(DartType type) {
    if (type.isDartCoreBool) {
      return protocol.FlutterWidgetPropertyEditor(
        protocol.FlutterWidgetPropertyEditorKind.BOOL,
      );
    }
    if (type.isDartCoreDouble) {
      return protocol.FlutterWidgetPropertyEditor(
        protocol.FlutterWidgetPropertyEditorKind.DOUBLE,
      );
    }
    if (type.isDartCoreInt) {
      return protocol.FlutterWidgetPropertyEditor(
        protocol.FlutterWidgetPropertyEditorKind.INT,
      );
    }
    if (type.isDartCoreString) {
      return protocol.FlutterWidgetPropertyEditor(
        protocol.FlutterWidgetPropertyEditorKind.STRING,
      );
    }
    if (type is InterfaceType) {
      var classElement = type.element;
      if (classElement is EnumElement) {
        return protocol.FlutterWidgetPropertyEditor(
          protocol.FlutterWidgetPropertyEditorKind.ENUM,
          enumItems: _enumItemsForEnum(classElement),
        );
      }
      if (classElement.isExactAlignmentGeometry) {
        var items = <protocol.FlutterWidgetPropertyValueEnumItem>[];
        var classAlignment = _classAlignment;
        if (classAlignment != null) {
          items.addAll(_enumItemsForStaticFields(classAlignment));
        }
        var classAlignmentDirectional = _classAlignmentDirectional;
        if (classAlignmentDirectional != null) {
          items.addAll(_enumItemsForStaticFields(classAlignmentDirectional));
        }
        return protocol.FlutterWidgetPropertyEditor(
          protocol.FlutterWidgetPropertyEditorKind.ENUM_LIKE,
          enumItems: items,
        );
      }
    }
    return null;
  }

  /// If the [parentCreation] has a property with the given [name], replace
  /// with it the corresponding nested property of the [containerProperty].
  void _replaceNestedContainerProperty(
    PropertyDescription containerProperty,
    InstanceCreationExpression parentCreation,
    String name,
  ) {
    var argument = parentCreation.argumentList.byName(name);
    if (argument != null) {
      var staticParameterElement = argument.correspondingParameter;
      if (staticParameterElement != null) {
        var replacements = <PropertyDescription>[];
        _addProperty(
          properties: replacements,
          parent: containerProperty,
          parameter: staticParameterElement,
          instanceCreation: parentCreation,
          argumentExpression: argument,
          valueExpression: argument.argumentExpression,
        );

        var replacement = replacements[0];
        containerProperty.replaceChild(name, replacement);
        containerProperty.virtualContainer?.setParentCreation(
          parentCreation,
          argument,
        );
      }
    }
  }

  protocol.FlutterWidgetPropertyValueEnumItem _toEnumItem(FieldElement field) {
    var interfaceElement = field.enclosingElement as InterfaceElement;
    var libraryUriStr = '${interfaceElement.enclosingElement.uri}';
    var documentation = getFieldDocumentation(field);

    return protocol.FlutterWidgetPropertyValueEnumItem(
      libraryUriStr,
      interfaceElement.name!,
      field.name!,
      documentation: documentation,
    );
  }

  protocol.FlutterWidgetPropertyValue? _toValue(Expression valueExpression) {
    if (valueExpression is BooleanLiteral) {
      return protocol.FlutterWidgetPropertyValue(
        boolValue: valueExpression.value,
      );
    } else if (valueExpression is DoubleLiteral) {
      return protocol.FlutterWidgetPropertyValue(
        doubleValue: valueExpression.value,
      );
    } else if (valueExpression is Identifier) {
      var element = valueExpression.element;
      if (element is GetterElement) {
        var field = element.variable;
        if (field is FieldElement && field.isStatic) {
          var enclosingClass = field.enclosingElement as InterfaceElement;
          if (field.isEnumConstant ||
              enclosingClass.isExactAlignment ||
              enclosingClass.isExactAlignmentDirectional) {
            return protocol.FlutterWidgetPropertyValue(
              enumValue: _toEnumItem(field),
            );
          }
        }
      }
    } else if (valueExpression is IntegerLiteral) {
      return protocol.FlutterWidgetPropertyValue(
        intValue: valueExpression.value,
      );
    } else if (valueExpression is SimpleStringLiteral) {
      return protocol.FlutterWidgetPropertyValue(
        stringValue: valueExpression.value,
      );
    }
    return null;
  }
}
