/*
 * Copyright 2021 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 utils.MathUtils;

/**
 * In traditional color spaces, a color can be identified solely by the observer's measurement of
 * the color. Color appearance models such as CAM16 also use information about the environment where
 * the color was observed, known as the viewing conditions.
 *
 * <p>For example, white under the traditional assumption of a midday sun white point is accurately
 * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
 *
 * <p>This class caches intermediate values of the CAM16 conversion process that depend only on
 * viewing conditions, enabling speed ups.
 */
public final class ViewingConditions {
  /** sRGB-like viewing conditions. */
  public static final ViewingConditions DEFAULT =
      ViewingConditions.defaultWithBackgroundLstar(50.0);

  private final double aw;
  private final double nbb;
  private final double ncb;
  private final double c;
  private final double nc;
  private final double n;
  private final double[] rgbD;
  private final double fl;
  private final double flRoot;
  private final double z;

  public double getAw() {
    return aw;
  }

  public double getN() {
    return n;
  }

  public double getNbb() {
    return nbb;
  }

  double getNcb() {
    return ncb;
  }

  double getC() {
    return c;
  }

  double getNc() {
    return nc;
  }

  public double[] getRgbD() {
    return rgbD;
  }

  double getFl() {
    return fl;
  }

  public double getFlRoot() {
    return flRoot;
  }

  double getZ() {
    return z;
  }

  /**
   * Create ViewingConditions from a simple, physically relevant, set of parameters.
   *
   * @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
   *     afternoon
   * @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
   *     the room where the color is viewed. Can be calculated from lux by multiplying lux by
   *     0.0586. default = 11.72, or 200 lux.
   * @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
   *     L*a*b*. default = 50.0
   * @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
   *     like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
   *     night. 2.0 means there is no difference between the lighting on the color and around it.
   *     default = 2.0
   * @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
   *     such as knowing an apple is still red in green light. default = false, the eye does not
   *     perform this process on self-luminous objects like displays.
   */
  public static ViewingConditions make(
      double[] whitePoint,
      double adaptingLuminance,
      double backgroundLstar,
      double surround,
      boolean discountingIlluminant) {
    // A background of pure black is non-physical and leads to infinities that represent the idea
    // that any color viewed in pure black can't be seen.
    backgroundLstar = Math.max(0.1, backgroundLstar);
    // Transform white point XYZ to 'cone'/'rgb' responses
    double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
    double[] xyz = whitePoint;
    double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
    double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
    double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
    double f = 0.8 + (surround / 10.0);
    double c =
        (f >= 0.9)
            ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
            : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
    double d =
        discountingIlluminant
            ? 1.0
            : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
    d = MathUtils.clampDouble(0.0, 1.0, d);
    double nc = f;
    double[] rgbD =
        new double[] {
          d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d
        };
    double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
    double k4 = k * k * k * k;
    double k4F = 1.0 - k4;
    double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance));
    double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
    double z = 1.48 + Math.sqrt(n);
    double nbb = 0.725 / Math.pow(n, 0.2);
    double ncb = nbb;
    double[] rgbAFactors =
        new double[] {
          Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
          Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
          Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
        };

    double[] rgbA =
        new double[] {
          (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
          (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
          (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
        };

    double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
    return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
  }

  /**
   * Create sRGB-like viewing conditions with a custom background lstar.
   *
   * <p>Default viewing conditions have a lstar of 50, midgray.
   */
  public static ViewingConditions defaultWithBackgroundLstar(double lstar) {
    return ViewingConditions.make(
        ColorUtils.whitePointD65(),
        (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
        lstar,
        2.0,
        false);
  }

  /**
   * Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
   * for technical color science terminology, this class would not benefit from documenting them
   * individually. A brief overview is available in the CAM16 specification, and a complete overview
   * requires a color science textbook, such as Fairchild's Color Appearance Models.
   */
  private ViewingConditions(
      double n,
      double aw,
      double nbb,
      double ncb,
      double c,
      double nc,
      double[] rgbD,
      double fl,
      double flRoot,
      double z) {
    this.n = n;
    this.aw = aw;
    this.nbb = nbb;
    this.ncb = ncb;
    this.c = c;
    this.nc = nc;
    this.rgbD = rgbD;
    this.fl = fl;
    this.flRoot = flRoot;
    this.z = z;
  }
}
