// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#ifndef LIB_JXL_COLOR_ENCODING_INTERNAL_H_
#define LIB_JXL_COLOR_ENCODING_INTERNAL_H_

// Metadata for color space conversions.

#include <jxl/cms_interface.h>
#include <jxl/color_encoding.h>
#include <jxl/types.h>

#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>  // free
#include <ostream>
#include <string>
#include <utility>

#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/cms/color_encoding_cms.h"
#include "lib/jxl/cms/jxl_cms_internal.h"
#include "lib/jxl/field_encodings.h"

namespace jxl {

using IccBytes = ::jxl::cms::IccBytes;
using ColorSpace = ::jxl::cms::ColorSpace;
using WhitePoint = ::jxl::cms::WhitePoint;
using Primaries = ::jxl::cms::Primaries;
using TransferFunction = ::jxl::cms::TransferFunction;
using RenderingIntent = ::jxl::cms::RenderingIntent;
using CIExy = ::jxl::cms::CIExy;
using PrimariesCIExy = ::jxl::cms::PrimariesCIExy;

namespace cms {

static inline const char* EnumName(ColorSpace /*unused*/) {
  return "ColorSpace";
}
static inline constexpr uint64_t EnumBits(ColorSpace /*unused*/) {
  using CS = ColorSpace;
  return MakeBit(CS::kRGB) | MakeBit(CS::kGray) | MakeBit(CS::kXYB) |
         MakeBit(CS::kUnknown);
}

static inline const char* EnumName(WhitePoint /*unused*/) {
  return "WhitePoint";
}
static inline constexpr uint64_t EnumBits(WhitePoint /*unused*/) {
  return MakeBit(WhitePoint::kD65) | MakeBit(WhitePoint::kCustom) |
         MakeBit(WhitePoint::kE) | MakeBit(WhitePoint::kDCI);
}

static inline const char* EnumName(Primaries /*unused*/) { return "Primaries"; }
static inline constexpr uint64_t EnumBits(Primaries /*unused*/) {
  using Pr = Primaries;
  return MakeBit(Pr::kSRGB) | MakeBit(Pr::kCustom) | MakeBit(Pr::k2100) |
         MakeBit(Pr::kP3);
}

static inline const char* EnumName(TransferFunction /*unused*/) {
  return "TransferFunction";
}

static inline constexpr uint64_t EnumBits(TransferFunction /*unused*/) {
  using TF = TransferFunction;
  return MakeBit(TF::k709) | MakeBit(TF::kLinear) | MakeBit(TF::kSRGB) |
         MakeBit(TF::kPQ) | MakeBit(TF::kDCI) | MakeBit(TF::kHLG) |
         MakeBit(TF::kUnknown);
}

static inline const char* EnumName(RenderingIntent /*unused*/) {
  return "RenderingIntent";
}
static inline constexpr uint64_t EnumBits(RenderingIntent /*unused*/) {
  using RI = RenderingIntent;
  return MakeBit(RI::kPerceptual) | MakeBit(RI::kRelative) |
         MakeBit(RI::kSaturation) | MakeBit(RI::kAbsolute);
}

}  // namespace cms

struct ColorEncoding;

// Serializable form of CIExy.
struct Customxy : public Fields {
  Customxy();
  JXL_FIELDS_NAME(Customxy)

  Status VisitFields(Visitor* JXL_RESTRICT visitor) override;

 private:
  friend struct ColorEncoding;
  ::jxl::cms::Customxy storage_;
};

struct CustomTransferFunction : public Fields {
  CustomTransferFunction();
  JXL_FIELDS_NAME(CustomTransferFunction)

  // Sets fields and returns true if nonserialized_color_space has an implicit
  // transfer function, otherwise leaves fields unchanged and returns false.
  bool SetImplicit();

  Status VisitFields(Visitor* JXL_RESTRICT visitor) override;

  // Must be set before calling VisitFields!
  ColorSpace nonserialized_color_space = ColorSpace::kRGB;

 private:
  friend struct ColorEncoding;
  ::jxl::cms::CustomTransferFunction storage_;
};

// Compact encoding of data required to interpret and translate pixels to a
// known color space. Stored in Metadata. Thread-compatible.
struct ColorEncoding : public Fields {
  ColorEncoding();
  JXL_FIELDS_NAME(ColorEncoding)

  // Returns ready-to-use color encodings (initialized on-demand).
  static const ColorEncoding& SRGB(bool is_gray = false);
  static const ColorEncoding& LinearSRGB(bool is_gray = false);

