// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import '../util/map.dart';
import '../util/nullable.dart';
import '../value.dart';
import 'compilation_dispatcher.dart';
import 'embedded_sass.pb.dart' as proto;
import 'embedded_sass.pb.dart' hide Value, ListSeparator, CalculationOperator;
import 'host_callable.dart';
import 'opaque_registry.dart';
import 'utils.dart';

/// A class that converts Sass [Value] objects into [Value] protobufs.
///
/// A given [Protofier] instance is valid only within the scope of a single
/// custom function call.
final class Protofier {
  /// The dispatcher, for invoking deprotofied [Value_HostFunction]s.
  final CompilationDispatcher _dispatcher;

  /// The IDs of first-class functions.
  final OpaqueRegistry<SassFunction> _functions;

  /// The IDs of first-class mixins.
  final OpaqueRegistry<SassMixin> _mixins;

  /// Any argument lists transitively contained in [value].
  ///
  /// The IDs of the [Value_ArgumentList] protobufs are always one greater than
  /// the index of the corresponding list in this array (since 0 is reserved for
  /// argument lists created by the host).
  final _argumentLists = <SassArgumentList>[];

  /// Creates a [Protofier] that's valid within the scope of a single custom
  /// function call.
  ///
  /// The [functions] tracks the IDs of first-class functions so that the host
  /// can pass them back to the compiler.
  ///
  /// Similarly, the [mixins] tracks the IDs of first-class mixins so that the
  /// host can pass them back to the compiler.
  Protofier(this._dispatcher, this._functions, this._mixins);

  /// Converts [value] to its protocol buffer representation.
  proto.Value protofy(Value value) {
    var result = proto.Value();
    switch (value) {
      case SassString():
        result.string = Value_String()
          ..text = value.text
          ..quoted = value.hasQuotes;
      case SassNumber():
        result.number = _protofyNumber(value);
      case SassColor():
        result.color = Value_Color(
          space: value.space.name,
          channel1: value.channel0OrNull,
          channel2: value.channel1OrNull,
          channel3: value.channel2OrNull,
          alpha: value.alphaOrNull,
        );
      case SassArgumentList():
        _argumentLists.add(value);
        result.argumentList = Value_ArgumentList()
          ..id = _argumentLists.length
          ..separator = _protofySeparator(value.separator)
          ..keywords.addAll({
            for (var (key, value) in value.keywordsWithoutMarking.pairs)
              key: protofy(value),
          })
          ..contents.addAll(value.asList.map(protofy));
      case SassList():
        result.list = Value_List()
          ..separator = _protofySeparator(value.separator)
          ..hasBrackets = value.hasBrackets
          ..contents.addAll(value.asList.map(protofy));
      case SassMap():
        result.map = Value_Map();
        for (var (key, value) in value.contents.pairs) {
          result.map.entries.add(
            Value_Map_Entry()
              ..key = protofy(key)
              ..value = protofy(value),
          );
        }
      case SassCalculation():
        result.calculation = _protofyCalculation(value);
      case SassFunction():
        result.compilerFunction = Value_CompilerFunction(
          id: _functions.getId(value),
        );
      case SassMixin():
        result.compilerMixin = Value_CompilerMixin(id: _mixins.getId(value));
      case sassTrue:
        result.singleton = SingletonValue.TRUE;
      case sassFalse:
        result.singleton = SingletonValue.FALSE;
      case sassNull:
        result.singleton = SingletonValue.NULL;
      case _:
        throw "Unknown Value $value";
    }
    return result;
  }

  /// Converts [number] to its protocol buffer representation.
  Value_Number _protofyNumber(SassNumber number) => Value_Number()
    ..value = number.value * 1.0
    ..numerators.addAll(number.numeratorUnits)
    ..denominators.addAll(number.denominatorUnits);

  /// Converts [separator] to its protocol buffer representation.
  proto.ListSeparator _protofySeparator(ListSeparator separator) =>
      switch (separator) {
        ListSeparator.comma => proto.ListSeparator.COMMA,
        ListSeparator.space => proto.ListSeparator.SPACE,
        ListSeparator.slash => proto.ListSeparator.SLASH,
        ListSeparator.undecided => proto.ListSeparator.UNDECIDED,
      };

