#
# == config/config
#
# Revision:: $Id$
#

module Config

  class ConfigError < StandardError
    attr_reader :list
    
    def initialize(message, list, file_path)
      super(message)
      @list = list
      @file_path = file_path
    end

    def to_str
      s = super.to_str
      s += ": #{@file_path}" if @file_path != nil
      @list.each do |e|
        s += "#{e.error_line}: #{e.message}\n"
      end
      #s.chop!
      return s
    end
  end #ConfigError#

  #
  # 設定ファイルの中身がおかしかった場合のエラー。
  #
  class ConfigFormatError < ConfigError
    attr_reader :error_line
    
    # コンストラクタ
    # === ARGS
    # message:: エラーメッセージ
    # line:: エラーとなった設定ストリームの行番号
    def initialize(message, line)
      super(message)
      @error_line = line
    end

  end #ConfigFormatError#

  module Validator
    attr_accessor :file_path, :line
    
    def initialize_validator
      @line = nil
    end

    def validate_key_value
    end
  end

  #
  # 何もしないValidator
  # 
  class NullValidator
    include Validator
    
    def initialize
      initialize_validator
    end
  end

  #
  # 設定ファイルを読み込むクラス。
  # === CONFIGURATION FILE FORMAT
  #   # comment
  #   key1 = value1
  #   key2=value2
  #   ...
  #
  # === EXAMPLE 1
  #   file = File.open("abc.conf", "r")
  #   config = ConfigReader.read(file)
  #   p config["key1"]
  #
  class ConfigReader
    # ファイルを検索するディレクトリ
    SEARCH_DIRS = [ "." ]

    attr_reader :search_path, :as_hash

    # 指定された設定ファイル名のフルパスを取得する。
    # === ARGS
    # file_name:: 設定ファイルの名前
    # === RETURNS
    # ファイル名のフルパス
    def self.resolve_path(file_name)
      resolved_path = nil
      SEARCH_DIRS.each do |dir|
        path = File.join(dir, file_name)
        if FileTest.exist?(path) \
          && !FileTest.directory?(path)
          resolved_path = path
          break
        end
      end
      return resolved_path
    end

    # 引数のFileオブジェクトまたはファイルパスの文字列から設定ファイルを読み込み、
    # その内容を格納しているオブジェクト(HashまたはOpenStruct)を返す。
    # === ARGS
    # input:: Fileオブジェクトまたは設定ファイルパスの文字列
    # as_hash:: 戻り値をHashで返すかどうかのフラグ。
    #           Hashで返す場合は+true+。OpenStructで返す場合は+false+。
    # === RETURNS
    # 設定ファイルの内容を格納しているオブジェクト(HashまたはOpenStruct)
    def self.parse(input, as_hash = true)
      return ConfigReader.new(as_hash).parse(input)
    end

    # コンストラクタ
    # === ARGS
    # as_hash:: readメソッドの戻り値をHashで返すかどうかのフラグ。
    #           Hashで返す場合は+true+。OpenStructで返す場合は+false+。
    # validator:: 設定ファイルを検証するバリデータ
    def initialize(as_hash = true, validator = nil)
      @as_hash = as_hash
      @validator = validator
      @validator ||= NullValidator.new
    end

    # 引数のFileオブジェクトまたはファイルパスの文字列から設定ファイルを読み込み、
    # その内容を格納しているオブジェクト(HashまたはOpenStruct)を返す。
    # === ARGS
    # input:: Fileオブジェクトまたは設定ファイルパスの文字列
    # === RETURNS
    # 設定ファイルの内容を格納しているオブジェクト(HashまたはOpenStruct)
    def parse(input)
      case input
      when IO
        return read_from_io(input)
      when String
        return parse_by_path(input)
      else
        raise(ArgumentError, "arg1 must be an instance of File or String.")
      end
    end

    # 引数のIOより設定を読み込み、その内容を格納するオブジェクト
    # (HashまたはOpenStruct)を返す。
    # === ARGS
    # io:: 設定情報を格納しているIOオブジェクト
    # === RETURNS
    # 設定ファイルの内容を格納しているオブジェクト(HashまたはOpenStruct)
    # === RAISES
    # ConfigError:: 設定ファイルの内容が不正な場合
    # IOError:: 設定ファイルの読み込みに失敗した場合
    def read_from_io(io)
      config = Hash.new

      errors = Array.new
      count = 0
      while line = io.gets
        count += 1
        # コメント行か改行のみの行は無視
        if /^#/ =~ line \
          || /^(\r\n|\n)$/ =~ line
          next
        end
        
        @validator.line = count
        
        array = line.split("=", 2)
        key = array[0].strip
        value = array[1].strip
        
        begin
          if @validator.respond_to?("validate_key_value")
            @validator.validate_key_value(key, value)
          end
        rescue ConfigFormatError => e
          errors << e
          next
        end

        config.store(key, value)
      end
      
      unless errors.empty?
        file_path = ""
        file_path = io.path if io.instance_of?(File)
        raise(ConfigError.new("Errors occurred while reading configuration.",
                              errors, file_path))
      end

      unless @as_hash
        require 'ostruct'
        return OpenStruct.new(config)
      end
      
      return config
    end

    # 引数のファイルパスより設定ファイルを読み込み、
    # その内容を格納しているオブジェクト(HashまたはOpenStruct)を返す。
    # === ARGS
    # file:: 設定ファイルであるFileオブジェクト
    # === RETURNS
    # 設定ファイルの内容を格納しているオブジェクト(HashまたはOpenStruct)
    # === RAISES
    # ConfigError:: 設定ファイルの内容が不正な場合
    # IOError:: 設定ファイルの読み込みに失敗した場合
    # === SEE ALSO
    # resolve_path
    def read_by_path(path)
      begin
        file = File.open(path, "r")
        return read_from_io(file)
      ensure
        file.close if file != nil
      end
    end
  end #ConfigReader#
end
