// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/icc_profile.h"

#include <array>
#include <list>
#include <set>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/containers/lru_cache.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/synchronization/lock.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/encode/SkICC.h"
#include "third_party/skia/include/private/chromium/SkCodecsICCProfileChromium.h"
#include "third_party/skia/modules/skcms/skcms.h"
#include "ui/gfx/skia_color_space_util.h"

namespace gfx {

namespace {

static const size_t kMaxCachedICCProfiles = 16;

// An LRU cache mapping data to ICCProfile objects, to avoid re-parsing
// profiles every time they are read.
using DataToProfileCacheBase = base::LRUCache<std::vector<char>, ICCProfile>;
class DataToProfileCache : public DataToProfileCacheBase {
 public:
  DataToProfileCache() : DataToProfileCacheBase(kMaxCachedICCProfiles) {}
};

DataToProfileCache& GetDataToProfileCache() {
  static base::NoDestructor<DataToProfileCache> cache;
  return *cache;
}

// Lock that must be held to access |GetDataToProfileCache()|.
base::Lock& GetIccProfileLock() {
  static base::NoDestructor<base::Lock> lock;
  return *lock;
}

}  // namespace

void ICCProfile::Internals::Initialize() {
  // Start out with no parametric data.
  if (data_.empty())
    return;

  // Parse the profile.
  auto parsed = SkCodecs::ICCProfileChromium::Make(
      SkData::MakeWithoutCopy(data_.data(), data_.size()));
  if (!parsed) {
    DLOG(ERROR) << "Failed to parse ICC profile.";
    return;
  }
  skcms_ICCProfile profile = parsed->GetProfile();

  // We have seen many users with profiles that don't have a D50 white point.
  // Windows appears to detect these profiles, and not use them for OS drawing.
  // It still returns them when we query the system for the installed profile.
  // For consistency (and to match old behavior) we reject these profiles on
  // all platforms.
  // https://crbug.com/847024
  const skcms_Matrix3x3& m(profile.toXYZD50);
  float wX = m.vals[0][0] + m.vals[0][1] + m.vals[0][2];
  float wY = m.vals[1][0] + m.vals[1][1] + m.vals[1][2];
  float wZ = m.vals[2][0] + m.vals[2][1] + m.vals[2][2];
  static const std::array<float, 3> kD50_WhitePoint = {
      0.96420f,
      1.00000f,
      0.82491f,
  };
  if (fabsf(wX - kD50_WhitePoint[0]) > 0.04f ||
      fabsf(wY - kD50_WhitePoint[1]) > 0.04f ||
      fabsf(wZ - kD50_WhitePoint[2]) > 0.04f) {
    return;
  }

  // At this point, the profile is considered valid. We still need to determine
  // if it's representable with a parametric transfer function.
  is_valid_ = true;

  // Extract the primary matrix, and assume that transfer function is sRGB until
  // we get something more precise.
  to_XYZD50_ = profile.toXYZD50;
  transfer_fn_ = SkNamedTransferFn::kSRGB;

  // Coerce it into a rasterization destination (if possible). If the profile
  // can't be approximated accurately, then use an sRGB transfer function and
  // return failure. We will continue to use the gamut from this profile.
  if (!skcms_MakeUsableAsDestinationWithSingleCurve(&profile)) {
    DLOG(ERROR) << "Parsed ICC profile but can't make usable as destination, "
                   "using sRGB gamma";
    return;
  }

  // If SkColorSpace will treat the gamma as that of sRGB, then use the named
  // constants.
  sk_sp<SkColorSpace> sk_color_space = SkColorSpace::Make(profile);
  if (!sk_color_space) {
    DLOG(ERROR) << "Parsed ICC profile but cannot create SkColorSpace from it, "
                   "using sRGB gamma.";
    return;
  }

  // We were able to get a parametric representation of the transfer function.
  is_parametric_ = true;

  if (sk_color_space->gammaCloseToSRGB())
    return;

  // We assume that if we accurately approximated the profile, then the
  // single-curve version (which may have higher error) is also okay. If we
  // want to maintain the distinction between accurate and inaccurate profiles,
  // we could check to see if the single-curve version is/ approximately equal
  // to the original (or to the multi-channel approximation).
  transfer_fn_ = profile.trc[0].parametric;
}

ICCProfile::ICCProfile() = default;
ICCProfile::ICCProfile(ICCProfile&& other) = default;
ICCProfile::ICCProfile(const ICCProfile& other) = default;
ICCProfile& ICCProfile::operator=(ICCProfile&& other) = default;
ICCProfile& ICCProfile::operator=(const ICCProfile& other) = default;
ICCProfile::~ICCProfile() = default;

bool ICCProfile::operator==(const ICCProfile& other) const {
  if (!internals_ && !other.internals_)
    return true;
  if (internals_ && other.internals_) {
    return internals_->data_ == other.internals_->data_;
  }
  return false;
}

bool ICCProfile::IsValid() const {
  return internals_ ? internals_->is_valid_ : false;
}

std::vector<char> ICCProfile::GetData() const {
  return internals_ ? internals_->data_ : std::vector<char>();
}

// static
ICCProfile ICCProfile::FromData(const void* data_as_void, size_t size) {
  const char* data_as_byte = reinterpret_cast<const char*>(data_as_void);
  std::vector<char> data(data_as_byte, UNSAFE_TODO(data_as_byte + size));

  base::AutoLock lock(GetIccProfileLock());

  // See if there is already an entry with the same data. If so, return that
  // entry. If not, parse the data.
  ICCProfile icc_profile;
  auto& cache = GetDataToProfileCache();
  auto found_by_data = cache.Get(data);
  if (found_by_data != cache.end()) {
    icc_profile = found_by_data->second;
  } else {
    icc_profile.internals_ = base::MakeRefCounted<Internals>(std::move(data));
  }

  // Insert the profile into all caches.
  cache.Put(icc_profile.internals_->data_, icc_profile);

  return icc_profile;
}

ColorSpace ICCProfile::GetColorSpace() const {
  if (!internals_ || !internals_->is_valid_)
    return ColorSpace();

  return ColorSpace(ColorSpace::PrimaryID::CUSTOM,
                    ColorSpace::TransferID::CUSTOM, ColorSpace::MatrixID::RGB,
                    ColorSpace::RangeID::FULL, &internals_->to_XYZD50_,
                    &internals_->transfer_fn_);
}

ColorSpace ICCProfile::GetPrimariesOnlyColorSpace() const {
  if (!internals_ || !internals_->is_valid_)
    return ColorSpace();

  return ColorSpace(ColorSpace::PrimaryID::CUSTOM, ColorSpace::TransferID::SRGB,
                    ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL,
                    &internals_->to_XYZD50_, nullptr);
}

bool ICCProfile::IsColorSpaceAccurate() const {
  if (!internals_)
    return false;

  if (!internals_->is_valid_)
    return false;

  return internals_->is_parametric_;
}

// static
ICCProfile ICCProfile::FromColorSpace(const ColorSpace& color_space) {
  if (!color_space.IsValid()) {
    return ICCProfile();
  }
  if (color_space.GetMatrixID() != ColorSpace::MatrixID::RGB) {
    DLOG(ERROR) << "Not creating non-RGB ICCProfile";
    return ICCProfile();
  }
  if (color_space.GetRangeID() != ColorSpace::RangeID::FULL) {
    DLOG(ERROR) << "Not creating non-full-range ICCProfile";
    return ICCProfile();
  }
  skcms_Matrix3x3 to_XYZD50_matrix;
  color_space.GetPrimaryMatrix(&to_XYZD50_matrix);
  skcms_TransferFunction fn;
  if (!color_space.GetTransferFunction(&fn)) {
    DLOG(ERROR) << "Failed to get ColorSpace transfer function for ICCProfile.";
    return ICCProfile();
  }
  sk_sp<SkData> data = SkWriteICCProfile(fn, to_XYZD50_matrix);
  if (!data) {
    DLOG(ERROR) << "Failed to create SkICC.";
    return ICCProfile();
  }
  return FromData(data->data(), data->size());
}

ICCProfile::Internals::Internals(std::vector<char> data)
    : data_(std::move(data)) {
  // Parse the ICC profile
  Initialize();
}

ICCProfile::Internals::~Internals() {}

}  // namespace gfx
