# Defines the relations between ActiveRepository objects.
#
# Author::    Caio Torres  (mailto:efreesen@gmail.com)
# License::   GPL


module ActiveRepository
  module Associations
    def self.included(base)
      base.extend Methods
    end

    module Methods
      # Defines "has many" type relation between ActiveRepository objects
      def has_many(association_id, options = {})
        define_method(association_id) do
          options = {
            class_name:  association_id.to_s.classify,
            foreign_key: self.class.to_s.foreign_key
          }.merge(options)

          foreign_key = options[:foreign_key]
          klass = options[:class_name].constantize

          klass.where(foreign_key => id)
        end
      end

      # Defines "has one" type relation between ActiveRepository objects
      def has_one(association_id, options = {})
        options = {
          class_name:  association_id.to_s.classify,
          foreign_key: self.to_s.foreign_key
        }.merge(options)

        has_one_methods(association_id, options)
      end

      # Defines "belongs to" type relation between ActiveRepository objects
      def belongs_to(association_id, options = {})
        options = {
          class_name:  association_id.to_s.classify,
          foreign_key: association_id.to_s.foreign_key
        }.merge(options)

        field options[:foreign_key].to_sym

        belongs_to_methods(association_id, options)
      end

    private
      def has_one_methods(association_id, options)
        define_has_one_method(association_id, options)
        define_has_one_setter(association_id, options)
        define_has_one_create(association_id, options)
      end

      def belongs_to_methods(association_id, options)
        define_belongs_to_method(association_id, options)
        define_belongs_to_setter(association_id, options)
        define_belongs_to_create(association_id, options)
      end

      def define_has_one_method(association_id, options)
        define_method(association_id) do
          foreign_key = options[:foreign_key]
          klass       = options[:class_name].constantize

          klass.where(foreign_key => self.id).first
        end
      end

      def define_has_one_setter(association_id, options)
        define_method("#{association_id}=") do |object|
          primary_key = self.send(self.class.primary_key)
          foreign_key = options[:foreign_key]
          association = self.send(association_id)

          association.update_attribute(foreign_key, nil) if association

          object.update_attribute(foreign_key, primary_key) if object
        end
      end

      def define_has_one_create(association_id, options)
        define_method("create_#{association_id}") do |attributes|
          primary_key = self.send(self.class.primary_key)
          foreign_key = self.class.to_s.foreign_key
          association = self.send(association_id)
          klass       = options[:class_name].constantize

          association.update_attribute(foreign_key, nil) if 
          self.send("#{association_id}=", nil)

          klass.create(attributes.merge(foreign_key => primary_key))
        end
      end

      def define_belongs_to_method(association_id, options)
        define_method(association_id) do
          klass = options[:class_name].constantize
          foreign_key = send(options[:foreign_key])

          klass.where(klass.primary_key => foreign_key).first
        end
      end

      def define_belongs_to_setter(association_id, options)
        define_method("#{association_id}=") do |new_value|
          attributes.delete(association_id.to_sym)
          send("#{options[:foreign_key]}=", (new_value.try(:id) ? new_value.id : new_value))
        end
      end

      def define_belongs_to_create(association_id, options)
        define_method("create_#{association_id}") do |attributes|
          klass = options[:class_name].constantize

          object = klass.create(attributes)

          self.update_attribute(options[:foreign_key], object.id)

          object
        end
      end

    end
  end
end
