# An CMYK colour object. CMYK (cyan, magenta, yellow, and black) colours are
# based on additive percentages of ink. A CMYK colour of (0.3, 0, 0.8, 0.3)
# would be mixed from 30% cyan, 0% magenta, 80% yellow, and 30% black.
# Primarily used in four-colour printing processes.
class Color::CMYK
  include Color

  # The format of a DeviceCMYK colour for PDF. In color-tools 2.0 this will
  # be removed from this package and added back as a modification by the
  # PDF::Writer package.
  PDF_FORMAT_STR = "%.3f %.3f %.3f %.3f %s"

  # Coerces the other Color object into CMYK.
  def coerce(other)
    other.to_cmyk
  end

  class << self
    # Creates a CMYK colour object from fractional values 0..1.
    #
    #   Color::CMYK.from_fraction(0.3, 0, 0.8, 0.3)
    def from_fraction(c = 0, m = 0, y = 0, k = 0, &block)
      new(c, m, y, k, 1.0, &block)
    end

    # Creates a CMYK colour object from percentages. Internally, the colour is
    # managed as fractional values 0..1.
    #
    #   Color::CMYK.new(30, 0, 80, 30)
    def from_percent(c = 0, m = 0, y = 0, k = 0, &block)
      new(c, m, y, k, &block)
    end
  end

  # Creates a CMYK colour object from percentages. Internally, the colour is
  # managed as fractional values 0..1.
  #
  #   Color::CMYK.new(30, 0, 80, 30)
  def initialize(c = 0, m = 0, y = 0, k = 0, radix = 100.0, &block) # :yields self:
    @c, @m, @y, @k = [ c, m, y, k ].map { |v| Color.normalize(v / radix) }
    block.call(self) if block
  end

  # Present the colour as a DeviceCMYK fill colour string for PDF. This will
  # be removed from the default package in color-tools 2.0.
  def pdf_fill
    PDF_FORMAT_STR % [ @c, @m, @y, @k, "k" ]
  end

  # Present the colour as a DeviceCMYK stroke colour string for PDF. This
  # will be removed from the default package in color-tools 2.0.
  def pdf_stroke
    PDF_FORMAT_STR % [ @c, @m, @y, @k, "K" ]
  end

  # Present the colour as an RGB HTML/CSS colour string (e.g., "#aabbcc").
  # Note that this will perform a #to_rgb operation using the default
  # conversion formula.
  def html
    to_rgb.html
  end

  # Present the colour as an RGB HTML/CSS colour string (e.g., "rgb(0%, 50%,
  # 100%)"). Note that this will perform a #to_rgb operation using the
  # default conversion formula.
  def css_rgb
    to_rgb.css_rgb
  end

  # Present the colour as an RGBA (with alpha) HTML/CSS colour string (e.g.,
  # "rgb(0%, 50%, 100%, 1)"). Note that this will perform a #to_rgb
  # operation using the default conversion formula.
  def css_rgba(alpha = 1)
    to_rgb.css_rgba(alpha)
  end

  # Present the colour as an HSL HTML/CSS colour string (e.g., "hsl(180,
  # 25%, 35%)"). Note that this will perform a #to_hsl operation using the
  # default conversion formula.
  def css_hsl
    to_hsl.css_hsl
  end

  # Present the colour as an HSLA (with alpha) HTML/CSS colour string (e.g.,
  # "hsla(180, 25%, 35%, 1)"). Note that this will perform a #to_hsl
  # operation using the default conversion formula.
  def css_hsla
    to_hsl.css_hsla
  end

  # Converts the CMYK colour to RGB. Most colour experts strongly suggest
  # that this is not a good idea (some even suggesting that it's a very bad
  # idea). CMYK represents additive percentages of inks on white paper,
  # whereas RGB represents mixed colour intensities on a black screen.
  #
  # However, the colour conversion can be done, and there are two different
  # methods for the conversion that provide slightly different results.
  # Adobe PDF conversions are done with the first form.
  #
  #     # Adobe PDF Display Formula
  #   r = 1.0 - min(1.0, c + k)
  #   g = 1.0 - min(1.0, m + k)
  #   b = 1.0 - min(1.0, y + k)
  #
  #     # Other
  #   r = 1.0 - (c * (1.0 - k) + k)
  #   g = 1.0 - (m * (1.0 - k) + k)
  #   b = 1.0 - (y * (1.0 - k) + k)
  #
  # If we have a CMYK colour of [33% 66% 83% 25%], the first method will
  # give an approximate RGB colour of (107, 23, 0) or #6b1700. The second
  # method will give an approximate RGB colour of (128, 65, 33) or #804121.
  # Which is correct? Although the colours may seem to be drastically
  # different in the RGB colour space, they are very similar colours,
  # differing mostly in intensity. The first is a darker, slightly redder
  # brown; the second is a lighter brown.
  #
  # Because of this subtlety, both methods are now offered for conversion.
  # The Adobe method is not used by default; to enable it, pass +true+ to
  # #to_rgb.
  #
  # Future versions of Color may offer other conversion mechanisms that
  # offer greater colour fidelity, including recognition of ICC colour
  # profiles.
  def to_rgb(use_adobe_method = false)
    if use_adobe_method
      Color::RGB.from_fraction(*adobe_cmyk_rgb)
    else
      Color::RGB.from_fraction(*standard_cmyk_rgb)
    end
  end

  # Converts the CMYK colour to a single greyscale value. There are
  # undoubtedly multiple methods for this conversion, but only a minor
  # variant of the Adobe conversion method will be used:
  #
  #   g = 1.0 - min(1.0, 0.299 * c + 0.587 * m + 0.114 * y + k)
  #
  # This treats the CMY values similarly to YIQ (NTSC) values and then adds
  # the level of black. This is a variant of the Adobe version because it
  # uses the more precise YIQ (NTSC) conversion values for Y (intensity)
  # rather than the approximates provided by Adobe (0.3, 0.59, and 0.11).
  def to_grayscale
    c = 0.299 * @c.to_f
    m = 0.587 * @m.to_f
    y = 0.114 * @y.to_f
    g = 1.0 - [1.0, c + m + y + @k].min
    Color::GrayScale.from_fraction(g)
  end
  alias to_greyscale to_grayscale

  def to_cmyk
    self
  end

  def inspect
    "CMYK [%.2f%%, %.2f%%, %.2f%%, %.2f%%]" % [ cyan, magenta, yellow, black ]
  end

  # Converts to RGB then YIQ.
  def to_yiq
    to_rgb.to_yiq
  end

  # Converts to RGB then HSL.
  def to_hsl
    to_rgb.to_hsl
  end

  # Returns the cyan (C) component of the CMYK colour as a percentage value.
  def cyan
    @c * 100.0
  end
  # Returns the cyan (C) component of the CMYK colour as a value in the
  # range 0.0 .. 1.0.
  def c
    @c
  end
  # Sets the cyan (C) component of the CMYK colour as a percentage value.
  def cyan=(cc)
    @c = Color.normalize(cc / 100.0)
  end
  # Sets the cyan (C) component of the CMYK colour as a value in the range
  # 0.0 .. 1.0.
  def c=(cc)
    @c = Color.normalize(cc)
  end

  # Returns the magenta (M) component of the CMYK colour as a percentage
  # value.
  def magenta
    @m * 100.0
  end
  # Returns the magenta (M) component of the CMYK colour as a value in the
  # range 0.0 .. 1.0.
  def m
    @m
  end
  # Sets the magenta (M) component of the CMYK colour as a percentage value.
  def magenta=(mm)
    @m = Color.normalize(mm / 100.0)
  end
  # Sets the magenta (M) component of the CMYK colour as a value in the
  # range 0.0 .. 1.0.
  def m=(mm)
    @m = Color.normalize(mm)
  end

  # Returns the yellow (Y) component of the CMYK colour as a percentage
  # value.
  def yellow
    @y * 100.0
  end
  # Returns the yellow (Y) component of the CMYK colour as a value in the
  # range 0.0 .. 1.0.
  def y
    @y
  end
  # Sets the yellow (Y) component of the CMYK colour as a percentage value.
  def yellow=(yy)
    @y = Color.normalize(yy / 100.0)
  end
  # Sets the yellow (Y) component of the CMYK colour as a value in the range
  # 0.0 .. 1.0.
  def y=(kk)
    @y = Color.normalize(kk)
  end

  # Returns the black (K) component of the CMYK colour as a percentage
  # value.
  def black
    @k * 100.0
  end
  # Returns the black (K) component of the CMYK colour as a value in the
  # range 0.0 .. 1.0.
  def k
    @k
  end
  # Sets the black (K) component of the CMYK colour as a percentage value.
  def black=(kk)
    @k = Color.normalize(kk / 100.0)
  end
  # Sets the black (K) component of the CMYK colour as a value in the range
  # 0.0 .. 1.0.
  def k=(kk)
    @k = Color.normalize(kk)
  end

  def to_a
    [ c, m, y, k ]
  end

  private
  # Implements the Adobe PDF conversion of CMYK to RGB.
  def adobe_cmyk_rgb
    [ @c, @m, @y ].map { |v| 1.0 - [ 1.0, v + @k ].min }
  end

  # Implements the standard conversion of CMYK to RGB.
  def standard_cmyk_rgb
    [ @c, @m, @y ].map { |v| 1.0 - (v * (1.0 - k) + k) }
  end
end
