# -*- ruby encoding: utf-8 -*-

# An HSL colour object. Internally, the hue (#h), saturation (#s), and
# luminosity/lightness (#l) values are dealt with as fractional values in
# the range 0..1.
class Color::HSL
  include Color

  class << self
    # Creates an HSL colour object from fractional values 0..1.
    def from_fraction(h = 0.0, s = 0.0, l = 0.0, &block)
      new(h, s, l, 1.0, 1.0, &block)
    end
  end

  # Coerces the other Color object into HSL.
  def coerce(other)
    other.to_hsl
  end

  # Creates an HSL colour object from the standard values of degrees and
  # percentages (e.g., 145 deg, 30%, 50%).
  def initialize(h = 0, s = 0, l = 0, radix1 = 360.0, radix2 = 100.0, &block) # :yields self:
    @h = Color.normalize(h / radix1)
    @s = Color.normalize(s / radix2)
    @l = Color.normalize(l / radix2)
    block.call if block
  end

  # Present the colour as an HTML/CSS colour string.
  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
    to_rgb.css_rgba
  end

  # Present the colour as an HSL HTML/CSS colour string (e.g., "hsl(180,
  # 25%, 35%)").
  def css_hsl
    "hsl(%3.2f, %3.2f%%, %3.2f%%)" % [ hue, saturation, luminosity ]
  end

  # Present the colour as an HSLA (with alpha) HTML/CSS colour string (e.g.,
  # "hsla(180, 25%, 35%, 1)").
  def css_hsla
    "hsla(%3.2f, %3.2f%%, %3.2f%%, %3.2f)" % [ hue, saturation, luminosity, 1 ]
  end

  # Converting from HSL to RGB. As with all colour conversions, this is
  # approximate at best. The code here is adapted from fvd and van Dam,
  # originally found at [1] (implemented similarly at [2]).
  #
  # This simplifies the calculations with the following assumptions:
  # - Luminance values <= 0 always translate to Color::RGB::Black.
  # - Luminance values >= 1 always translate to Color::RGB::White.
  # - Saturation values <= 0 always translate to a shade of gray using
  #   luminance as a percentage of gray.
  #
  # [1] http://bobpowell.net/RGBHSB.aspx
  # [2] http://support.microsoft.com/kb/29240
  def to_rgb(*)
    if Color.near_zero_or_less?(l)
      Color::RGB::Black
    elsif Color.near_one_or_more?(l)
      Color::RGB::White
    elsif Color.near_zero?(s)
      Color::RGB.from_grayscale_fraction(l)
    else
      # Only needed for Ruby 1.8. For Ruby 1.9+, we can do:
      # Color::RGB.new(*compute_fvd_rgb, 1.0)
      Color::RGB.new(*(compute_fvd_rgb + [ 1.0 ]))
    end
  end

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

  # Converts to RGB then CMYK.
  def to_cmyk
    to_rgb.to_cmyk
  end

  # Returns the luminosity (#l) of the colour.
  def brightness
    @l
  end
  def to_greyscale
    Color::GrayScale.from_fraction(@l)
  end
  alias to_grayscale to_greyscale

  # Returns the hue of the colour in degrees.
  def hue
    @h * 360.0
  end
  # Returns the hue of the colour in the range 0.0 .. 1.0.
  def h
    @h
  end
  # Sets the hue of the colour in degrees. Colour is perceived as a wheel,
  # so values should be set properly even with negative degree values.
  def hue=(hh)
    hh = hh / 360.0

    hh += 1.0 if hh < 0.0
    hh -= 1.0 if hh > 1.0

    @h = Color.normalize(hh)
  end
  # Sets the hue of the colour in the range 0.0 .. 1.0.
  def h=(hh)
    @h = Color.normalize(hh)
  end
  # Returns the percentage of saturation of the colour.
  def saturation
    @s * 100.0
  end
  # Returns the saturation of the colour in the range 0.0 .. 1.0.
  def s
    @s
  end
  # Sets the percentage of saturation of the colour.
  def saturation=(ss)
    @s = Color.normalize(ss / 100.0)
  end
  # Sets the saturation of the colour in the ragne 0.0 .. 1.0.
  def s=(ss)
    @s = Color.normalize(ss)
  end

  # Returns the percentage of luminosity of the colour.
  def luminosity
    @l * 100.0
  end
  alias lightness luminosity
  # Returns the luminosity of the colour in the range 0.0 .. 1.0.
  def l
    @l
  end
  # Sets the percentage of luminosity of the colour.
  def luminosity=(ll)
    @l = Color.normalize(ll / 100.0)
  end
  alias lightness= luminosity= ;
  # Sets the luminosity of the colour in the ragne 0.0 .. 1.0.
  def l=(ll)
    @l = Color.normalize(ll)
  end

  def to_hsl
    self
  end

  def inspect
    "HSL [%.2f deg, %.2f%%, %.2f%%]" % [ hue, saturation, luminosity ]
  end

  # Mix the mask colour (which will be converted to an HSL colour) with the
  # current colour at the stated mix percentage as a decimal value.
  #
  # NOTE:: This differs from Color::RGB#mix_with.
  def mix_with(color, mix_percent = 0.5)
    v = to_a.zip(coerce(color).to_a).map { |(x, y)|
      ((y - x) * mix_percent) + x
    }
    self.class.from_fraction(*v)
  end

  def to_a
    [ h, s, l ]
  end

  private

  # This algorithm calculates based on a mixture of the saturation and
  # luminance, and then takes the RGB values from the hue + 1/3, hue, and
  # hue - 1/3 positions in a circular representation of colour divided into
  # four parts (confusing, I know, but it's the way that it works). See
  # #hue_to_rgb for more information.
  def compute_fvd_rgb
    t1, t2 = fvd_mix_sat_lum
    [ h + (1 / 3.0), h, h - (1 / 3.0) ].map { |v|
      hue_to_rgb(rotate_hue(v), t1, t2)
    }
  end

  # Mix saturation and luminance for use in hue_to_rgb. The base value is
  # different depending on whether luminance is <= 50% or > 50%.
  def fvd_mix_sat_lum
    t = if Color.near_zero_or_less?(l - 0.5)
             l * (1.0 + s.to_f)
           else
             l + s - (l * s.to_f)
           end
    [ 2.0 * l - t, t ]
  end

  # In HSL, hues are referenced as degrees in a colour circle. The flow
  # itself is endless; therefore, we can rotate around. The only thing our
  # implementation restricts is that you should not be > 1.0.
  def rotate_hue(h)
    h += 1.0 if Color.near_zero_or_less?(h)
    h -= 1.0 if Color.near_one_or_more?(h)
    h
  end

  # We calculate the interaction of the saturation/luminance mix (calculated
  # earlier) based on the position of the hue in the circular colour space
  # divided into quadrants. Our hue range is [0, 1), not [0, 360º).
  #
  # - The first quadrant covers the first 60º [0, 60º].
  # - The second quadrant covers the next 120º (60º, 180º].
  # - The third quadrant covers the next 60º (180º, 240º].
  # - The fourth quadrant covers the final 120º (240º, 360º).
  def hue_to_rgb(h, t1, t2)
    if Color.near_zero_or_less?((6.0 * h) - 1.0)
      t1 + ((t2 - t1) * h * 6.0)
    elsif Color.near_zero_or_less?((2.0 * h) - 1.0)
      t2
    elsif Color.near_zero_or_less?((3.0 * h) - 2.0)
      t1 + (t2 - t1) * ((2 / 3.0) - h) * 6.0
    else
      t1
    end
  end
end