  /// Converts [calculation] to its protocol buffer representation.
  Value_Calculation _protofyCalculation(SassCalculation calculation) =>
      Value_Calculation()
        ..name = calculation.name
        ..arguments.addAll(calculation.arguments.map(_protofyCalculationValue));

  /// Converts a calculation value that appears within a `SassCalculation` to
  /// its protocol buffer representation.
  Value_Calculation_CalculationValue _protofyCalculationValue(Object value) {
    var result = Value_Calculation_CalculationValue();
    switch (value) {
      case SassNumber():
        result.number = _protofyNumber(value);
      case SassCalculation():
        result.calculation = _protofyCalculation(value);
      case SassString():
        result.string = value.text;
      case CalculationOperation():
        result.operation = Value_Calculation_CalculationOperation()
          ..operator = _protofyCalculationOperator(value.operator)
          ..left = _protofyCalculationValue(value.left)
          ..right = _protofyCalculationValue(value.right);
      case _:
        throw "Unknown calculation value $value";
    }
    return result;
  }

  /// Converts [operator] to its protocol buffer representation.
  proto.CalculationOperator _protofyCalculationOperator(
    CalculationOperator operator,
  ) =>
      switch (operator) {
        CalculationOperator.plus => proto.CalculationOperator.PLUS,
        CalculationOperator.minus => proto.CalculationOperator.MINUS,
        CalculationOperator.times => proto.CalculationOperator.TIMES,
        CalculationOperator.dividedBy => proto.CalculationOperator.DIVIDE,
      };

  /// Converts [response]'s return value to its Sass representation.
  Value deprotofyResponse(InboundMessage_FunctionCallResponse response) {
    for (var id in response.accessedArgumentLists) {
      // Mark the `keywords` field as accessed.
      _argumentListForId(id).keywords;
    }

    return _deprotofy(response.success);
  }

