require "yaml"
require "erb"

# A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash
  class MissingSetting < StandardError; end
  
  class << self
    
    def source(value = nil)
      if value.nil?
        @source
      else
        @source = value
      end
    end

    def namespace(value = nil)
      if value.nil?
        @namespace
      else
        @namespace = value
      end
    end
    
    def default_namespace(value = nil)
      if value.nil?
        @default_namespace || 'defaults'
      else
        @default_namespace = value
      end
    end
    
    def key_by_path(key_path, separator = ".")
      # Settings.get_nested_key('some.nested.setting')
      tmp = instance
      key_path.split(separator).each do |k|
        if tmp[k].respond_to?("[]") && !tmp[k].nil?
          tmp = tmp[k]
        else
          return nil
        end
      end
      tmp
    end
    
    def [](key)
      # Setting.key.value or Setting[:key][:value] or Setting['key']['value']
      fetch(key.to_s,nil)
    end

    def []=(key,val)
      # Setting[:key] = 'value' for dynamic settings
      store(key.to_s,val)
    end
    
    def load!
      instance
      true
    end
    
    def reload!
      @instance = nil
      load!
    end
    
    private
      def instance
        @instance ||= new
      end
      
      def method_missing(name, *args, &block)
        instance.send(name, *args, &block)
      end
  end

  # Initializes a new settings object. You can initialize an object in any of the following ways:
  #
  #   Settings.new(:application) # will look for config/application.yml
  #   Settings.new("application.yaml") # will look for application.yaml
  #   Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml
  #   Settings.new(:config1 => 1, :config2 => 2)
  #
  # Basically if you pass a symbol it will look for that file in the configs directory of your rails app,
  # if you are using this in rails. If you pass a string it should be an absolute path to your settings file.
  # Then you can pass a hash, and it just allows you to access the hash via methods.
  def initialize(hash_or_file = self.class.source, section = nil)
    case hash_or_file
    when Hash
      self.replace hash_or_file
    else
      hash = YAML.load(ERB.new(File.read(hash_or_file)).result).to_hash
      default_hash = hash[self.class.default_namespace] || {}      
      hash = hash[self.class.namespace] if self.class.namespace
      self.replace default_hash.deep_merge(hash)
    end
    @section = section || hash_or_file  # so end of error says "in application.yml"
    create_accessors!
  end

  def [](key)
    # @settings.key.value or @settings[:key][:value] or @settings['key']['value']
    super(key.to_s)
  end

  # Called for dynamically-defined keys, and also the first key deferenced at the top-level, if load! is not used.
  # Otherwise, create_accessors! (called by new) will have created actual methods for each key.
  def method_missing(key, *args, &block)
    begin
      value = fetch(key.to_s)
    rescue IndexError
      raise MissingSetting, "Missing setting '#{key}' in #{@section}"
    end
    value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
  end

  private
    def create_accessors!
      self.each do |key,val|
        begin
          Kernel.Float(key.to_s) # allow numeric keys          
        rescue ArgumentError, TypeError
          self.class.class_eval(<<-EndEval, __FILE__, __LINE__ + 1)
            def #{key}
              return @#{key} if @#{key}  # cache (performance)
              value = fetch('#{key}')
              @#{key} = value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
            end
          EndEval
        end        
      end
    end

end