# encoding: utf-8

require 'yaml'
require 'set'

require 'liquid/ext/hash'
require 'liquid/ext/ordered_set'
require 'liquid/environment'

##
# A {Configuration} consists of one or more Sections. A section is a hash-like
# object that responds to all keys in the hash as if they were methods:
#
#   > s = Section.from_hash({:v1 => 2, :nested => {:v2 => 1}})
#   > s.v1
#   => 2
#   > s.nested.v2
#   => 1
#
class Section < Hash

  class << self

    # How to handle nil values in the configuration?
    #
    # Possible values are:
    #  - :nil, nil  (return nil)
    #  - :raise  (raise an exception)
    #  - :section  (return a NilSection which can be chained)
    #
    attr_accessor :nil_action

    # Create a new section from the given hash-like object.
    #
    # @param [Hash] hsh  The hash to convert into a section.
    # @return [Section]  The new {Section} object.
    def from_hash(hsh)
      new.tap do |result|
        hsh.each do |key, value|
          result[key.to_sym] = from_value(value)
        end
      end
    end

    # Convert the given value into a Section, list of Sections or the pure
    # value. Used to recursively build the Section hash.
    #
    # @private
    def from_value(value)
      case value
      when Hash
        from_hash(value)
      when Array
        value.map do |item|
          from_value(item)
        end
      else
        value
      end
    end

  end

  # @private
  def method_missing(name, *args)
    if name.to_s =~ /(.*)=$/
      self[$1.to_sym] = Section.from_value(args.first)
    else
      value = self[name]
      self[name] = value.call if value.is_a?(Proc)
      self[name]
    end
  end
end

##
# The Configuration class provides a simple interface to configuration stored
# inside of YAML files.
#
class Configuration < Section

  DEFAULTS = {
    generic: {
      log_backend: :stdout,
      log_caller: false,
      log_level: :info,
      log_format: "%{time} %{progname}(%{pid}) [%{severity}] %{msg}\n",
      log4j_format: "%d %c(%t) [%p] %m%n",
    },
    production: {
      log_format: "%{msg}\n",
      log4j_format: "%m%n",
    },
    staging: {
      log_format: "%{msg}\n",
      log4j_format: "%m%n",
    },
  }

  # Create a new {Configuration} object.
  #
  # @yield [config]  The new configuration object.
  def initialize
    @mixins = OrderedSet.new
    @callbacks = []
    mixin(DEFAULTS)
  end

  # Mixin a configuration snippet into the current section.
  #
  # @param [Hash, String] value  A hash to merge into the current
  #                              configuration. If a string is given a filename
  #                              is assumed and the given file is expected to
  #                              contain a YAML hash.
  # @return [void]
  def mixin(value)
    @mixins << value

    if value.is_a?(String)
      value = YAML.load(File.read(value))
    end

    return unless value

    value = Section.from_hash(value)

    deep_merge!(value[:generic]) if value.has_key?(:generic)

    if value.has_key?(Env.to_sym)
      deep_merge!(value[Env.to_sym])
    else
      deep_merge!(value)
    end
  end

  # Reload all mixins.
  #
  # @return [void]
  def reload!
    clear

    @mixins.each do |file|
      mixin(file)
    end

    @callbacks.each do |callback|
      callback.call(self)
    end
  end

  # Register a callback for config mixins.
  #
  # @return [void]
  def callback(&block)
    @callbacks << block
  end

end