// Copyright 2020 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 'package:meta/meta.dart';

import '../../util/number.dart';
import '../../utils.dart';
import '../../util/nullable.dart';
import '../../value.dart';
import '../number.dart';

/// Sets of units that are known to be compatible with one another in the
/// browser.
///
/// These units are likewise known to be *incompatible* with units in other
/// sets in this list.
const _knownCompatibilities = [
  {
    "em", "rem", "ex", "rex", "cap", "rcap", "ch", "rch", "ic", "ric", "lh", //
    "rlh", "vw", "lvw", "svw", "dvw", "vh", "lvh", "svh", "dvh", "vi", "lvi", //
    "svi", "dvi", "vb", "lvb", "svb", "dvb", "vmin", "lvmin", "svmin", //
    "dvmin", "vmax", "lvmax", "svmax", "dvmax", "cqw", "cqh", "cqi", "cqb", //
    "cqmin", "cqmax", "cm", "mm", "q", "in", "pt", "pc", "px",
  },
  {"deg", "grad", "rad", "turn"},
  {"s", "ms"},
  {"hz", "khz"},
  {"dpi", "dpcm", "dppx"},
];

/// A map from units to the other units they're known to be compatible with.
final _knownCompatibilitiesByUnit = {
  for (var set in _knownCompatibilities)
    for (var unit in set) unit: set,
};

/// A specialized subclass of [SassNumber] for numbers that have exactly one
/// numerator unit.
///
/// {@category Value}
@sealed
class SingleUnitSassNumber extends SassNumber {
  final String _unit;

  List<String> get numeratorUnits => List.unmodifiable([_unit]);

  List<String> get denominatorUnits => const [];

  bool get hasUnits => true;
  bool get hasComplexUnits => false;

  SingleUnitSassNumber(
    double value,
    this._unit, [
    (SassNumber, SassNumber)? asSlash,
  ]) : super.protected(value, asSlash);

  SassNumber withValue(num value) =>
      SingleUnitSassNumber(value.toDouble(), _unit);

  SassNumber withSlash(SassNumber numerator, SassNumber denominator) =>
      SingleUnitSassNumber(value, _unit, (numerator, denominator));

  bool hasUnit(String unit) => unit == _unit;

  bool hasCompatibleUnits(SassNumber other) =>
      other is SingleUnitSassNumber && compatibleWithUnit(other._unit);

  @internal
  bool hasPossiblyCompatibleUnits(SassNumber other) {
    if (other is! SingleUnitSassNumber) return false;

    var knownCompatibilities = _knownCompatibilitiesByUnit[_unit.toLowerCase()];
    if (knownCompatibilities == null) return true;

    var otherUnit = other._unit.toLowerCase();
    return knownCompatibilities.contains(otherUnit) ||
        !_knownCompatibilitiesByUnit.containsKey(otherUnit);
  }

  bool compatibleWithUnit(String unit) => conversionFactor(_unit, unit) != null;

  SassNumber coerceToMatch(
    SassNumber other, [
    String? name,
    String? otherName,
  ]) =>
      (other is SingleUnitSassNumber ? _coerceToUnit(other._unit) : null) ??
      // Call this to generate a consistent error message.
      super.coerceToMatch(other, name, otherName);

  double coerceValueToMatch(
    SassNumber other, [
    String? name,
    String? otherName,
  ]) =>
      (other is SingleUnitSassNumber
          ? _coerceValueToUnit(other._unit)
          : null) ??
      // Call this to generate a consistent error message.
      super.coerceValueToMatch(other, name, otherName);

  double convertValueToUnit(String unit, [String? name]) =>
      _coerceValueToUnit(unit) ??
      // Call this to generate a consistent error message.
      super.convertValueToUnit(unit, name);

  SassNumber convertToMatch(
    SassNumber other, [
    String? name,
    String? otherName,
  ]) =>
      (other is SingleUnitSassNumber ? _coerceToUnit(other._unit) : null) ??
      // Call this to generate a consistent error message.
      super.convertToMatch(other, name, otherName);

  double convertValueToMatch(
    SassNumber other, [
    String? name,
    String? otherName,
  ]) =>
      (other is SingleUnitSassNumber
          ? _coerceValueToUnit(other._unit)
          : null) ??
      // Call this to generate a consistent error message.
      super.convertValueToMatch(other, name, otherName);

  SassNumber coerce(
    List<String> newNumerators,
    List<String> newDenominators, [
    String? name,
  ]) =>
      (newNumerators.length == 1 && newDenominators.isEmpty
          ? _coerceToUnit(newNumerators[0])
          : null) ??
      // Call this to generate a consistent error message.
      super.coerce(newNumerators, newDenominators, name);

  double coerceValue(
    List<String> newNumerators,
    List<String> newDenominators, [
    String? name,
  ]) =>
      (newNumerators.length == 1 && newDenominators.isEmpty
          ? _coerceValueToUnit(newNumerators[0])
          : null) ??
      // Call this to generate a consistent error message.
      super.coerceValue(newNumerators, newDenominators, name);

  double coerceValueToUnit(String unit, [String? name]) =>
      _coerceValueToUnit(unit) ??
      // Call this to generate a consistent error message.
      super.coerceValueToUnit(unit, name);

  /// A shorthand for [coerce] with only one numerator unit, except that it
  /// returns `null` if coercion fails.
  SassNumber? _coerceToUnit(String unit) {
    if (_unit == unit) return this;
    return conversionFactor(
      unit,
      _unit,
    ).andThen((factor) => SingleUnitSassNumber(value * factor, unit));
  }

  /// Like [coerceValueToUnit], except that it returns `null` if coercion fails.
  double? _coerceValueToUnit(String unit) =>
      conversionFactor(unit, _unit).andThen((factor) => value * factor);

  SassNumber multiplyUnits(
    num value,
    List<String> otherNumerators,
    List<String> otherDenominators,
  ) {
    var newNumerators = otherNumerators;
    var mutableOtherDenominators = otherDenominators.toList();
    removeFirstWhere<String>(
      mutableOtherDenominators,
      (denominator) {
        var factor = conversionFactor(denominator, _unit);
        if (factor == null) return false;
        value *= factor;
        return true;
      },
      orElse: () {
        newNumerators = [_unit, ...newNumerators];
      },
    );

    return SassNumber.withUnits(
      value,
      numeratorUnits: newNumerators,
      denominatorUnits: mutableOtherDenominators,
    );
  }

  Value unaryMinus() => SingleUnitSassNumber(-value, _unit);

  bool operator ==(Object other) {
    if (other is SingleUnitSassNumber) {
      var factor = conversionFactor(other._unit, _unit);
      return factor != null && fuzzyEquals(value * factor, other.value);
    } else {
      return false;
    }
  }

  int get hashCode =>
      hashCache ??= fuzzyHashCode(value * canonicalMultiplierForUnit(_unit));
}
