require 'active_support'
require 'active_support/string_inquirer'
require 'old_api_resource/associations/relation_scope'
require 'old_api_resource/associations/resource_scope'
require 'old_api_resource/associations/multi_object_proxy'
require 'old_api_resource/associations/single_object_proxy'
require 'old_api_resource/associations/related_object_hash'

module OldApiResource
  
  module Associations
    extend ActiveSupport::Concern
    
    ASSOCIATION_TYPES = [:belongs_to, :has_one, :has_many]

    included do 
    
      class_inheritable_accessor :related_objects

      # Hash to hold onto the definitions of the related objects
      self.related_objects = RelatedObjectHash.new({
        :belongs_to => RelatedObjectHash.new,
        :has_one => RelatedObjectHash.new,
        :has_many => RelatedObjectHash.new,
        :scope => RelatedObjectHash.new
      })
    end


    module ClassMethods
      
      # Define the methods for creating and testing for associations, unfortunately
      # scopes are different enough to require different methods :(
      OldApiResource::Associations::ASSOCIATION_TYPES.each do |assoc|
        self.module_eval <<-EOE, __FILE__, __LINE__ + 1
          def #{assoc}(*args)
            options = args.extract_options!
            # Raise an error if we have multiple args and options
            raise "Invalid arguments to #{assoc}" unless options.blank? || args.length == 1
            args.each do |arg|
              klass_name = (options[:class_name] ? options[:class_name].to_s.classify : arg.to_s.classify)
              # add this to any descendants - the other methods etc are handled by inheritance
              ([self] + self.descendants).each do |klass|
                klass.related_objects[:#{assoc}][arg.to_sym] = klass_name
              end
              # We need to define reader and writer methods here
              define_association_as_attribute(:#{assoc}, arg)
            end
          end
          
          def #{assoc}?(name)
            return self.related_objects[:#{assoc}][name.to_s.pluralize.to_sym].present? || self.related_objects[:#{assoc}][name.to_s.singularize.to_sym].present?
          end
          
          def #{assoc}_class_name(name)
            raise "No such" + :#{assoc}.to_s + " association on #{name}" unless self.#{assoc}?(name)
            return self.find_namespaced_class_name(self.related_objects[:#{assoc}][name.to_sym])
          end            

        EOE
      end
      
      def scopes
        return self.related_objects[:scope]
      end
      
      def scope(name, hsh)
        raise ArgumentError, "Expecting an attributes hash given #{hsh.inspect}" unless hsh.is_a?(Hash)
        self.related_objects[:scope][name.to_sym] = hsh
        # we also need to define a class method for each scope
        self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
          def #{name}(opts = {})
            return OldApiResource::Associations::ResourceScope.new(self, :#{name}, opts)
          end
        EOE
      end
      
      def scope?(name)
        self.related_objects[:scope][name.to_sym].present?
      end
      
      def scope_attributes(name)
        raise "No such scope #{name}" unless self.scope?(name)
        self.related_objects[:scope][name.to_sym]
      end
      
      def association?(assoc)
        self.related_objects.any? do |key, value|
          next if key.to_s == "scope"
          value.detect { |k,v| k.to_sym == assoc.to_sym }
        end
      end
      
      def association_class_name(assoc)
        raise ArgumentError, "#{assoc} is not a valid association of #{self}" unless self.association?(assoc)
        result = self.related_objects.detect do |key,value|
          ret = value.detect{|k,v| k.to_sym == assoc.to_sym }
          return self.find_namespaced_class_name(ret[1]) if ret
        end
      end
      
      def clear_associations
        self.related_objects.each do |_, val|
          val.clear
        end
      end
      
      protected
        def define_association_as_attribute(assoc_type, assoc_name)
          define_attributes assoc_name
          case assoc_type.to_sym
            when :has_many
              self.class_eval <<-EOE, __FILE__, __LINE__ + 1
                def #{assoc_name}
                  self.attributes[:#{assoc_name}] ||= MultiObjectProxy.new(self.association_class_name('#{assoc_name}'), nil)
                end
              EOE
            else
              self.class_eval <<-EOE, __FILE__, __LINE__ + 1
                def #{assoc_name}
                  self.attributes[:#{assoc_name}] ||= SingleObjectProxy.new(self.association_class_name('#{assoc_name}'), nil)
                end
              EOE
          end
          # Always define the setter the same
          self.class_eval <<-EOE, __FILE__, __LINE__ + 1
            def #{assoc_name}=(val)
              #{assoc_name}_will_change! unless self.#{assoc_name}.internal_object == val
              self.#{assoc_name}.internal_object = val
            end
          EOE
        end
        
        def find_namespaced_class_name(klass)
          # return the name if it is itself namespaced
          return klass if klass =~ /::/
          ancestors = self.name.split("::")
          if ancestors.size > 1
            receiver = Object
            namespaces = ancestors[0..-2].collect do |mod|
              receiver = receiver.const_get(mod)
            end
            if namespace = namespaces.reverse.detect{|ns| ns.const_defined?(klass, false)}
              return namespace.const_get(klass).name
            end
          end

          return klass
        end
      
    end
    
    module InstanceMethods
      # For convenience we will define the methods for testing for the existence of an association
      # and getting the class for an association as instance methods too to avoid tons of self.class calls
      OldApiResource::Associations::ASSOCIATION_TYPES.each do |assoc|
        module_eval <<-EOE, __FILE__, __LINE__ + 1
          def #{assoc}?(name)
            return self.class.#{assoc}?(name)
          end
          
          def #{assoc}_class_name(name)
            return self.class.#{assoc}_class_name(name)
          end
        EOE
      end
      
      def association?(assoc)
        self.class.association?(assoc)
      end
      
      def association_class_name(assoc)
        self.class.association_class_name(assoc)
      end
      
      def scopes
        return self.class.scopes
      end
      
      def scope?(name)
        return self.class.scope?(name)
      end
      
      def scope_attributes(name)
        return self.class.scope_attributes(name)
      end
      
    end

  end
  
end