  // Returns true if an ICC profile was successfully created from fields.
  // Must be called after modifying fields. Defined in color_management.cc.
  Status CreateICC() {
    storage_.icc.clear();
    const JxlColorEncoding external = ToExternal();
    if (!MaybeCreateProfile(external, &storage_.icc)) {
      storage_.icc.clear();
      return JXL_FAILURE("Failed to create ICC profile");
    }
    return true;
  }

  // Returns non-empty and valid ICC profile, unless:
  // - WantICC() == true and SetICC() was not yet called;
  // - after a failed call to SetSRGB(), SetICC(), or CreateICC().
  const IccBytes& ICC() const { return storage_.icc; }

  // Returns true if `icc` is assigned and decoded successfully. If so,
  // subsequent WantICC() will return true until DecideIfWantICC() changes it.
  // Returning false indicates data has been lost.
  Status SetICC(IccBytes&& icc, const JxlCmsInterface* cms) {
    JXL_ENSURE(cms != nullptr);
    JXL_ENSURE(!icc.empty());
    storage_.have_fields = true;
    want_icc_ = storage_.SetFieldsFromICC(std::move(icc), *cms);
    return want_icc_;
  }

  // Sets the raw ICC profile bytes, without parsing the ICC, and without
  // updating the direct fields such as white point, primaries and color
  // space. Functions to get and set fields, such as SetWhitePoint, cannot be
  // used anymore after this and functions such as IsSRGB return false no matter
  // what the contents of the icc profile.
  void SetICCRaw(IccBytes&& icc) {
    JXL_DASSERT(!icc.empty());
    storage_.icc = std::move(icc);
    storage_.have_fields = false;
    want_icc_ = true;
  }

  // Returns whether to send the ICC profile in the codestream.
  bool WantICC() const { return want_icc_; }

  // Return whether the direct fields are set, if false but ICC is set, only
  // raw ICC bytes are known.
  bool HaveFields() const { return storage_.have_fields; }

  // Causes WantICC() to return false if ICC() can be reconstructed from fields.
  void DecideIfWantICC(const JxlCmsInterface& cms);

  bool IsGray() const { return storage_.color_space == ColorSpace::kGray; }
  bool IsCMYK() const { return storage_.cmyk; }
  size_t Channels() const { return storage_.Channels(); }

  // Returns false if the field is invalid and unusable.
  bool HasPrimaries() const { return storage_.HasPrimaries(); }

  // Returns true after setting the field to a value defined by color_space,
  // otherwise false and leaves the field unchanged.
  bool ImplicitWhitePoint() {
    // TODO(eustas): inline
    if (storage_.color_space == ColorSpace::kXYB) {
      storage_.white_point = WhitePoint::kD65;
      return true;
    }
    return false;
  }

  // Returns whether the color space is known to be sRGB. If a raw unparsed ICC
  // profile is set without the fields being set, this returns false, even if
  // the content of the ICC profile would match sRGB.
  bool IsSRGB() const {
    if (!storage_.have_fields) return false;
    if (!IsGray() && storage_.color_space != ColorSpace::kRGB) return false;
    if (storage_.white_point != WhitePoint::kD65) return false;
    if (storage_.primaries != Primaries::kSRGB) return false;
    if (!storage_.tf.IsSRGB()) return false;
    return true;
  }

  // Returns whether the color space is known to be linear sRGB. If a raw
  // unparsed ICC profile is set without the fields being set, this returns
  // false, even if the content of the ICC profile would match linear sRGB.
  bool IsLinearSRGB() const {
    if (!storage_.have_fields) return false;
    if (!IsGray() && storage_.color_space != ColorSpace::kRGB) return false;
    if (storage_.white_point != WhitePoint::kD65) return false;
    if (storage_.primaries != Primaries::kSRGB) return false;
    if (!storage_.tf.IsLinear()) return false;
    return true;
  }

  Status SetSRGB(const ColorSpace cs,
                 const RenderingIntent ri = RenderingIntent::kRelative) {
    storage_.icc.clear();
    JXL_ENSURE(cs == ColorSpace::kGray || cs == ColorSpace::kRGB);
    storage_.color_space = cs;
    storage_.white_point = WhitePoint::kD65;
    storage_.primaries = Primaries::kSRGB;
    storage_.tf.transfer_function = TransferFunction::kSRGB;
    storage_.rendering_intent = ri;
    return CreateICC();
  }

  Status VisitFields(Visitor* JXL_RESTRICT visitor) override;

  // Accessors ensure tf.nonserialized_color_space is updated at the same time.
  ColorSpace GetColorSpace() const { return storage_.color_space; }
  void SetColorSpace(const ColorSpace cs) { storage_.color_space = cs; }
  CIExy GetWhitePoint() const { return storage_.GetWhitePoint(); }

