class RefinerySetting < ActiveRecord::Base

  FORM_VALUE_TYPES = [
    ['Multi-line', 'text_area'],
    ['Checkbox', 'check_box']
  ]

  validates :name, :presence => true

  serialize :value # stores into YAML format
  serialize :callback_proc_as_string

  # Docs for acts_as_indexed http://github.com/dougal/acts_as_indexed
  acts_as_indexed :fields => [:name]

  attr_accessible :name, :value, :destroyable,
                  :scoping, :restricted, :callback_proc_as_string,
                  :form_value_type

  before_save do |setting|
    setting.restricted = false if setting.restricted.nil?
  end

  after_save do |setting|
    setting.class.rewrite_cache
  end

  class << self
    # Number of settings to show per page when using will_paginate
    def per_page
      12
    end

    def ensure_cache_exists!
      if (result = Rails.cache.read(cache_key, :multithread => true)).nil?
        result = rewrite_cache
      end

      result
    end
    protected :ensure_cache_exists!

    def cache_read(name = nil, scoping = nil)
      result = ensure_cache_exists!

      if name.present?
        scoping = scoping.to_s if scoping.is_a?(Symbol)
        result = result.detect do |rs|
          rs[:name] == name.to_s.downcase.to_sym and rs[:scoping] == scoping
        end
        result = result[:value] if !result.nil? and result.keys.include?(:value)
      end

      result
    end

    def to_cache(settings)
      settings.collect do |rs|
        {
          :name => rs.name.to_s.downcase.to_sym,
          :value => rs.value,
          :scoping => rs.scoping,
          :destroyable => rs.destroyable
        }
      end
    end

    def rewrite_cache
      # delete cache
      Rails.cache.delete(cache_key, :multithread => true)

      # generate new cache
      result = (to_cache(all) if (table_exists? rescue false))

      # write cache
      Rails.cache.write(cache_key, result, :multithread => true)

      # return cache, or lack thereof.
      result ||= []
    end

    def cache_key
      [Refinery.base_cache_key, 'refinery_settings_cache'].join('_')
    end

    # find_or_set offers a convenient way to
    def find_or_set(name, the_value, options={})
      # Merge default options with supplied options.
      options = {
        :scoping => nil, :restricted => false,
        :callback_proc_as_string => nil, :form_value_type => 'text_area'
      }.merge(options)

      # try to find the setting first
      value = get(name, :scoping => options[:scoping])

      # if the setting's value is nil, store a new one using the existing functionality.
      value = set(name, options.merge({:value => the_value})) if value.nil?

      # Return what we found.
      value
    end

    alias :get_or_set :find_or_set

    # Retrieve the current value for the setting whose name is supplied.
    def get(name, options = {})
      options = {:scoping => nil}.update(options)
      cache_read(name, options[:scoping])
    end

    alias :[] :get

    def set(name, value)
      return (value.is_a?(Hash) ? value[:value] : value) unless (table_exists? rescue false)

      scoping = (value[:scoping] if value.is_a?(Hash) and value.has_key?(:scoping))
      setting = find_or_initialize_by_name_and_scoping(:name => name.to_s, :scoping => scoping)

      # you could also pass in {:value => 'something', :scoping => 'somewhere'}
      unless value.is_a?(Hash) and value.has_key?(:value)
        setting.value = value
      else
        # set the value last, so that the other attributes can transform it if required.
        setting.form_value_type = value[:form_value_type] || 'text_area' if setting.respond_to?(:form_value_type)
        setting.scoping = value[:scoping] if value.has_key?(:scoping)
        setting.callback_proc_as_string = value[:callback_proc_as_string] if value.has_key?(:callback_proc_as_string)
        setting.destroyable = value[:destroyable] if value.has_key?(:destroyable)
        setting.value = value[:value]
      end

      # Save because we're in a setter method.
      setting.save

      # Return the value
      setting.value
    end
  end

  # prettier version of the name.
  # site_name becomes Site Name
  def title
    result = name.to_s.titleize
    result += ' (' + scoping.titleize + ')' if scoping.is_a?(String)
    result
  end

  # form_value is so that on the web interface we can display a sane value.
  def form_value
    unless self[:value].blank? or self[:value].is_a?(String)
      YAML::dump(self[:value])
    else
      self[:value]
    end
  end

  def value
    replacements!(self[:value])
  end

  def value=(new_value)
    # must convert "1" to true and "0" to false when supplied using 'check_box', unfortunately.
    if ["1", "0"].include?(new_value) and form_value_type == 'check_box'
      new_value = new_value == "1" ? true : false
    end

    # must convert to string if true or false supplied otherwise it becomes 0 or 1, unfortunately.
    if [true, false].include?(new_value)
      new_value = new_value.to_s
    end

    super
  end

  def callback_proc
    eval("Proc.new{#{callback_proc_as_string} }") if callback_proc_as_string.present?
  end

private
  # Below is not very nice, but seems to be required. The problem is when Rails
  # serialises a fields like booleans it doesn't retrieve it back out as a boolean
  # it just returns a string. This code maps the two boolean values
  # correctly so that a boolean is returned instead of a string.
  REPLACEMENTS = {"true" => true, "false" => false}

  def replacements!(current_value)
    # This bit handles true and false so that true and false are actually returned
    # not "0" and "1"
    REPLACEMENTS.each do |current, new_value|
      current_value = new_value if current_value == current
    end

    # converts the serialised value back to an integer
    # if the value is an integer
    begin
      if current_value.to_i.to_s == current_value
        current_value = current_value.to_i
      end
    rescue
      current_value
    end

    current_value
  end

end