
module Loquacious

  # A Configuration provides a "blank slate" for storing configuration
  # properties along with descriptions and default values. Configurations are
  # accessed by name, and hence, the configuration properties can be retrieved
  # from any location in your code.
  #
  # Each property has an associated description that can be displayed to the
  # user via the Configuration::Help class. This is the main point of the
  # Loquacious library - tell the user what all yoru configruation properties
  # actually do!
  #
  # Each configurationp property can also have a default value that is
  # returned if no value has been set for that property. Each property should
  # hae a sensible default - the user should not have to configure every
  # property in order to use a piece of code.
  #
  class Configuration

    # :stopdoc:
    class Error < StandardError; end
    @table = Hash.new
    # :startdoc:

    class << self
      # call-seq:
      #    Configuration.for( name )
      #    Configuration.for( name ) { block }
      #
      # Returns the configuration associated with the given _name_. If a
      # _block_ is given, then it will be used to create the configuration.
      #
      # The same _name_ can be used multiple times with different
      # configuration blocks. Each different block will be used to add to the
      # configuration; i.e. the configurations are additive.
      #
      def for( name, &block )
        if block.nil?
          return @table.has_key?(name) ? @table[name] : nil
        end

        if @table.has_key? name
          DSL.evaluate(:config => @table[name], &block)
        else
          @table[name] = DSL.evaluate(&block)
        end
      end

      # call-seq:
      #    Configuration.defaults_for( name ) { block }
      #
      # Set the default values for the configuration associated with the given
      # _name_. A _block_ is required by this method.
      #
      # Default values do not interfere with normal configuration values. If
      # both are defined for a particualr configruation setting, then the
      # regular configuration value will be returned.
      #
      # Defaults allow the user to define configuration values before the
      # library defaults have been loaded. They prevent library defaults from
      # overriding user settings.
      #
      def defaults_for( name, &block )
        raise "defaults require a block" if block.nil?

        if @table.has_key? name
          DSL.evaluate(:config => @table[name], :defaults_mode => true, &block)
        else
          @table[name] = DSL.evaluate(:defaults_mode => true, &block)
        end
      end

      # call-seq:
      #    Configuration.help_for( name, opts = {} )
      #
      # Returns a Help instance for the configuration associated with the
      # given _name_. See the Help#initialize method for the options that
      # can be used with this method.
      #
      def help_for( name, opts = {} )
        ::Loquacious::Configuration::Help.new(name, opts)
      end
      alias :help :help_for

      # call-seq:
      #   Configuration.to_hash( config )
      #
      # Recursively convert a configuration object to a hash.
      #
      def to_hash( config )
        cache = { nil => {} }

        Iterator.new(config).each do |node|
          ary = node.name.split('.')
          name = ary.pop.to_sym
          parent = ary.empty? ? nil : ary.join('.')

          if node.config?
            cache[node.name] = cache[parent][name] = {}
          else
            cache[parent][name] = node.obj
          end
        end

        return cache[nil]
      end
    end

    instance_methods(true).each do |m|
      next if m[::Loquacious::KEEPERS]
      undef_method m
    end
    Kernel.methods.each do |m|
      next if m[::Loquacious::KEEPERS]
      module_eval <<-CODE
        def #{m}( *args, &block )
          self.method_missing('#{m}', *args, &block)
        end
      CODE
    end
    undef_method :method_missing rescue nil

    # Accessor for the description hash.
    attr_reader :__desc

    # Accessor for configuration values
    attr_reader :__values

    # Accessor for configuration defaults
    attr_reader :__defaults

    # Flag to switch the configuration object into defaults mode. This allows
    # default values to be set instead regular values.
    attr_accessor :__defaults_mode

    # Create a new configuration object and initialize it using an optional
    # _block_ of code.
    #
    def initialize( &block )
      @__desc = Hash.new
      @__values = Hash.new
      @__defaults = Hash.new
      @__defaults_mode = false
      DSL.evaluate(:config => self, &block) if block
    end

    # When invoked, an attribute reader and writer are defined for the
    # _method_. Any arguments given are used to set the value of the
    # attributes. If a _block_ is given, then the attribute is a nested
    # configuration and the _block_ is evaluated in the context of a new
    # configuration object.
    #
    def method_missing( method, *args, &block )
      m = method.to_s.delete('=').to_sym

      __eigenclass_eval "def #{m}=( value ) @__values[#{m.inspect}] = value; end", __FILE__, __LINE__
      __eigenclass_eval <<-CODE, __FILE__, __LINE__+1
        def #{m}( *args, &block )
          value = @__values[#{m.inspect}]

          if args.empty? and !block
            return value if value.kind_of?(Configuration)
            value = @__defaults[#{m.inspect}] if value.kind_of?(Loquacious::Undefined) and @__defaults.has_key? #{m.inspect}
            return value.respond_to?(:call) ? value.call : value
          end

          if block
            v = DSL.evaluate(:defaults_mode => __defaults_mode, &block)
            if value.kind_of?(Configuration)
              value.merge! v
            else
              @__values[#{m.inspect}] = v
            end
          else
            v = (1 == args.length ? args.first : args)
            if __defaults_mode
              @__defaults[#{m.inspect}] = v
            else
              @__values[#{m.inspect}] = v
            end
          end
        end
      CODE

      __desc[m] = nil unless __desc.has_key? m

      default = ((__defaults_mode or args.empty?) and !block) ? Loquacious::Undefined.new(m.to_s) : nil
      self.__send("#{m}=", default)
      self.__send("#{m}", *args, &block)
    end

    # Only invoke public methods on the Configuration instances.
    #
    def __send( symbol, *args, &block )
      if self.respond_to? symbol
        self.__send__(symbol, *args, &block)
      else
        self.method_missing(symbol, *args, &block)
      end
    end

    # Evaluate the given _code_ string in the context of this object's
    # eigenclass (singleton class).
    #
    def __eigenclass_eval( code, file, line )
      ec = class << self; self; end
      ec.module_eval code, file, line
    rescue StandardError
      Kernel.raise Error, "cannot evalutate this code:\n#{code}\n"
    end

    # Merge the contents of the _other_ configuration into this one. Values
    # from the _other_ configuratin will overwite values in this
    # configuration.
    #
    # This function is recursive. Nested configurations will be merged with
    # their counterparts in the _other_ configuration.
    #
    def merge!( other )
      return self if other.equal? self
      Kernel.raise Error, "can only merge another Configuration" unless other.kind_of?(Configuration)

      other_values = other.__values
      other_defaults = other.__defaults

      other.__desc.each do |key,desc|
        value = @__values[key]
        other_value = other_values[key]

        if value.kind_of?(Configuration) and other_value.kind_of?(Configuration)
          value.merge! other_value
        elsif !other_value.kind_of?(Loquacious::Undefined)
          self.__send__(key, other_value)
        end

        if other_defaults.has_key? key
          @__defaults[key] = other_defaults[key]
        end

        if desc
          __desc[key] = desc
        end
      end

      self
    end

    # Provides hash accessor notation for configuration values.
    #
    #   config = Configuration.for('app') {
    #              port  1234
    #            }
    #   config[:port]  #=> 1234
    #   config.port    #=> 1234
    #
    def []( key )
      self.__send(key)
    end

    # Provides hash accessor notation for configuration values.
    #
    #   config = Configuration.for('app')
    #   config[:port] = 8808
    #   config.port            #=> 8808
    #
    def []=( key, value )
      self.__send(key, value)
    end

    # Recursively convert the configuration object to a hash.
    #
    def to_hash
      ::Loquacious::Configuration.to_hash(self)
    end

    # Implementation of a domain specific language for creating configuration
    # objects. Blocks of code are evaluted by the DSL which returns a new
    # configuration object.
    #
    class DSL
      instance_methods(true).each do |m|
        next if m[::Loquacious::KEEPERS]
        undef_method m
      end
      private_instance_methods(true).each do |m|
        next if m[::Loquacious::KEEPERS]
        undef_method m
      end
      Kernel.methods.each do |m|
        next if m[::Loquacious::KEEPERS]
        module_eval <<-CODE, __FILE__, __LINE__+1
          def #{m}( *args, &block )
            self.method_missing('#{m}', *args, &block)
          end
        CODE
      end
      undef_method :method_missing rescue nil

      # Create a new DSL and evaluate the given _block_ in the context of
      # the DSL. Returns a newly created configuration object.
      #
      def self.evaluate( opts = {}, &block )
        dsl = self.new(opts, &block)
        dsl.__config
      end

      # Returns the configuration object.
      attr_reader :__config

      # Creates a new DSL and evaluates the given _block_ in the context of
      # the DSL.
      #
      def initialize( opts = {}, &block )
        @description = nil
        @__config = opts[:config] || Configuration.new
        @__config.__defaults_mode = opts.key?(:defaults_mode) ? opts[:defaults_mode] : false
        instance_eval(&block)
      ensure
        @__config.__defaults_mode = false
      end

      # Dynamically adds the given _method_ to the configuration as an
      # attribute. The _args_ will be used to set the value of the
      # attribute. If a _block_ is given then the _args_ are ignored and the
      # attribute will be a nested configuration object.
      #
      def method_missing( method, *args, &block )
        m = method.to_s.delete('=').to_sym

        if args.length > 1
          opts = args.last.instance_of?(Hash) ? args.pop : {}
          self.desc(opts[:desc]) if opts.has_key? :desc
        end

        rv = __config.__send(m, *args, &block)
        __config.__desc[m] = @description if @description
        @description = nil
        rv
      end

      # Store the _string_ as the description for the next attribute that
      # will be configured. This description will be overwritten if the
      # attribute has a description passed as an options hash.
      #
      def desc( string )
        string = string.to_s
        string.strip!
        string.gutter!
        @description = string.empty? ? nil : string
      end
    end  # class DSL

  end  # class Configuration
end  # module Loquacious

