module RDF::RDFa
  ##
  # Context representation existing of a hash of terms, prefixes, a default vocabulary and a URI.
  #
  # Contexts are used for storing RDFa context representations. A representation is created
  # by serializing a context graph (typically also in RDFa, but may be in other representations).
  #
  # The class may be backed by an RDF::Repository, which will be used to retrieve a context graph
  # or to load into, if no such graph exists
  class Context
    # Prefix mappings defined in this context
    # @!attribute [r] prefixes
    # @return  [Hash{Symbol => RDF::URI}]
    attr_reader :prefixes

    # Term mappings defined in this context
    # @!attribute [r] terms
    # @return [Hash{Symbol => RDF::URI}]
    attr_reader :terms
    
    # Default URI defined for this vocabulary
    # @!attribute [r] vocabulary
    # @return [RDF::URI]
    attr_reader :vocabulary

    # URI defining this context
    # @!attribute [r] uri
    # @return [RDF::URI]
    attr_reader :uri
    
    ##
    # Initialize a new context from the given URI.
    #
    # Parses the context and places it in the repository and cache
    #
    # @param [RDF::URI, #to_s] uri URI of context to be represented
    # @yield [context]
    # @yieldparam [RDF::RDFa::Context] context
    # @yieldreturn [void] ignored
    # @return [RDF::RDFa::Context]
    def initialize(uri, options = {}, &block)
      @uri = RDF::URI.intern(uri)
      @prefixes = options.fetch(:prefixes, {})
      @terms = options.fetch(:terms, {})
      @vocabulary = options[:vocabulary]
      
      yield(self) if block_given?
      self
    end
    
    ##
    # @return [RDF::Util::Cache]
    # @private
    def self.cache
      require 'rdf/util/cache' unless defined?(::RDF::Util::Cache)
      @cache ||= begin
        RDF::Util::Cache.new(-1)
      end
    end

    ##
    # Repository used for saving contexts
    # @return [RDF::Repository]
    # @raise [RDF::RDFa::ContextError] if context does not support contexts
    def self.repository
      @repository ||= RDF::Repository.new(:title => "RDFa Contexts")
    end
    
    ##
    # Set repository used for saving contexts
    # @param [RDF::Repository] repo
    # @return [RDF::Repository]
    def self.repository=(repo)
      raise ContextError, "Context Repository must support context" unless repo.supports?(:context)
      @repository = repo
    end
    
    # Return a context faulting through the cache
    # @return [RDF::RDFa::Context]
    def self.find(uri)
      uri = RDF::URI.intern(uri)
      
      return cache[uri] unless cache[uri].nil?
      
      # Two part creation to prevent re-entrancy problems if p1 => p2 and p2 => p1
      # Return something to make the caller happy if we're re-entered
      cache[uri] = Struct.new(:prefixes, :terms, :vocabulary).new({}, {}, nil)
      # Now do the actual load
      cache[uri] = new(uri) do |context|
        STDERR.puts("process_context: retrieve context <#{uri}>") if RDF::RDFa.debug?
        Context.load(uri)
        context.parse(repository.query(:context => uri))
      end
    rescue Exception => e
      raise ContextError, "Context #{uri}: #{e.message}", e.backtrace
    end

    # Load context into repository
    def self.load(uri)
      uri = RDF::URI.intern(uri)
      repository.load(uri.to_s, :base_uri => uri, :context => uri) unless repository.has_context?(uri)
    end
    
    # @return [RDF::Repository]
    def repository
      Context.repository
    end
    
    ##
    # Defines the given named URI prefix for this context.
    #
    # @example Defining a URI prefix
    #   context.prefix :dc, RDF::URI('http://purl.org/dc/terms/')
    #
    # @example Returning a URI prefix
    #   context.prefix(:dc)    #=> RDF::URI('http://purl.org/dc/terms/')
    #
    # @param  [Symbol, #to_s]   name
    # @param  [RDF::URI, #to_s] uri
    # @return [RDF::URI]
    def prefix(name, uri = nil)
      name = name.to_s.empty? ? nil : (name.respond_to?(:to_sym) ? name.to_sym : name.to_s.to_sym)
      uri.nil? ? prefixes[name] : prefixes[name] = uri
    end

    ##
    # Defines the given named URI term for this context.
    #
    # @example Defining a URI term
    #   context.term :title, RDF::URI('http://purl.org/dc/terms/title')
    #
    # @example Returning a URI context
    #   context.term(:title)    #=> RDF::URI('http://purl.org/dc/terms/TITLE')
    #
    # @param  [Symbol, #to_s]   name
    # @param  [RDF::URI, #to_s] uri
    # @return [RDF::URI]
    def term(name, uri = nil)
      name = name.to_s.empty? ? nil : (name.respond_to?(:to_sym) ? name.to_sym : name.to_s.to_sym)
      uri.nil? ? terms[name] : terms[name] = uri
    end
    
    ##
    # Extract vocabulary, prefix mappings and terms from a enumerable object into an instance
    #
    # @param [RDF::Enumerable, Enumerator] enumerable
    # @return [void] ignored
    def parse(enumerable)
      STDERR.puts("process_context: parse context <#{uri}>") if RDF::RDFa.debug?
      resource_info = {}
      enumerable.each do |statement|
        res = resource_info[statement.subject] ||= {}
        next unless statement.object.is_a?(RDF::Literal)
        STDERR.puts("process_context: statement=#{statement.inspect}") if RDF::RDFa.debug?
        %w(uri term prefix vocabulary).each do |term|
          res[term] ||= statement.object.value if statement.predicate == RDF::RDFA[term]
        end
      end

      resource_info.values.each do |res|
        # If one of the objects is not a Literal or if there are additional rdfa:uri or rdfa:term
        # predicates sharing the same subject, no mapping is created.
        uri = res["uri"]
        term = res["term"]
        prefix = res["prefix"]
        vocab = res["vocabulary"]
        STDERR.puts("process_context: uri=#{uri.inspect}, term=#{term.inspect}, prefix=#{prefix.inspect}, vocabulary=#{vocab.inspect}") if RDF::RDFa.debug?

        @vocabulary = vocab if vocab
        
        # For every extracted triple that is the common subject of an rdfa:prefix and an rdfa:uri
        # predicate, create a mapping from the object literal of the rdfa:prefix predicate to the
        # object literal of the rdfa:uri predicate. Add or update this mapping in the local list of
        # URI mappings after transforming the 'prefix' component to lower-case.
        # For every extracted
        prefix(prefix.downcase, uri) if uri && prefix && prefix != "_"
      
        # triple that is the common subject of an rdfa:term and an rdfa:uri predicate, create a
        # mapping from the object literal of the rdfa:term predicate to the object literal of the
        # rdfa:uri predicate. Add or update this mapping in the local term mappings.
        term(term, uri) if term && uri
      end
    end
  end

  ##
  # The base class for RDF context errors.
  class ContextError < IOError; end
end

# Load cooked contexts
Dir.glob(File.join(File.expand_path(File.dirname(__FILE__)), 'context', '*')).each {|f| load f}