#
# Author::      BJ Dierkes <derks@datafolklabs.com>
# Copyright::   Copyright (c) 2006,2016 Data Folk Labs, LLC
# License::     MIT
# URL::         https://github.com/datafolklabs/ruby-parseconfig
#

# This class was written to simplify the parsing of configuration
# files in the format of "param = value".  Please review the
# demo files included with this package.
#
# For further information please refer to the './doc' directory
# as well as the ChangeLog and README files included.
#

# Note: A group is a set of parameters defined for a subpart of a
# config file

class ParseConfig
  attr_accessor :config_file, :params, :groups

  # Initialize the class with the path to the 'config_file'
  # The class objects are dynamically generated by the
  # name of the 'param' in the config file.  Therefore, if
  # the config file is 'param = value' then the itializer
  # will eval "@param = value"
  #
  def initialize(config_file = nil, separator = '=', comments = ['#', ';'])
    @config_file = config_file
    @params = {}
    @groups = []
    @split_regex = '\s*' + separator + '\s*'
    @comments = comments

    return unless config_file

    validate_config
    import_config
  end

  # Validate the config file, and contents
  def validate_config
    return if File.readable?(config_file)

    raise Errno::EACCES, "#{config_file} is not readable"

    # FIX ME: need to validate contents/structure?
  end

  # Import data from the config to our config object.
  def import_config
    # The config is top down.. anything after a [group] gets added as part
    # of that group until a new [group] is found.
    group = nil
    open(config_file) do |f|
      f.each_with_index do |line, i|
        line.strip!

        # force_encoding not available in all versions of ruby
        begin
          if i.eql? 0 && line.include?("\xef\xbb\xbf".force_encoding('UTF-8'))
            line.delete!("\xef\xbb\xbf".force_encoding('UTF-8'))
          end
        rescue NoMethodError
        end

        is_comment = false
        @comments.each do |comment|
          if /^#{comment}/.match(line)
            is_comment = true
            break
          end
        end

        unless is_comment
          if /#{@split_regex}/.match(line)
            param, value = line.split(/#{@split_regex}/, 2)
            var_name = param.to_s.chomp.strip
            value = value.chomp.strip
            new_value = ''
            if value
              if value =~ /^['"](.*)['"]$/
                new_value = Regexp.last_match(1)
              else
                new_value = value
              end
            else
              new_value = ''
            end

            if group
              add_to_group(group, var_name, new_value)
            else
              add(var_name, new_value)
            end
          elsif /^\[(.+)\](\s*#{escaped_comment_regex}+.*)?$/.match(line).to_a != []
            group = /^\[(.+)\](\s*#{escaped_comment_regex}+.*)?$/.match(line).to_a[1]
            add(group, {})
          elsif /\w+/.match(line)
            add(line.to_s.chomp.strip, true)
          end
        end
      end
    end
  end

  def escaped_comment_regex
    /[#{Regexp.escape(@comments.join(''))}]/
  end

  # This method will provide the value held by the object "@param"
  # where "@param" is actually the name of the param in the config
  # file.
  #
  # DEPRECATED - will be removed in future versions
  #
  def get_value(param)
    puts 'ParseConfig Deprecation Warning: get_value() is deprecated. Use ' \
         "config['param'] or config['group']['param'] instead."
    params[param]
  end

  # This method is a shortcut to accessing the @params variable
  def [](param)
    params[param]
  end

  # This method returns all parameters/groups defined in a config file.
  def get_params
    params.keys
  end

  # List available sub-groups of the config.
  def get_groups
    groups
  end

  # This method adds an element to the config object (not the config file)
  # By adding a Hash, you create a new group
  def add(param_name, value, override = false)
    if value.class == Hash
      if params.key? param_name
        if params[param_name].class == Hash
          if override
            params[param_name] = value
          else
            params[param_name].merge!(value)
          end
        elsif params.key? param_name
          if params[param_name].class != value.class
            raise ArgumentError, "#{param_name} already exists, and is of different type!"
          end
        end
      else
        params[param_name] = value
      end
      unless groups.include?(param_name)
        groups.push(param_name)
      end
    else
      params[param_name] = value
    end
  end

  # Add parameters to a group. Note that parameters with the same name
  # could be placed in different groups
  def add_to_group(group, param_name, value)
    add(group, {}) unless groups.include?(group)
    params[group][param_name] = value
  end

  # Writes out the config file to output_stream
  def write(output_stream = STDOUT, quoted = true)
    params.each do |name, value|
      if value.class.to_s != 'Hash'
        if quoted == true
          output_stream.puts "#{name} = \"#{value}\""
        else
          output_stream.puts "#{name} = #{value}"
        end
      end
    end
    output_stream.puts "\n"

    groups.each do |group|
      output_stream.puts "[#{group}]"
      params[group].each do |param, value|
        if quoted == true
          output_stream.puts "#{param} = \"#{value}\""
        else
          output_stream.puts "#{param} = #{value}"
        end
      end
      output_stream.puts "\n"
    end
  end

  # Public: Compare this ParseConfig to some other ParseConfig. For two config to
  # be equivalent, they must have the same sections with the same parameters
  #
  # other - The other ParseConfig.
  #
  # Returns true if ParseConfig are equivalent and false if they differ.

  def eql?(other)
    params == other.params && groups == other.groups
  end
  alias == eql?
end