  WhitePoint GetWhitePointType() const { return storage_.white_point; }
  Status SetWhitePointType(const WhitePoint& wp);
  Status GetPrimaries(PrimariesCIExy& p) const {
    return storage_.GetPrimaries(p);
  }

  Primaries GetPrimariesType() const { return storage_.primaries; }
  Status SetPrimariesType(const Primaries& p);

  jxl::cms::CustomTransferFunction& Tf() { return storage_.tf; }
  const jxl::cms::CustomTransferFunction& Tf() const { return storage_.tf; }

  RenderingIntent GetRenderingIntent() const {
    return storage_.rendering_intent;
  }
  void SetRenderingIntent(const RenderingIntent& ri) {
    storage_.rendering_intent = ri;
  }

  bool SameColorEncoding(const ColorEncoding& other) const {
    return storage_.SameColorEncoding(other.storage_);
  }

  mutable bool all_default;

  JxlColorEncoding ToExternal() const { return storage_.ToExternal(); }
  Status FromExternal(const JxlColorEncoding& external) {
    JXL_RETURN_IF_ERROR(storage_.FromExternal(external));
    (void)CreateICC();
    return true;
  }
  const jxl::cms::ColorEncoding& View() const { return storage_; }
  std::string Description() const;

 private:
  static std::array<ColorEncoding, 2> CreateC2(Primaries pr,
                                               TransferFunction tf);

  // If true, the codestream contains an ICC profile and we do not serialize
  // fields. Otherwise, fields are serialized and we create an ICC profile.
  bool want_icc_;

  ::jxl::cms::ColorEncoding storage_;
  // Only used if white_point == kCustom.
  Customxy white_;

  // Only valid if HaveFields()
  CustomTransferFunction tf_;

  // Only used if primaries == kCustom.
  Customxy red_;
  Customxy green_;
  Customxy blue_;
};

static inline std::string Description(const ColorEncoding& c) {
  const JxlColorEncoding external = c.View().ToExternal();
  return ColorEncodingDescription(external);
}

static inline std::ostream& operator<<(std::ostream& os,
                                       const ColorEncoding& c) {
  return os << Description(c);
}

class ColorSpaceTransform {
 public:
  explicit ColorSpaceTransform(const JxlCmsInterface& cms) : cms_(cms) {}
  ~ColorSpaceTransform() {
    if (cms_data_ != nullptr) {
      cms_.destroy(cms_data_);
    }
  }

  // Cannot copy.
  ColorSpaceTransform(const ColorSpaceTransform&) = delete;
  ColorSpaceTransform& operator=(const ColorSpaceTransform&) = delete;

  Status Init(const ColorEncoding& c_src, const ColorEncoding& c_dst,
              float intensity_target, size_t xsize, size_t num_threads) {
    JxlColorProfile input_profile;
    icc_src_ = c_src.ICC();
    input_profile.icc.data = icc_src_.data();
    input_profile.icc.size = icc_src_.size();
    input_profile.color_encoding = c_src.ToExternal();
    input_profile.num_channels = c_src.IsCMYK() ? 4 : c_src.Channels();
    JxlColorProfile output_profile;
    icc_dst_ = c_dst.ICC();
    output_profile.icc.data = icc_dst_.data();
    output_profile.icc.size = icc_dst_.size();
    output_profile.color_encoding = c_dst.ToExternal();
    if (c_dst.IsCMYK())
      return JXL_FAILURE("Conversion to CMYK is not supported");
    output_profile.num_channels = c_dst.Channels();
    cms_data_ = cms_.init(cms_.init_data, num_threads, xsize, &input_profile,
                          &output_profile, intensity_target);
    JXL_RETURN_IF_ERROR(cms_data_ != nullptr);
    return true;
  }

  float* BufSrc(const size_t thread) const {
    return cms_.get_src_buf(cms_data_, thread);
  }

  float* BufDst(const size_t thread) const {
    return cms_.get_dst_buf(cms_data_, thread);
  }

  Status Run(const size_t thread, const float* buf_src, float* buf_dst,
             size_t xsize) {
    // TODO(eustas): convert false to Status?
    return FROM_JXL_BOOL(cms_.run(cms_data_, thread, buf_src, buf_dst, xsize));
  }

 private:
  JxlCmsInterface cms_;
  void* cms_data_ = nullptr;
  // The interface may retain pointers into these.
  IccBytes icc_src_;
  IccBytes icc_dst_;
};

}  // namespace jxl

#endif  // LIB_JXL_COLOR_ENCODING_INTERNAL_H_
