require 'rbconfig'

module Nugrant
  class Config
    DEFAULT_ARRAY_MERGE_STRATEGY = :replace
    DEFAULT_PARAMS_FILENAME = ".nuparams"
    DEFAULT_PARAMS_FORMAT = :yaml

    SUPPORTED_ARRAY_MERGE_STRATEGIES = [:concat, :extend, :replace]
    SUPPORTED_PARAMS_FORMATS = [:json, :yaml]

    attr_reader :params_filename, :params_format,
                :current_path, :user_path, :system_path,
                :array_merge_strategy,
                :key_error, :parse_error

    attr_writer :array_merge_strategy

    ##
    # Convenience method to easily accept either a hash that will
    # be converted to a Nugrant::Config object or directly a config
    # object.
    def self.convert(config = {})
      return config.kind_of?(Nugrant::Config) ? config : Nugrant::Config.new(config)
    end

    ##
    # Return the fully expanded path of the user parameters
    # default location that is used in the constructor.
    #
    def self.default_user_path()
      File.expand_path("~")
    end

    ##
    # Return the fully expanded path of the system parameters
    # default location that is used in the constructor.
    #
    def self.default_system_path()
      if Config.on_windows?
        return File.expand_path(ENV['PROGRAMDATA'] || ENV['ALLUSERSPROFILE'])
      end

      "/etc"
    end

    ##
    # Method to fix-up a received path. The fix-up do the follows
    # the following rules:
    #
    # 1. If the path is callable, call it to get the value.
    # 2. If value is nil, return default value.
    # 3. If value is a directory, return path + params_filename to it.
    # 4. Otherwise, return value
    #
    # @param path The path parameter received.
    # @param default The default path to use, can be a directory.
    # @param params_filename The params filename to append if path is a directory
    #
    # @return The fix-up path following rules defined above.
    #
    def self.fixup_path(path, default, params_filename)
      path = path.call if path.respond_to?(:call)

      path = File.expand_path(path || default)
      path = "#{path}/#{params_filename}" if ::File.directory?(path)

      path
    end

    def self.supported_array_merge_strategy(strategy)
      SUPPORTED_ARRAY_MERGE_STRATEGIES.include?(strategy)
    end

    def self.supported_params_format(format)
      SUPPORTED_PARAMS_FORMATS.include?(format)
    end

    ##
    # Return true if we are currently on a Windows platform.
    #
    def self.on_windows?()
      (RbConfig::CONFIG['host_os'].downcase =~ /mswin|mingw|cygwin/) != nil
    end

    ##
    # Creates a new config object that is used to configure an instance
    # of Nugrant::Parameters. See the list of options and how they interact
    # with Nugrant::Parameters.
    #
    # =| Options
    #  * +:params_filename+ - The filename used to fetch parameters from. This
    #                         will be appended to various default locations.
    #                         Location are system, project and current that
    #                         can be tweaked individually by using the options
    #                         below.
    #                           Defaults => ".nuparams"
    #  * +:params_format+   - The format in which parameters are to be parsed.
    #                         Presently supporting :yaml and :json.
    #                           Defaults => :yaml
    #  * +:current_path+    - The current path has the highest precedence over
    #                         any other location. It can be be used for many purposes
    #                         depending on your usage.
    #                          * A path from where to read project parameters
    #                          * A path from where to read overriding parameters for a cli tool
    #                          * A path from where to read user specific settings not to be committed in a VCS
    #                           Defaults => "./#{@params_filename}"
    #  * +:user_path+       - The user path is the location where the user
    #                         parameters should resides. The parameters loaded from this
    #                         location have the second highest precedence.
    #                           Defaults => "~/#{@params_filename}"
    #  * +:system_path+     - The system path is the location where system wide
    #                         parameters should resides. The parameters loaded from this
    #                         location have the third highest precedence.
    #                           Defaults => Default system path depending on OS + @params_filename
    #  * +:array_merge_strategy+  - This option controls how array values are merged together when merging
    #                               two Bag instances. Possible values are:
    #                                 * :replace => Replace current values by new ones
    #                                 * :extend => Merge current values with new ones
    #                                 * :concat => Append new values to current ones
    #                                Defaults => The strategy :replace.
    #  * +:key_error+     - A callback method receiving one argument, the key as a symbol, and that
    #                       deal with the error. If the callable does not
    #                       raise an exception, the result of it's execution is returned.
    #                         Defaults => A callable that throws a KeyError exception.
    #  * +:parse_error+   - A callback method receiving two arguments, the offending filename and
    #                       the error object, that deal with the error. If the callable does not
    #                       raise an exception, the result of it's execution is returned.
    #                         Defaults => A callable that returns the empty hash.
    #
    def initialize(options = {})
      @params_filename = options[:params_filename] || DEFAULT_PARAMS_FILENAME
      @params_format = options[:params_format] || DEFAULT_PARAMS_FORMAT

      @current_path = Config.fixup_path(options[:current_path], ".", @params_filename)
      @user_path = Config.fixup_path(options[:user_path], Config.default_user_path(), @params_filename)
      @system_path = Config.fixup_path(options[:system_path], Config.default_system_path(), @params_filename)

      @array_merge_strategy = options[:array_merge_strategy] || :replace

      @key_error = options[:key_error] || Proc.new do |key|
        raise KeyError, "Undefined parameter '#{key}'"
      end

      @parse_error = options[:parse_error] || Proc.new do |filename, error|
        {}
      end

      validate()
    end

    def ==(other)
      self.class.equal?(other.class) &&
      instance_variables.all? do |variable|
        instance_variable_get(variable) == other.instance_variable_get(variable)
      end
    end

    def [](key)
      instance_variable_get("@#{key}")
    rescue
      nil
    end

    def merge(other)
      result = dup()
      result.merge!(other)
    end

    def merge!(other)
      other.instance_variables.each do |variable|
        instance_variable_set(variable, other.instance_variable_get(variable)) if instance_variables.include?(variable)
      end

      self
    end

    def to_h()
      Hash[instance_variables.map do |variable|
        [variable[1..-1].to_sym, instance_variable_get(variable)]
      end]
    end

    alias_method :to_hash, :to_h

    def validate()
      raise ArgumentError,
        "Invalid value for :params_format. \
         The format [#{@params_format}] is currently not supported." if not Config.supported_params_format(@params_format)

      raise ArgumentError,
          "Invalid value for :array_merge_strategy. \
           The array merge strategy [#{@array_merge_strategy}] is currently not supported." if not Config.supported_array_merge_strategy(@array_merge_strategy)
    end
  end
end