# Defines the Autumn::Speciator class, which stores the configurations of many
# Autumn objects.

require 'singleton'

module Autumn

  # The Speciator stores the global, season, stem, and leaf configurations. It
  # generates composite hashes, so that any leaf or stem can know its specific
  # configuration as a combination of its options and those of the scopes above
  # it.
  #
  # Smaller scopes override larger ones; any season-specific options will
  # replace global options, and leaf or stem options will overwrite season
  # options. Leaf and stem options are independent from each other, however,
  # since leaves and stems share a many-to-many relationship.
  #
  # Option identifiers can be specified as strings or symbols but are always
  # stored as symbols and never accessed as strings.
  #
  # This is a singleton class; only one instance of it exists for any Autumn
  # process. However, for the sake of convenience, many other objects use a
  # +config+ attribute containing the instance.

  class Speciator
    include Singleton
  
    # Creates a new instance storing no options.
  
    def initialize
      @global_options = Hash.new
      @season_options = Hash.new
      @stem_options = Hash.autonew
      @leaf_options = Hash.autonew
    end
  
    # Returns the global-scope or season-scope config option with the given
    # symbol. Season-scope config options will override global ones.
  
    def [](sym)
      @season_options[sym] or @global_options[sym]
    end
  
    # When called with a hash: Takes a hash of options and values, and sets them
    # at the global scope level.
    #
    # When called with an option identifier: Returns the value for that option at
    # the global scope level.
  
    def global(arg)
      arg.kind_of?(Hash) ? @global_options.update(arg.rekey(&:to_sym)) : @global_options[arg]
    end
  
    # When called with a hash: Takes a hash of options and values, and sets them
    # at the season scope level.
    #
    # When called with an option identifier: Returns the value for that option
    # exclusively at the season scope level.
    #
    # Since Autumn can only be run in one season per process, there is no need
    # to store the options of specific seasons, only the current season.
  
    def season(arg)
      arg.kind_of?(Hash) ? @season_options.update(arg.rekey(&:to_sym)) : @season_options[arg]
    end
    
    # Returns true if the given identifier is a known stem identifier.
    
    def stem?(stem)
      return !@stem_options[stem].nil?
    end
    
    # When called with a hash: Takes a hash of options and values, and sets them
    # at the stem scope level.
    #
    # When called with an option identifier: Returns the value for that option
    # exclusively at the stem scope level.
    #
    # The identifier for the stem must be specified.
  
    def stem(stem, arg)
      arg.kind_of?(Hash) ? @stem_options[stem].update(arg.rekey(&:to_sym)) : @stem_options[stem][arg]
    end
    
    # Returns true if the given identifier is a known leaf identifier.
    
    def leaf?(leaf)
      return !@leaf_options[leaf].nil?
    end
  
    # When called with a hash: Takes a hash of options and values, and sets them
    # at the leaf scope level.
    #
    # When called with an option identifier: Returns the value for that option
    # exclusively at the leaf scope level.
    #
    # The identifier for the leaf must be specified.
  
    def leaf(leaf, arg)
      arg.kind_of?(Hash) ? @leaf_options[leaf].update(arg.rekey(&:to_sym)) : @leaf_options[leaf][arg]
    end
    
    # Yields each stem identifier and its options.
    
    def each_stem
      @stem_options.each { |stem, options| yield stem, options }
    end
    
    # Yields each leaf identifier and its options.
    
    def each_leaf
      @leaf_options.each { |leaf, options| yield leaf, options }
    end
    
    # Returns an array of all leaf class names in use.
    
    def all_leaf_classes
      @leaf_options.values.collect { |opts| opts[:class] }.uniq
    end
  
    # Returns the composite options for a stem (by identifier), as an
    # amalgamation of all the scope levels' options.
  
    def options_for_stem(identifier)
      OptionsProxy.new(@global_options, @season_options, @stem_options[identifier])
    end
    
    # Returns the composite options for a leaf (by identifier), as an
    # amalgamation of all the scope levels' options.
    
    def options_for_leaf(identifier)
      OptionsProxy.new(@global_options, @season_options, @leaf_options[identifier])
    end
  end
  
  class OptionsProxy # :nodoc:
    MERGED_METHODS = [ :[], :each, :each_key, :each_pair, :each_value, :eql?,
      :fetch, :has_key?, :include?, :key?, :member?, :has_value?, :value?,
      :hash, :index, :inspect, :invert, :keys, :length, :size, :merge, :reject,
      :select, :sort, :to_a, :to_hash, :to_s, :values, :values_at ]
    
    def initialize(*hashes)
      raise ArgumentError unless hashes.all? { |hsh| hsh.kind_of? Hash }
      @hashes = hashes
      @hashes << Hash.new # the runtime settings, which take precedence over all
    end
    
    def method_missing(meth, *args, &block)
      if MERGED_METHODS.include? meth then
        merged.send meth, *args, &block
      else
        @hashes.last.send meth, *args, &block
      end
    end
    
    private
    
    #TODO optimize this by not regenerating it every time it's accessed
    def merged
      merged = Hash.new
      @hashes.each { |hsh| merged.merge! hsh }
      return merged
    end
  end
end