/*
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package hct

import utils.ColorUtils
import kotlin.math.roundToInt

/**
 * A color system built using CAM16 hue and chroma, and L* from L*a*b*.
 *
 * Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
 * ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
 * be calculated from Y.
 *
 * Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
 *
 * Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A difference
 * of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50 guarantees a
 * contrast ratio >= 4.5.
 */

/**
 * HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
 * measurement system that can also accurately render what colors will appear as in different
 * lighting environments.
 */
class Hct private constructor(argb: Int) {
  var hue = 0.0
    private set

  var chroma = 0.0
    private set

  var tone = 0.0
    private set

  private var argb = 0

  init {
    setInternalState(argb)
  }

  fun toInt(): Int {
    return argb
  }

  /**
   * Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
   * given hue and tone.
   *
   * @param newHue 0 <= newHue < 360; invalid values are corrected.
   */
  fun setHue(newHue: Double) {
    setInternalState(HctSolver.solveToInt(newHue, chroma, tone))
  }

  /**
   * Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
   * any given hue and tone.
   *
   * @param newChroma 0 <= newChroma < ?
   */
  fun setChroma(newChroma: Double) {
    setInternalState(HctSolver.solveToInt(hue, newChroma, tone))
  }

  /**
   * Set the tone of this color. Chroma may decrease because chroma has a different maximum for any
   * given hue and tone.
   *
   * @param newTone 0 <= newTone <= 100; invalid valids are corrected.
   */
  fun setTone(newTone: Double) {
    setInternalState(HctSolver.solveToInt(hue, chroma, newTone))
  }

  override fun toString(): String {
    return "HCT(${hue.roundToInt()}, ${chroma.roundToInt()}, ${tone.roundToInt()})"
  }

  /**
   * Translate a color into different ViewingConditions.
   *
   * Colors change appearance. They look different with lights on versus off, the same color, as in
   * hex code, on white looks different when on black. This is called color relativity, most
   * famously explicated by Josef Albers in Interaction of Color.
   *
   * In color science, color appearance models can account for this and calculate the appearance of
   * a color in different settings. HCT is based on CAM16, a color appearance model, and uses it to
   * make these calculations.
   *
   * See ViewingConditions.make for parameters affecting color appearance.
   */
  fun inViewingConditions(vc: ViewingConditions): Hct {
    // 1. Use CAM16 to find XYZ coordinates of color in specified VC.
    val cam16 = Cam16.fromInt(toInt())
    val viewedInVc = cam16.xyzInViewingConditions(vc, null)

    // 2. Create CAM16 of those XYZ coordinates in default VC.
    val recastInVc =
      Cam16.fromXyzInViewingConditions(
        viewedInVc[0],
        viewedInVc[1],
        viewedInVc[2],
        ViewingConditions.DEFAULT,
      )

    // 3. Create HCT from:
    // - CAM16 using default VC with XYZ coordinates in specified VC.
    // - L* converted from Y in XYZ coordinates in specified VC.
    return from(recastInVc.hue, recastInVc.chroma, ColorUtils.lstarFromY(viewedInVc[1]))
  }

  private fun setInternalState(argb: Int) {
    this.argb = argb
    val cam = Cam16.fromInt(argb)
    hue = cam.hue
    chroma = cam.chroma
    tone = ColorUtils.lstarFromArgb(argb)
  }

  companion object {
    /**
     * Create an HCT color from hue, chroma, and tone.
     *
     * @param hue 0 <= hue < 360; invalid values are corrected.
     * @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than
     *   the requested chroma. Chroma has a different maximum for any given hue and tone.
     * @param tone 0 <= tone <= 100; invalid values are corrected.
     * @return HCT representation of a color in default viewing conditions.
     */
    @JvmStatic
    fun from(hue: Double, chroma: Double, tone: Double): Hct {
      val argb = HctSolver.solveToInt(hue, chroma, tone)
      return Hct(argb)
    }

    /**
     * Create an HCT color from a color.
     *
     * @param argb ARGB representation of a color.
     * @return HCT representation of a color in default viewing conditions
     */
    @JvmStatic
    fun fromInt(argb: Int): Hct {
      return Hct(argb)
    }

    @JvmStatic
    fun isBlue(hue: Double): Boolean {
      return hue >= 250 && hue < 270
    }

    @JvmStatic
    fun isYellow(hue: Double): Boolean {
      return hue >= 105 && hue < 125
    }

    @JvmStatic
    fun isCyan(hue: Double): Boolean {
      return hue >= 170 && hue < 207
    }
  }
}
