module RDF
  ##
  # An RDF repository.
  #
  # @example Creating a transient in-memory repository
  #   repository = RDF::Repository.new
  #
  # @example Checking whether a repository is readable/writable
  #   repository.readable?
  #   repository.writable?
  #
  # @example Checking whether a repository is persistent or transient
  #   repository.persistent?
  #   repository.transient?
  #
  # @example Checking whether a repository is empty
  #   repository.empty?
  #
  # @example Checking how many statements a repository contains
  #   repository.count
  #
  # @example Checking whether a repository contains a specific statement
  #   repository.has_statement?(statement)
  #
  # @example Enumerating statements in a repository
  #   repository.each_statement { |statement| statement.inspect! }
  #
  # @example Inserting statements into a repository
  #   repository.insert(*statements)
  #   repository.insert(statement)
  #   repository.insert([subject, predicate, object])
  #   repository << statement
  #   repository << [subject, predicate, object]
  #
  # @example Deleting statements from a repository
  #   repository.delete(*statements)
  #   repository.delete(statement)
  #   repository.delete([subject, predicate, object])
  #
  # @example Deleting all statements from a repository
  #   repository.clear!
  #
  class Repository
    include RDF::Enumerable

    # @return [URI]
    attr_reader :uri

    # @return [String]
    attr_reader :title

    ##
    # Loads an N-Triples file as a transient in-memory repository.
    #
    # @param  [String] filename
    # @yield  [repository]
    # @yieldparam [Repository]
    # @return [void]
    def self.load(filename, options = {}, &block)
      self.new(options) do |repository|
        repository.load(filename, options)

        if block_given?
          case block.arity
            when 1 then block.call(repository)
            else repository.instance_eval(&block)
          end
        end
      end
    end

    ##
    # @yield  [repository]
    # @yieldparam [Repository]
    def initialize(options = {}, &block)
      @uri   = options.delete(:uri)
      @title = options.delete(:title)
      @data, @options = [], options

      if block_given?
        case block.arity
          when 1 then block.call(self)
          else instance_eval(&block)
        end
      end
    end

    ##
    # Returns `true` if the repository is transient.
    #
    # @return [Boolean]
    # @see    #persistent?
    def transient?
      !persistent?
    end

    ##
    # Returns `true` if the repository is persistent.
    #
    # @return [Boolean]
    # @see    #transient?
    # @abstract
    def persistent?
      false # NOTE: override this in any persistent subclasses
    end

    ##
    # Returns `true` if the repository is readable.
    #
    # @return [Boolean]
    def readable?
      true
    end

    ##
    # Returns `true` if the repository is mutable.
    #
    # @return [Boolean]
    # @see    #immutable?
    # @see    #immutable!
    def mutable?
      !immutable?
    end

    alias_method :writable?, :mutable?

    ##
    # Returns `true` if the repository is immutable.
    #
    # @return [Boolean]
    # @see    #mutable?
    # @see    #immutable!
    def immutable?
      @options[:mutable] == false
    end

    ##
    # Makes the repository contents immutable.
    #
    # @return [void]
    # @see    #mutable?
    # @see    #immutable?
    def immutable!
      @options[:mutable] = true
    end

    ##
    # Returns `true` if the repository contains no RDF statements.
    #
    # @return [Boolean]
    def empty?
      @data.empty?
    end

    ##
    # Returns the number of RDF statements in the repository.
    #
    # @return [Integer]
    def size
      @data.size
    end

    alias_method :count, :size

    ##
    # Returns `true` if this repository contains the given RDF `statement`.
    #
    # @param  [Statement] statement
    # @return [Boolean]
    def has_statement?(statement)
      @data.include?(statement)
    end

    alias_method :include?, :has_statement?

    ##
    # Enumerates each RDF statement in the repository.
    #
    # @yield  [statement]
    # @yieldparam [Statement]
    # @return [Enumerator]
    def each(&block)
      @data.each(&block)
    end

    ##
    # Queries the repository for RDF statements matching the given pattern.
    #
    # @param  [Query, Statement, Array(Value)] pattern
    # @yield  [statement]
    # @yieldparam [Statement]
    # @return [Array<Statement>, nil]
    def query(pattern, &block)
      raise TypeError.new("repository is not readable") unless readable?
      case pattern
        when Query
          pattern.execute(self, &block)
        when Array
          query(Statement.new(*pattern), &block)
        when Statement
          if block_given?
            # TODO: yield any found statements
            nil
          else
            find_all { |statement| pattern === statement }
          end
      end
    end

    ##
    # Loads RDF statements from the given N-Triples file into the repository.
    #
    # @param  [String]  filename
    # @return [Integer] the number of inserted RDF statements
    def load(filename, options = {})
      raise TypeError.new("repository is immutable") if immutable?
      count = 0
      Reader.open(filename, options) do |reader|
        reader.each_statement do |statement|
          insert_statement(statement)
          count += 1
        end
      end
      count
    end

    ##
    # Inserts an RDF statement into the repository.
    #
    # @param  [Statement, Array(Value), #to_a] statement
    # @return [Repository]
    def <<(statement)
      raise TypeError.new("repository is immutable") if immutable?
      case statement
        when Statement then insert_statement(statement)
        else insert_statement(Statement.new(*statement.to_a))
      end
      self
    end

    ##
    # Updates RDF statements in the repository.
    #
    # @param  [Array<Statement>] statements
    # @raise  [TypeError] if the repository is immutable
    # @return [Repository]
    def update(*statements)
      raise TypeError.new("repository is immutable") if immutable?
      statements.each do |statement|
        if (statement = create_statement(statement))
          delete([statement.subject, statement.predicate, nil])
          insert(statement) if statement.has_object?
        end
      end
    end

    ##
    # Inserts RDF statements into the repository.
    #
    # @param  [Array<Statement>] statements
    # @raise  [TypeError] if the repository is immutable
    # @return [Repository]
    def insert(*statements)
      raise TypeError.new("repository is immutable") if immutable?
      statements.each do |statement|
        if (statement = create_statement(statement)).valid?
          insert_statement(statement)
        else
          raise ArgumentError.new # FIXME
        end
      end
      self
    end

    ##
    # Deletes RDF statements from the repository.
    #
    # @param  [Array<Statement>] statements
    # @raise  [TypeError] if the repository is immutable
    # @return [Repository]
    def delete(*statements)
      raise TypeError.new("repository is immutable") if immutable?
      statements.each do |statement|
        if (statement = create_statement(statement)).valid?
          delete_statement(statement)
        else
          query(statement).each do |statement|
            delete_statement(statement)
          end
        end
      end
      self
    end

    ##
    # Deletes all RDF statements from this repository.
    #
    # @return [Repository]
    def clear
      @data.clear
      self
    end

    alias_method :clear!, :clear

    ##
    # Outputs a developer-friendly representation of this repository to
    # `stderr`.
    #
    # @return [void]
    def inspect!
      each_statement { |statement| statement.inspect! }
      nil
    end

    protected

      def insert_statement(statement)
        @data.push(statement) unless @data.include?(statement)
      end

      def delete_statement(statement)
        @data.delete(statement)
      end

      def create_statement(statement)
        case statement
          when Statement then statement
          when Hash      then Statement.new(statement)
          when Array     then Statement.new(*statement)
          else raise ArgumentError.new # FIXME
        end
      end

  end
end