module Spira
 module Resource

   ##
   # This module contains all class methods available to a declared Spira::Resource class.
   # {Spira::Resource} contains more information about Spira resources.
   #
   # @see Spira::Resource
   # @see Spira::Resource::InstanceMethods
   # @see Spira::Resource::DSL
    module ClassMethods

      ##
      # A symbol name for the repository this class is currently using.
      attr_reader :repository_name

      ##
      # The current repository for this class
      # 
      # @return [RDF::Repository, nil]
      # @private
      def repository
        name = @repository_name || :default
        Spira.repository(name)
      end

      ##
      # Get the current repository for this class, and raise a
      # Spira::NoRepositoryError if it is nil.
      #
      # @raise  [Spira::NoRepositoryError]
      # @return [RDF::Repository]
      # @private
      def repository_or_fail
        repository || (raise Spira::NoRepositoryError, "#{self} is configured to use #{@repository_name} as a repository, but it has not been set.")
      end

      ##
      # Create a new projection instance of this class for the given URI.  If a
      # class has a base_uri given, and the argument is not an `RDF::URI`, the
      # given identifier will be appended to the base URI.
      #
      # Spira does not have 'find' or 'create' functions.  As RDF identifiers
      # are globally unique, they all simply 'are'.
      #
      # On calling `for`, a new instance is created for the given URI.  The
      # first time access is attempted on a field, the repository will be
      # queried for existing attributes, which will be used for the given URI.
      # Underlying repositories are not accessed at the time of calling `for`.
      # 
      # A class with a base URI may still be projected for any URI, whether or
      # not it uses the given resource class' base URI.
      #
      # @raise [TypeError] if an RDF type is given in the attributes and one is
      # given in the attributes.  
      # @raise [ArgumentError] if a non-URI is given and the class does not
      # have a base URI.  
      # @overload for(uri, attributes = {})
      #   @param [RDF::URI] uri The URI to create an instance for
      #   @param [Hash{Symbol => Any}] attributes Initial attributes
      # @overload for(identifier, attributes = {})
      #   @param [Any] uri The identifier to append to the base URI for this class
      #   @param [Hash{Symbol => Any}] attributes Initial attributes
      # @return  [Spira::Resource] The newly created instance
      # @see http://rdf.rubyforge.org/RDF/URI.html
      def for(identifier, attributes = {})
        if !self.type.nil? && attributes[:type]
          raise TypeError, "#{self} has an RDF type, #{self.type}, and cannot accept one as an argument."
        end
        uri = id_for(identifier)
        self.new(uri, attributes)
      end

      ##
      # Creates a URI or RDF::Node based on a potential base_uri and string,
      # URI, or Node, or Addressable::URI.  If not a URI or Node, the given
      # identifier should be a string representing an absolute URI, or
      # something responding to to_s which can be appended to a base URI, which
      # this class must have.
      #
      # @param  [Any] Identifier
      # @return [RDF::URI, RDF::Node]
      # @raise  [ArgumentError] If this class cannot create an identifier from the given argument
      # @see http://rdf.rubyforge.org/RDF/URI.html
      def id_for(identifier)
        case 
          # Catches RDF::URI and implementing subclasses
          when identifier.respond_to?(:to_uri)
            identifier.to_uri
          # Catches RDF::Nodes
          when identifier.respond_to?(:node?) && identifier.node?
            identifier
          when identifier.is_a?(Addressable::URI)
            RDF::URI.new(identifier)
          # Treat identifier as a string, and create a URI out of it.
          else
            uri = RDF::URI.new(identifier.to_s)
            return uri if uri.absolute?
            raise ArgumentError, "Cannot create identifier for #{self} by String without base_uri; RDF::URI required" if self.base_uri.nil?
            separator = self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/'
            RDF::URI.new(self.base_uri.to_s + separator + identifier.to_s)
        end
      end


      ##
      # The number of URIs projectable as a given class in the repository.
      # This method is only valid for classes which declare a `type` with the
      # `type` method in the DSL.
      #
      # @raise  [Spira::NoTypeError] if the resource class does not have an RDF type declared
      # @return [Integer] the count
      # @see Spira::Resource::DSL
      def count
        raise Spira::NoTypeError, "Cannot count a #{self} without a reference type URI." if @type.nil?
        repository.query(:predicate => RDF.type, :object => @type).subjects.count
      end

      ##
      # Enumerate over all resources projectable as this class.  This method is
      # only valid for classes which declare a `type` with the `type` method in
      # the DSL.
      #
      # @raise  [Spira::NoTypeError] if the resource class does not have an RDF type declared
      # @overload each
      #   @yield [instance] A block to perform for each available projection of this class
      #   @yieldparam [self] instance
      #   @yieldreturn [Void]
      #   @return [Void]
      #
      # @overload each
      #   @return [Enumerator]
      # @see Spira::Resource::DSL
      def each(&block)
        raise Spira::NoTypeError, "Cannot count a #{self} without a reference type URI." if @type.nil?
        case block_given?
          when false
            enum_for(:each)
          else
            repository.query(:predicate => RDF.type, :object => @type).each_subject do |subject|
              block.call(self.for(subject))
            end
        end
      end


      ##
      # Returns true if the given property is a has_many property, false otherwise
      #
      # @return [true, false]
      def is_list?(property)
        @lists.has_key?(property)
      end

      ##
      # Handling inheritance
      #
      # @private
      def inherited(child)
        child.instance_eval do
          include Spira::Resource
        end
        # FIXME: This is clearly brittle and ugly.
        [:@base_uri, :@default_vocabulary, :@repository_name, :@type].each do |variable|
          value = instance_variable_get(variable).nil? ? nil : instance_variable_get(variable).dup
          child.instance_variable_set(variable, value)
        end
        [:@properties, :@lists, :@validators].each do |variable|
          if child.instance_variable_get(variable).nil?
            if instance_variable_get(variable).nil?
              child.instance_variable_set(variable, nil)
            else
              child.instance_variable_set(variable, instance_variable_get(variable).dup)
            end
          elsif !(instance_variable_get(variable).nil?)
            child.instance_variable_set(variable, instance_variable_get(variable).dup.merge(child.instance_variable_get(variable)))
          end
        end
      end

      ## 
      # Handling module inclusions
      #
      # @private
      def included(child)
        inherited(child)
      end

      ## 
      # The list of validation functions for this projection
      #
      # @return [Array<Symbol>]
      def validators
        @validators ||= []
      end

    end
  end
end