require 'color/palette'

# Generates a monochromatic constrasting colour palette for background and
# foreground. What does this mean?
#
# Monochromatic: A single colour is used to generate the base palette, and
# this colour is lightened five times and darkened five times to provide
# eleven distinct colours.
#
# Contrasting: The foreground is also generated as a monochromatic colour
# palette; however, all generated colours are tested to see that they are
# appropriately contrasting to ensure maximum readability of the foreground
# against the background.
class Color::Palette::MonoContrast
  # Hash of CSS background colour values.
  #
  # This is always 11 values:
  #
  # 0::       The starting colour.
  # +1..+5::  Lighter colours.
  # -1..-5::  Darker colours.
  attr_reader :background
  # Hash of CSS foreground colour values.
  #
  # This is always 11 values:
  #
  # 0::       The starting colour.
  # +1..+5::  Lighter colours.
  # -1..-5::  Darker colours.
  attr_reader :foreground

  DEFAULT_MINIMUM_BRIGHTNESS_DIFF = (125.0 / 255.0)

  # The minimum brightness difference between the background and the
  # foreground, and must be between 0..1. Setting this value will regenerate
  # the palette based on the base colours. The default value for this is 125
  # / 255.0. If this value is set to +nil+, it will be restored to the
  # default.
  attr_accessor :minimum_brightness_diff
  remove_method :minimum_brightness_diff= ;
  def minimum_brightness_diff=(bd) #:nodoc:
    if bd.nil?
      @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
    elsif bd > 1.0
      @minimum_brightness_diff = 1.0
    elsif bd < 0.0
      @minimum_brightness_diff = 0.0
    else
      @minimum_brightness_diff = bd
    end

    regenerate(@background[0], @foreground[0])
  end

  DEFAULT_MINIMUM_COLOR_DIFF = (500.0 / 255.0)

  # The minimum colour difference between the background and the foreground,
  # and must be between 0..3. Setting this value will regenerate the palette
  # based on the base colours. The default value for this is 500 / 255.0.
  attr_accessor :minimum_color_diff
  remove_method :minimum_color_diff= ;
  def minimum_color_diff=(cd) #:noco:
    if cd.nil?
      @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF
    elsif cd > 3.0
      @minimum_color_diff = 3.0
    elsif cd < 0.0
      @minimum_color_diff = 0.0
    else
      @minimum_color_diff = cd
    end
    regenerate(@background[0], @foreground[0])
  end

  # Generate the initial palette.
  def initialize(background, foreground = nil)
    @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
    @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF

    regenerate(background, foreground)
  end

  # Generate the colour palettes.
  def regenerate(background, foreground = nil)
    foreground ||= background
    background = background.to_rgb
    foreground = foreground.to_rgb

    @background = {}
    @foreground = {}

    @background[-5] = background.darken_by(10)
    @background[-4] = background.darken_by(25)
    @background[-3] = background.darken_by(50)
    @background[-2] = background.darken_by(75)
    @background[-1] = background.darken_by(85)
    @background[ 0] = background
    @background[+1] = background.lighten_by(85)
    @background[+2] = background.lighten_by(75)
    @background[+3] = background.lighten_by(50)
    @background[+4] = background.lighten_by(25)
    @background[+5] = background.lighten_by(10)

    @foreground[-5] = calculate_foreground(@background[-5], foreground)
    @foreground[-4] = calculate_foreground(@background[-4], foreground)
    @foreground[-3] = calculate_foreground(@background[-3], foreground)
    @foreground[-2] = calculate_foreground(@background[-2], foreground)
    @foreground[-1] = calculate_foreground(@background[-1], foreground)
    @foreground[ 0] = calculate_foreground(@background[ 0], foreground)
    @foreground[+1] = calculate_foreground(@background[+1], foreground)
    @foreground[+2] = calculate_foreground(@background[+2], foreground)
    @foreground[+3] = calculate_foreground(@background[+3], foreground)
    @foreground[+4] = calculate_foreground(@background[+4], foreground)
    @foreground[+5] = calculate_foreground(@background[+5], foreground)
  end

  # Given a background colour and a foreground colour, modifies the
  # foreground colour so that it will have enough contrast to be seen
  # against the background colour.
  #
  # Uses #mininum_brightness_diff and #minimum_color_diff.
  def calculate_foreground(background, foreground)
    nfg = nil
    # Loop through brighter and darker versions of the foreground color. The
    # numbers here represent the amount of foreground color to mix with
    # black and white.
    [100, 75, 50, 25, 0].each do |percent|
      dfg = foreground.darken_by(percent)
      lfg = foreground.lighten_by(percent)

      dbd = brightness_diff(background, dfg)
      lbd = brightness_diff(background, lfg)

      if lbd > dbd
        nfg = lfg
        nbd = lbd
      else
        nfg = dfg
        nbd = dbd
      end

      ncd = color_diff(background, nfg)

      break if nbd >= @minimum_brightness_diff and ncd >= @minimum_color_diff
    end
    nfg
  end

  # Returns the absolute difference between the brightness levels of two
  # colours. This will be a decimal value between 0 and 1. W3C accessibility
  # guidelines for colour contrast[http://www.w3.org/TR/AERT#color-contrast]
  # suggest that this value be at least approximately 0.49 (125 / 255.0) for
  # proper contrast.
  def brightness_diff(c1, c2)
    (c1.brightness - c2.brightness).abs
  end

  # Returns the contrast between to colours, a decimal value between 0 and
  # 3. W3C accessibility guidelines for colour
  # contrast[http://www.w3.org/TR/AERT#color-contrast] suggest that this
  # value be at least approximately 1.96 (500 / 255.0) for proper contrast.
  def color_diff(c1, c2)
    r = (c1.r - c2.r).abs
    g = (c1.g - c2.g).abs
    b = (c1.b - c2.b).abs
    r + g + b
  end
end
