##
## SmbSearch: Configs and options loader and accessors
## Copyright (c) 2007 SATOH Fumiyasu @ OSS Technology Co., Japan
##                    <http://www.osstech.co.jp/>
##
## Date: 2007-07-05, since 2007-07-04
##

module SmbSearch

class Config < Hash
  @@type = 0
  TYPE_STRING =		@@type += 1
  TYPE_INTEGER =	@@type += 1
  TYPE_BOOL =		@@type += 1
  TYPE_DEFAULT =	TYPE_STRING

  @@load = 0
  ## Override parameters that are not loaded before
  LOAD_NOTLOADED	= @@load += 1
  ## Override non array parameters and append values into array parameters
  LOAD_APPEND		= @@load += 1

  def initialize(attrs={})
    super()
    @loaded = Hash.new
    @attrs = Hash.new
    attrs.each do |name, attr|
      name = name.to_sym
      @attrs[name] = attr.clone
      if @attrs[name][:default]
	self[name] = @attrs[name][:default] if @attrs[name][:default]
      elsif @attrs[name][:array?]
	self[name] = []
      end
    end
  end

  def [](name)
    return super(name.to_sym)
  end

  def []=(name, value)
    return super(name.to_sym, value)
  end

  def array?(name)
    return @attrs[name.to_sym][:array?]
  end

  def type?(name, type)
    return (@attrs[name.to_sym][:type] || TYPE_DEFAULT) == type
  end

  def load(target, lflag=nil, &filter)
    config = case
      when target.class == Array
	load_array(target, &filter)
      when target.class == String
	load_stream(File.open(target), &filter)
      when target.respond_to?(:read)
	load_stream(target, &filter)
      else
	raise TypeError, "unsupported object to load: #{target.class}"
    end

    config.each do |name, value|
      if @attrs[name][:ok_on] && !(@attrs[name][:ok_on] === value)
	raise ArgumentError, "invalid option value: #{name}=#{value}"
      end
      if @attrs[name][:ng_on] && @attrs[name][:ng_on] === value
	raise ArgumentError, "invalid option value: #{name}=#{value}"
      end
    end

    case lflag
    when LOAD_APPEND
      config.each do |name, value|
	if array?(name)
	  self[name] += value
	else
	  self[name] = value
	end
      end
    when LOAD_NOTLOADED
      config.each do |name, value|
	self[name] = value unless @loaded[name]
      end
    else
      config.each do |name, value|
	self[name] = value
      end
    end

    config.each_key do |name|
      @loaded[name] = true
    end
  end

  protected

  ## Load command-line-style options with arguments
  def load_array(array, &filter)
    config = Hash.new
    if filter
      raise NotImplementedError, 'filter proc not supported for array'
    end

    while opt = array.shift
      if opt !~ /^-/ || opt == '--'
	array.unshift(opt)
	break
      end

      name = opt.sub!(/^--?/, '').gsub('-', '_').to_sym
      unless @attrs.key?(name)
	raise NameError, "unknown option name: #{name}"
      end

      if type?(name, TYPE_BOOL)
	value = true
      else
	unless value = array.shift
	  raise ArgumentError, "option requires an argument: #{opt}"
	end
	value = value.to_i if type?(name, TYPE_INTEGER)
      end
      if array?(name)
	config[name] = (config[name] || Array.new) << value
      else
	config[name] = value
      end
    end

    return config
  end

  ## Load hashed options
  def load_hash(hash, &filter)
    hash.each do |name, value|
      name, value = filter.call(name, value) if filter

      name = name.to_sym
      unless @attrs.key?(name)
	raise NameError, "unknown option name: #{name}"
      end

      values = array?(name) ? value.strip.split(/\s+/) : [value.strip]
      case
      when type?(name, TYPE_BOOL)
	values.map! do |value|
	  ## NOTE: Empty string means true.
	  value = value.match(/^(yes|true|1|)$/) ? true : false
	end
      when type?(name, TYPE_INTEGER)
	values.map! do |value|
	  value = value.to_i
	end
      end
      config[name] = array?(name) ? values : values[0]
    end
  end

  ## Load Postfix-style(?) config file
  def load_stream(stream, &filter)
    config = Hash.new

    stream.
      read.
      sub(/^\s*#.*\n/, '').
      scan(/^([\w]+)[ \t]*=(.*\n(?:[ \t]+.*\n)*)/) do |name, value|
	name, value = filter.call(name, value) if filter

	name = name.to_sym
	unless @attrs.key?(name)
	  raise NameError, "unknown option name: #{name}"
	end

	values = array?(name) ? value.strip.split(/\s+/) : [value.strip]
	case
	when type?(name, TYPE_BOOL)
	  values.map! do |value|
	    value = value.match(/^(yes|true|1)$/) ? true : false
	  end
	when type?(name, TYPE_INTEGER)
	  values.map! do |value|
	    value = value.to_i
	  end
	end
	config[name] = array?(name) ? values : values[0]
      end

    return config
  end
end

end ## module SmbSearch

