module RdfContext
  # An RDF Triple, or statement.
  #
  # Statements are composed of _subjects_, _predicates_ and _objects_.
  class Triple
    attr_accessor :subject, :object, :predicate

    ##
    # Creates a new triple directly from the intended subject, predicate, and object.
    #
    # Any or all of _subject_, _predicate_ or _object_ may be nil, to create a triple patern.
    # A patern may not be added to a graph.
    #
    # ==== Example
    #   Triple.new(BNode.new, URIRef.new("http://xmlns.com/foaf/0.1/knows"), BNode.new) # => results in the creation of a new triple and returns it
    #
    # @param [URIRef, BNode] subject:: the subject of the triple
    # @param [URIRef] predicate:: the predicate of the triple
    # @param [URIRef, BNode, Literal, TypedLiteral] object:: the object of the triple
    # @return [Triple]:: An array of the triples (leaky abstraction? consider returning the graph instead)
    # @raise [Error]:: Checks parameter types and raises if they are incorrect.
    #
    # @author Tom Morris
    def initialize (subject, predicate, object)
      @subject   = self.class.coerce_subject(subject)
      @predicate = self.class.coerce_predicate(predicate)
      @object    = self.class.coerce_object(object)
      @patern = subject.nil? || predicate.nil? || object.nil?
    end

    def is_patern?
      @patern
    end
    
    # Serialize Triple to N-Triples
    def to_ntriples
      raise RdfException.new("Can't serialize patern triple") if is_patern?
      @subject.to_ntriples + " " + @predicate.to_ntriples + " " + @object.to_ntriples + " ."
    end
    
    def to_s; self.to_ntriples; end
    
    def inspect
      [@subject, @predicate, @object, @patern].inspect
    end

    # Is the predicate of this statment rdf:type?
    def is_type?
      @predicate.to_s == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
    end

    # Two triples are equal if their of their subjects, predicates and objects are equal.
    # Or self or other is a patern and subject, predicate, object matches
    def eql? (other)
      other.is_a?(Triple) &&
      (other.subject == self.subject || other.subject.nil? || self.subject.nil?) &&
      (other.predicate == self.predicate || other.predicate.nil? || self.predicate.nil?) &&
      (other.object == self.object || other.object.nil? || self.object.nil?)
    end

    alias_method :==, :eql?

    # Clone triple, keeping references to literals and URIRefs, but cloning BNodes
    def clone
      raise RdfException.new("Can't clone patern triple") if is_patern?
      s = subject.is_a?(BNode) ? subject.clone : subject
      p = predicate.is_a?(BNode) ? predicate.clone : predicate
      o = object.is_a?(BNode) ? object.clone : object
      Triple.new(subject, predicate, object)
    end
    
    # For indexes
    def hash
      [subject, predicate, object].hash
    end
    
    protected

    # Coerce a subject to the appropriate RdfContext type.
    # 
    # @param[URI, URIRef, String] subject:: If a String looks like a URI, a URI is created, otherwise a BNode.
    # @raise[InvalidSubject]:: If subject can't be intuited.
    def self.coerce_subject(subject)
      case subject
      when Addressable::URI
        URIRef.new(subject.to_s)
      when URIRef, BNode
        subject
      when nil
        subject
      when /^\w+:\/\/\S+/ # does it smell like a URI?
        URIRef.new subject
      else
        raise InvalidSubject, "Subject is not of a known class (#{subject.class}: #{subject.inspect})"
      end
    end

    # Coerce a predicate to the appropriate RdfContext type.
    # 
    # @param[URI, URIRef, String] subject:: If a String looks like a URI, a URI is created
    # @raise[InvalidSubject]:: If subject can't be predicate.
    def self.coerce_predicate(predicate)
      case predicate
      when Addressable::URI
        URIRef.new(predicate.to_s)
      when URIRef
        predicate
      when String
        URIRef.new predicate
      when nil
        predicate
      else
        raise InvalidPredicate, "Predicate should be a URI"
      end
    rescue ParserException => e
      raise InvalidPredicate, "Couldn't make a URIRef: #{e.message}"
    end

    # Coerce a object to the appropriate RdfContext type.
    # 
    # @param[URI, URIRef, String, Integer, Float, BNode, Literal] object:: If a String looks like a URI, a URI is created, otherwise an untyped Literal.
    # @raise[InvalidSubject]:: If subject can't be predicate.
    def self.coerce_object(object)
      case object
      when Addressable::URI
        URIRef.new(object.to_s)
      when String
        if object.to_s =~ /^\w+:\/\/\S+/ # does it smell like a URI?
          URIRef.new(object.to_s)
        else
          Literal.untyped(object)
        end
      when Integer, Float
        Literal.build_from(object)
      when URIRef, BNode, Literal
        object
      when nil, regexp
        object
      else
        raise InvalidObject, "#{object.class}: #{object.inspect} is not a valid object"
      end
    end
  end
end