  /// Converts [value] to its Sass representation.
  Value _deprotofy(proto.Value value) {
    try {
      switch (value.whichValue()) {
        case Value_Value.string:
          return value.string.text.isEmpty
              ? SassString.empty(quotes: value.string.quoted)
              : SassString(value.string.text, quotes: value.string.quoted);

        case Value_Value.number:
          return _deprotofyNumber(value.number);

        case Value_Value.color:
          var space = ColorSpace.fromName(value.color.space);
          var channel1 =
              value.color.hasChannel1() ? value.color.channel1 : null;
          var channel2 =
              value.color.hasChannel2() ? value.color.channel2 : null;
          var channel3 =
              value.color.hasChannel3() ? value.color.channel3 : null;
          var alpha = value.color.hasAlpha() ? value.color.alpha : null;
          switch (space) {
            case ColorSpace.rgb:
              return SassColor.rgb(channel1, channel2, channel3, alpha);

            case ColorSpace.hsl:
              return SassColor.hsl(channel1, channel2, channel3, alpha);

            case ColorSpace.hwb:
              return SassColor.hwb(channel1, channel2, channel3, alpha);

            case ColorSpace.lab:
              return SassColor.lab(channel1, channel2, channel3, alpha);
            case ColorSpace.oklab:
              return SassColor.oklab(channel1, channel2, channel3, alpha);

            case ColorSpace.lch:
              return SassColor.lch(channel1, channel2, channel3, alpha);
            case ColorSpace.oklch:
              return SassColor.oklch(channel1, channel2, channel3, alpha);

            case ColorSpace.srgb:
              return SassColor.srgb(channel1, channel2, channel3, alpha);
            case ColorSpace.srgbLinear:
              return SassColor.srgbLinear(channel1, channel2, channel3, alpha);
            case ColorSpace.displayP3:
              return SassColor.displayP3(channel1, channel2, channel3, alpha);
            case ColorSpace.displayP3Linear:
              return SassColor.displayP3Linear(
                  channel1, channel2, channel3, alpha);
            case ColorSpace.a98Rgb:
              return SassColor.a98Rgb(channel1, channel2, channel3, alpha);
            case ColorSpace.prophotoRgb:
              return SassColor.prophotoRgb(channel1, channel2, channel3, alpha);
            case ColorSpace.rec2020:
              return SassColor.rec2020(channel1, channel2, channel3, alpha);

            case ColorSpace.xyzD50:
              return SassColor.xyzD50(channel1, channel2, channel3, alpha);
            case ColorSpace.xyzD65:
              return SassColor.xyzD65(channel1, channel2, channel3, alpha);

            default:
              throw "Unreachable";
          }

        case Value_Value.argumentList:
          if (value.argumentList.id != 0) {
            return _argumentListForId(value.argumentList.id);
          }

          var separator = _deprotofySeparator(value.argumentList.separator);
          var length = value.argumentList.contents.length;
          if (separator == ListSeparator.undecided && length > 1) {
            throw paramsError(
              "List $value can't have an undecided separator because it has "
              "$length elements",
            );
          }

          return SassArgumentList(
              value.argumentList.contents.map(_deprotofy),
              {
                for (var (name, value) in value.argumentList.keywords.pairs)
                  name: _deprotofy(value),
              },
              separator);

        case Value_Value.list:
          var separator = _deprotofySeparator(value.list.separator);
          if (value.list.contents.isEmpty) {
            return SassList.empty(
              separator: separator,
              brackets: value.list.hasBrackets,
            );
          }

          var length = value.list.contents.length;
          if (separator == ListSeparator.undecided && length > 1) {
            throw paramsError(
              "List $value can't have an undecided separator because it has "
              "$length elements",
            );
          }

          return SassList(
            value.list.contents.map(_deprotofy),
            separator,
            brackets: value.list.hasBrackets,
          );

        case Value_Value.map:
          return value.map.entries.isEmpty
              ? const SassMap.empty()
              : SassMap({
                  for (var Value_Map_Entry(:key, :value) in value.map.entries)
                    _deprotofy(key): _deprotofy(value),
                });

        case Value_Value.compilerFunction:
          var id = value.compilerFunction.id;
          if (_functions[id] case var function?) return function;
          throw paramsError(
            "CompilerFunction.id $id doesn't match any known functions",
          );

        case Value_Value.hostFunction:
          return SassFunction(
            hostCallable(
              _dispatcher,
              _functions,
              _mixins,
              value.hostFunction.signature,
              id: value.hostFunction.id,
            ),
          );

        case Value_Value.compilerMixin:
          var id = value.compilerMixin.id;
          if (_mixins[id] case var mixin?) return mixin;
          throw paramsError(
            "CompilerMixin.id $id doesn't match any known mixins",
          );

        case Value_Value.calculation:
          return _deprotofyCalculation(value.calculation);

        case Value_Value.singleton:
          return switch (value.singleton) {
            SingletonValue.TRUE => sassTrue,
            SingletonValue.FALSE => sassFalse,
            SingletonValue.NULL => sassNull,
            _ => throw "Unknown Value.singleton ${value.singleton}",
          };

        case Value_Value.notSet:
          throw mandatoryError("Value.value");
      }
    } on RangeError catch (error) {
      var name = error.name;
      if (name == null || error.start == null || error.end == null) {
        throw paramsError(error.toString());
      }

      if (value.whichValue() == Value_Value.color) {
        name = 'Color.$name';
      }

      throw paramsError(
        '$name must be between ${error.start} and ${error.end}, was '
        '${error.invalidValue}',
      );
    }
  }

  /// Converts [number] to its Sass representation.
  SassNumber _deprotofyNumber(Value_Number number) => SassNumber.withUnits(
        number.value,
        numeratorUnits: number.numerators,
        denominatorUnits: number.denominators,
      );

