// Copyright 2022 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 'dart:typed_data';

import 'package:meta/meta.dart';

import '../../exception.dart';
import '../color.dart';
import 'space/a98_rgb.dart';
import 'space/display_p3.dart';
import 'space/display_p3_linear.dart';
import 'space/hsl.dart';
import 'space/hwb.dart';
import 'space/lab.dart';
import 'space/lch.dart';
import 'space/lms.dart';
import 'space/oklab.dart';
import 'space/oklch.dart';
import 'space/prophoto_rgb.dart';
import 'space/rec2020.dart';
import 'space/rgb.dart';
import 'space/srgb.dart';
import 'space/srgb_linear.dart';
import 'space/xyz_d50.dart';
import 'space/xyz_d65.dart';

/// A color space whose channel names and semantics Sass knows.
///
/// {@category Value}
@sealed
abstract base class ColorSpace {
  /// The legacy RGB color space.
  static const ColorSpace rgb = RgbColorSpace();

  /// The legacy HSL color space.
  static const ColorSpace hsl = HslColorSpace();

  /// The legacy HWB color space.
  static const ColorSpace hwb = HwbColorSpace();

  /// The sRGB color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-sRGB
  static const ColorSpace srgb = SrgbColorSpace();

  /// The linear-light sRGB color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear
  static const ColorSpace srgbLinear = SrgbLinearColorSpace();

  /// The display-p3 color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-display-p3
  static const ColorSpace displayP3 = DisplayP3ColorSpace();

  /// The display-p3-linear color space.
  ///
  /// https://drafts.csswg.org/css-color/#predefined-display-p3-linear
  static const ColorSpace displayP3Linear = DisplayP3LinearColorSpace();

  /// The a98-rgb color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-a98-rgb
  static const ColorSpace a98Rgb = A98RgbColorSpace();

  /// The prophoto-rgb color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb
  static const ColorSpace prophotoRgb = ProphotoRgbColorSpace();

  /// The rec2020 color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-rec2020
  static const ColorSpace rec2020 = Rec2020ColorSpace();

  /// The xyz-d65 color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-xyz
  static const ColorSpace xyzD65 = XyzD65ColorSpace();

  /// The xyz-d50 color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#predefined-xyz
  static const ColorSpace xyzD50 = XyzD50ColorSpace();

  /// The CIE Lab color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#cie-lab
  static const ColorSpace lab = LabColorSpace();

  /// The CIE LCH color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#cie-lab
  static const ColorSpace lch = LchColorSpace();

  /// The internal LMS color space.
  ///
  /// This only used as an intermediate space for conversions to and from OKLab
  /// and OKLCH. It's never used in a real color value and isn't returned by
  /// [fromName].
  ///
  /// @nodoc
  @internal
  static const ColorSpace lms = LmsColorSpace();

  /// The Oklab color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#ok-lab
  static const ColorSpace oklab = OklabColorSpace();

  /// The Oklch color space.
  ///
  /// https://www.w3.org/TR/css-color-4/#ok-lab
  static const ColorSpace oklch = OklchColorSpace();

  /// The CSS name of the color space.
  final String name;

  /// See [SassApiColorSpace.channels].
  final List<ColorChannel> _channels;

  /// See [SassApiColorSpace.isBounded].
  ///
  /// @nodoc
  @internal
  bool get isBoundedInternal;

  /// See [SassApiColorSpace.isLegacy].
  ///
  /// @nodoc
  @internal
  bool get isLegacyInternal => false;

  /// See [SassApiColorSpace.isPolar].
  ///
  /// @nodoc
  @internal
  bool get isPolarInternal => false;

  /// @nodoc
  @internal
  const ColorSpace(this.name, this._channels);

  /// Given a color space name, returns the known color space with that name or
  /// throws a [SassScriptException] if there is none.
  ///
  /// If this came from a function argument, [argumentName] is the argument name
  /// (without the `$`). This is used for error reporting.
  static ColorSpace fromName(
    String name, [
    String? argumentName,
  ]) =>
      switch (name.toLowerCase()) {
        'rgb' => rgb,
        'hwb' => hwb,
        'hsl' => hsl,
        'srgb' => srgb,
        'srgb-linear' => srgbLinear,
        'display-p3' => displayP3,
        'display-p3-linear' => displayP3Linear,
        'a98-rgb' => a98Rgb,
        'prophoto-rgb' => prophotoRgb,
        'rec2020' => rec2020,
        'xyz' || 'xyz-d65' => xyzD65,
        'xyz-d50' => xyzD50,
        'lab' => lab,
        'lch' => lch,
        'oklab' => oklab,
        'oklch' => oklch,
        _ => throw SassScriptException(
            'Unknown color space "$name".', argumentName),
      };

  /// Converts a color with the given channels from this color space to [dest].
  ///
  /// By default, this uses this color space's [toLinear] and
  /// [transformationMatrix] as well as [dest]'s [fromLinear], and relies on
  /// individual color space conversions to do more than purely linear
  /// conversions.
  ///
  /// @nodoc
  @internal
  SassColor convert(
    ColorSpace dest,
    double? channel0,
    double? channel1,
    double? channel2,
    double? alpha,
  ) =>
      convertLinear(dest, channel0, channel1, channel2, alpha);

  /// The default implementation of [convert], which always starts with a linear
  /// transformation from RGB or XYZ channels to a linear destination space, and
  /// may then further convert to a polar space.
  ///
  /// @nodoc
  @internal
  @protected
  @nonVirtual
  SassColor convertLinear(
    ColorSpace dest,
    double? red,
    double? green,
    double? blue,
    double? alpha, {
    bool missingLightness = false,
    bool missingChroma = false,
    bool missingHue = false,
    bool missingA = false,
    bool missingB = false,
  }) {
    var linearDest = switch (dest) {
      ColorSpace.hsl || ColorSpace.hwb => const SrgbColorSpace(),
      ColorSpace.lab || ColorSpace.lch => const XyzD50ColorSpace(),
      ColorSpace.oklab || ColorSpace.oklch => const LmsColorSpace(),
      _ => dest,
    };

    double? transformedRed;
    double? transformedGreen;
    double? transformedBlue;
    if (linearDest == this) {
      transformedRed = red;
      transformedGreen = green;
      transformedBlue = blue;
    } else {
      var linearRed = toLinear(red ?? 0);
      var linearGreen = toLinear(green ?? 0);
      var linearBlue = toLinear(blue ?? 0);
      var matrix = transformationMatrix(linearDest);

      // (matrix * [linearRed, linearGreen, linearBlue]).map(linearDest.fromLinear)
      transformedRed = linearDest.fromLinear(
        matrix[0] * linearRed +
            matrix[1] * linearGreen +
            matrix[2] * linearBlue,
      );
      transformedGreen = linearDest.fromLinear(
        matrix[3] * linearRed +
            matrix[4] * linearGreen +
            matrix[5] * linearBlue,
      );
      transformedBlue = linearDest.fromLinear(
        matrix[6] * linearRed +
            matrix[7] * linearGreen +
            matrix[8] * linearBlue,
      );
    }

    return switch (dest) {
      ColorSpace.hsl || ColorSpace.hwb => const SrgbColorSpace().convert(
          dest,
          transformedRed,
          transformedGreen,
          transformedBlue,
          alpha,
          missingLightness: missingLightness,
          missingChroma: missingChroma,
          missingHue: missingHue,
        ),
      ColorSpace.lab || ColorSpace.lch => const XyzD50ColorSpace().convert(
          dest,
          transformedRed,
          transformedGreen,
          transformedBlue,
          alpha,
          missingLightness: missingLightness,
          missingChroma: missingChroma,
          missingHue: missingHue,
          missingA: missingA,
          missingB: missingB,
        ),
      ColorSpace.oklab || ColorSpace.oklch => const LmsColorSpace().convert(
          dest,
          transformedRed,
          transformedGreen,
          transformedBlue,
          alpha,
          missingLightness: missingLightness,
          missingChroma: missingChroma,
          missingHue: missingHue,
          missingA: missingA,
          missingB: missingB,
        ),
      _ => SassColor.forSpaceInternal(
          dest,
          red == null ? null : transformedRed,
          green == null ? null : transformedGreen,
          blue == null ? null : transformedBlue,
          alpha,
        ),
    };
  }

  /// Converts a channel in this color space into an element of a vector that
  /// can be linearly transformed into other color spaces.
  ///
  /// The precise semantics of this vector may vary from color space to color
  /// space. The only requirement is that, for any space `dest` for which
  /// `transformationMatrix(dest)` returns a value,
  /// `dest.fromLinear(toLinear(channels) * transformationMatrix(dest))`
  /// converts from this space to `dest`.
  ///
  /// If a color space explicitly supports all conversions in [convert], it need
  /// not override this at all.
  ///
  /// @nodoc
  @protected
  @internal
  double toLinear(double channel) => throw UnimplementedError(
        "[BUG] Color space $this doesn't support linear conversions.",
      );

  /// Converts an element of a 3-element vector that can be linearly transformed
  /// into other color spaces into a channel in this color space.
  ///
  /// The precise semantics of this vector may vary from color space to color
  /// space. The only requirement is that, for any space `dest` for which
  /// `transformationMatrix(dest)` returns a value,
  /// `dest.fromLinear(toLinear(channels) * transformationMatrix(dest))`
  /// converts from this space to `dest`.
  ///
  /// If a color space explicitly supports all conversions in [convert], it need
  /// not override this at all.
  ///
  /// @nodoc
  @protected
  @internal
  double fromLinear(double channel) => throw UnimplementedError(
        "[BUG] Color space $this doesn't support linear conversions.",
      );

  /// Returns the matrix for performing a linear transformation from this color
  /// space to [dest].
  ///
  /// Specifically, `dest.fromLinear(toLinear(channels) *
  /// transformationMatrix(dest))` must convert from this space to `dest`.
  ///
  /// This only needs to return values for color spaces that aren't explicitly
  /// supported in [convert]. If a color space explicitly supports all
  /// conversions in [convert], it need not override this at all.
  ///
  /// @nodoc
  @protected
  @internal
  Float64List transformationMatrix(ColorSpace dest) => throw UnimplementedError(
        '[BUG] Color space conversion from $this to $dest not implemented.',
      );

  String toString() => name;
}

/// ColorSpace methods that are only visible through the `sass_api` package.
extension SassApiColorSpace on ColorSpace {
  // This color space's channels.
  List<ColorChannel> get channels => _channels;

  /// Whether this color space has a bounded gamut.
  bool get isBounded => isBoundedInternal;

  /// Whether this is a legacy color space.
  bool get isLegacy => isLegacyInternal;

  /// Whether this color space uses a polar coordinate system.
  bool get isPolar => isPolarInternal;
}
