module IniParse
  # Represents a collection of lines in an INI document.
  #
  # LineCollection acts a bit like an Array/Hash hybrid, allowing arbitrary
  # lines to be added to the collection, but also indexes the keys of Section
  # and Option lines to enable O(1) lookup via LineCollection#[].
  #
  # The lines instances are stored in an array, +@lines+, while the index of
  # each Section/Option is held in a Hash, +@indicies+, keyed with the
  # Section/Option#key value (see LineCollection#[]=).
  #
  module LineCollection
    include Enumerable

    def initialize
      @lines    = []
      @indicies = {}
    end

    # Retrive a value identified by +key+.
    def [](key)
      has_key?(key) ? @lines[ @indicies[key] ] : nil
    end

    # Set a +value+ identified by +key+.
    #
    # If a value with the given key already exists, the value will be replaced
    # with the new one, with the new value taking the position of the old.
    #
    def []=(key, value)
      key = key.to_s

      if has_key?(key)
        @lines[ @indicies[key] ] = value
      else
        @lines << value
        @indicies[key] = @lines.length - 1
      end
    end

    # Appends a line to the collection.
    #
    # Note that if you pass a line with a key already represented in the
    # collection, the old item will be replaced.
    #
    def <<(line)
      line.blank? ? (@lines << line) : (self[line.key] = line) ; self
    end

    alias_method :push, :<<

    # Enumerates through the collection.
    #
    # By default #each does not yield blank and comment lines.
    #
    # ==== Parameters
    # include_blank<Boolean>:: Include blank/comment lines?
    #
    def each(include_blank = false)
      return enum_for(:each, include_blank) unless block_given?

      @lines.each do |line|
        if include_blank || ! (line.is_a?(Array) ? line.empty? : line.blank?)
          yield(line)
        end
      end
    end

    # Removes the value identified by +key+.
    def delete(key)
      key = key.key if key.respond_to?(:key)

      unless (idx = @indicies[key]).nil?
        @indicies.delete(key)
        @indicies.each { |k,v| @indicies[k] = v -= 1 if v > idx }
        @lines.delete_at(idx)
      end
    end

    # Returns whether +key+ is in the collection.
    def has_key?(*args)
      @indicies.has_key?(*args)
    end

    # Return an array containing the keys for the lines added to this
    # collection.
    def keys
      map { |line| line.key }
    end

    # Returns this collection as an array. Includes blank and comment lines.
    def to_a
      @lines.dup
    end

    # Returns this collection as a hash. Does not contain blank and comment
    # lines.
    def to_hash
      Hash[ *(map { |line| [line.key, line] }).flatten ]
    end

    alias_method :to_h, :to_hash
  end

  # A implementation of LineCollection used for storing (mostly) Option
  # instances contained within a Section.
  #
  # Since it is assumed that an INI document will only represent a section
  # once, if SectionCollection encounters a Section key already held in the
  # collection, the existing section is merged with the new one (see
  # IniParse::Lines::Section#merge!).
  class SectionCollection
    include LineCollection

    def <<(line)
      if line.kind_of?(IniParse::Lines::Option) || (has_key?('__anonymous__') && (line.blank? || line.kind_of?(IniParse::Lines::Comment)))
        option = line
        line   = IniParse::Lines::AnonymousSection.new

        line.lines << option if option
      end

      if line.blank? || (! has_key?(line.key))
        super # Adding a new section, comment or blank line.
      else
        self[line.key].merge!(line)
      end

      self
    end
  end

  # A implementation of LineCollection used for storing (mostly) Option
  # instances contained within a Section.
  #
  # Whenever OptionCollection encounters an Option key already held in the
  # collection, it treats it as a duplicate. This means that instead of
  # overwriting the existing value, the value is changed to an array
  # containing the previous _and_ the new Option instances.
  class OptionCollection
    include LineCollection

    # Appends a line to the collection.
    #
    # If you push an Option with a key already represented in the collection,
    # the previous Option will not be overwritten, but treated as a duplicate.
    #
    # ==== Parameters
    # line<IniParse::LineType::Line>:: The line to be added to this section.
    #
    def <<(line)
      if line.kind_of?(IniParse::Lines::Section)
        raise IniParse::LineNotAllowed,
          "You can't add a Section to an OptionCollection."
      end

      if line.blank? || (! has_key?(line.key))
        super # Adding a new option, comment or blank line.
      else
        self[line.key] = [self[line.key], line].flatten
      end

      self
    end

    def each(*args)
      return enum_for(:each, *args) unless block_given?

      super(*args) do |value|
        if value.is_a?(Array)
          value.each { |item| yield(item) }
        else
          yield value
        end
      end
    end

    # Return an array containing the keys for the lines added to this
    # collection.
    def keys
      map(&:key).uniq
    end
  end
end