  /// Returns the argument list in [_argumentLists] that corresponds to [id].
  SassArgumentList _argumentListForId(int id) {
    if (id < 1) {
      throw paramsError(
        "Value.ArgumentList.id $id can't be marked as accessed",
      );
    } else if (id > _argumentLists.length) {
      throw paramsError(
        "Value.ArgumentList.id $id doesn't match any known argument "
        "lists",
      );
    } else {
      return _argumentLists[id - 1];
    }
  }

  /// Converts [separator] to its Sass representation.
  ListSeparator _deprotofySeparator(proto.ListSeparator separator) =>
      switch (separator) {
        proto.ListSeparator.COMMA => ListSeparator.comma,
        proto.ListSeparator.SPACE => ListSeparator.space,
        proto.ListSeparator.SLASH => ListSeparator.slash,
        proto.ListSeparator.UNDECIDED => ListSeparator.undecided,
        _ => throw "Unknown ListSeparator $separator",
      };

  /// Converts [calculation] to its Sass representation.
  Value _deprotofyCalculation(Value_Calculation calculation) =>
      switch (calculation) {
        Value_Calculation(name: "calc", arguments: [var arg]) =>
          SassCalculation.calc(_deprotofyCalculationValue(arg)),
        Value_Calculation(name: "calc") => throw paramsError(
            "Value.Calculation.arguments must have exactly one argument for "
            "calc().",
          ),
        Value_Calculation(
          name: "clamp",
          arguments: [var arg1, ...var rest] && List(length: < 4),
        ) =>
          SassCalculation.clamp(
            _deprotofyCalculationValue(arg1),
            rest.elementAtOrNull(0).andThen(_deprotofyCalculationValue),
            rest.elementAtOrNull(1).andThen(_deprotofyCalculationValue),
          ),
        Value_Calculation(name: "clamp") => throw paramsError(
            "Value.Calculation.arguments must have 1 to 3 arguments for "
            "clamp().",
          ),
        Value_Calculation(name: "min" || "max", arguments: []) =>
          throw paramsError(
            "Value.Calculation.arguments must have at least 1 argument for "
            "${calculation.name}().",
          ),
        Value_Calculation(name: "min", :var arguments) => SassCalculation.min(
            arguments.map(_deprotofyCalculationValue),
          ),
        Value_Calculation(name: "max", :var arguments) => SassCalculation.max(
            arguments.map(_deprotofyCalculationValue),
          ),
        _ => throw paramsError(
            'Value.Calculation.name "${calculation.name}" is not a recognized '
            'calculation type.',
          ),
      };

  /// Converts [value] to its Sass representation.
  Object _deprotofyCalculationValue(Value_Calculation_CalculationValue value) =>
      switch (value.whichValue()) {
        Value_Calculation_CalculationValue_Value.number => _deprotofyNumber(
            value.number,
          ),
        Value_Calculation_CalculationValue_Value.calculation =>
          _deprotofyCalculation(value.calculation),
        Value_Calculation_CalculationValue_Value.string => SassString(
            value.string,
            quotes: false,
          ),
        Value_Calculation_CalculationValue_Value.operation =>
          SassCalculation.operate(
            _deprotofyCalculationOperator(value.operation.operator),
            _deprotofyCalculationValue(value.operation.left),
            _deprotofyCalculationValue(value.operation.right),
          ),
        Value_Calculation_CalculationValue_Value.interpolation => SassString(
            '(${value.interpolation})',
            quotes: false,
          ),
        Value_Calculation_CalculationValue_Value.notSet =>
          throw mandatoryError("Value.Calculation.value"),
      };

  /// Converts [operator] to its Sass representation.
  CalculationOperator _deprotofyCalculationOperator(
    proto.CalculationOperator operator,
  ) =>
      switch (operator) {
        proto.CalculationOperator.PLUS => CalculationOperator.plus,
        proto.CalculationOperator.MINUS => CalculationOperator.minus,
        proto.CalculationOperator.TIMES => CalculationOperator.times,
        proto.CalculationOperator.DIVIDE => CalculationOperator.dividedBy,
        _ => throw "Unknown CalculationOperator $operator",
      };
}
