require 'active_repository/associations'
require 'active_repository/uniqueness'
require 'active_repository/write_support'
require 'sql_query_executor'
require 'active_repository/finders'
require 'active_repository/writers'
require 'active_repository/adapters/persistence_adapter'
require 'active_repository/result_set'

module ActiveRepository

  # Base class for ActiveRepository gem.
  # Extends it in order to use it.
  # 
  # == Options
  #
  # There are 2 class attributes to help configure your ActiveRepository class:
  #
  #   * +class_model+: Use it to specify the class that is responsible for the
  #     persistence of the objects. Default is self, so it is always saving in
  #     memory by default.
  #
  #   * +save_in_memory+: Used to ignore the class_model attribute, you can use
  #     it in your test suite, this way all your tests will be saved in memory.
  #     Default is set to true so it saves in memory by default.
  #     
  #
  # == Examples
  #
  # Using ActiveHash to persist objects in memory:
  #
  #   class SaveInMemoryTest < ActiveRepository::Base
  #   end
  #
  # Using ActiveRecord/Mongoid to persist objects:
  #
  #    class SaveInORMOrODMTest < ActiveRepository::Base
  #      SaveInORMOrODMTest.persistence_class = ORMOrODMModelClass
  #      SaveInORMOrODMTest.save_in_memory = false
  #    end
  #
  # Author::    Caio Torres (mailto:efreesen@gmail.com)
  # License::   GPL
  class Base < ActiveHash::Base
    extend ActiveModel::Callbacks
    extend ActiveRepository::Finders
    extend ActiveRepository::Writers
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks
    include ActiveRepository::Associations
    include ActiveRepository::Writers::InstanceMethods

    class_attribute :model_class, :before_save_methods, :after_save_methods, :instance_writer => false
    class_attribute :before_create_methods, :after_create_methods, :instance_writer => false
    class_attribute :save_in_memory, :postfix, :instance_writer => true

    after_validation :set_timestamps

    # Returns all persisted objects
    def self.all
      (repository? ? super : PersistenceAdapter.all(self).map { |object| serialize!(object.attributes) })
    end

    def self.before_save(*methods, options)
      add_callbacks(__method__, methods, options)
    end

    def self.after_save(*methods, options)
      add_callbacks(__method__, methods, options)
    end

    # Constantize class name
    def self.constantize
      self.to_s.constantize
    end

    # Deletes all persisted objects
    def self.delete_all
      repository? ? super : PersistenceAdapter.delete_all(self)
    end

    # Checks the existence of a persisted object with the specified id
    def self.exists?(id)
      repository? ? find_by(id: id).present? : PersistenceAdapter.exists?(self, id)
    end

    def self.persistence_class
      return self if save_in_memory? || (postfix.nil? && self.model_class.nil?)
      return "#{self}#{postfix.classify}".constantize if postfix.present?
      self.model_class.to_s.constantize
    end

    def self.repository?
      self == persistence_class
    end

    # Returns the Class responsible for persisting the objects
    def self.get_model_class
      puts '[deprecation warning] This method is going to be deprecated, use "persistence_class" instead.'
      persistence_class
    end

    # Searches all objects that matches #field_name field with the #args value(s)
    def self.find_by(args)
      raise ArgumentError("Argument must be a Hash") unless args.is_a?(Hash)

      objects = where(args)

      objects.first
    end

    # Searches all objects that matches #field_name field with the #args value(s)
    def self.find_by!(args)
      object = find_by(args)

      raise ActiveHash::RecordNotFound unless object
      object
    end

    # Converts Persisted object(s) to it's ActiveRepository counterpart
    def self.serialize!(other)
      case other.class.to_s
      when "Hash", "ActiveSupport::HashWithIndifferentAccess" then self.new.serialize!(other)
      when "Array"                                            then other.map { |o| serialize!(o.attributes) }
      when "Moped::BSON::Document", "BSON::Document"          then self.new.serialize!(other)
      else self.new.serialize!(other.attributes)
      end
    end

    # Returns an array with the field names of the Class
    def self.serialized_attributes
      field_names.map &:to_s
    end

    def self.persistence_class=(value)
      self.model_class = value
    end

    # Sets the class attribute model_class, responsible to persist the ActiveRepository objects
    def self.set_model_class(value)
      puts '[deprecation warning] This method is going to be deprecated, use "persistence_class=" instead.'
      persistence_class = value
    end

    def self.save_in_memory?
      self.save_in_memory == nil ? true : self.save_in_memory
    end

    # Sets the class attribute save_in_memory, set it to true to ignore model_class attribute
    # and persist objects in memory
    def self.set_save_in_memory(value)
      puts '[deprecation warning] This method is going to be deprecated, use "save_in_memory=" instead.'
      self.save_in_memory = value
    end

    # Searches persisted objects that matches the criterias in the parameters.
    # Can be used in ActiveRecord/Mongoid way or in SQL like way.
    #
    # Example:
    #
    #   * RelatedClass.where(:name => "Peter")
    #   * RelatedClass.where("name = 'Peter'")
    def self.where(*args)
      raise ArgumentError.new("must pass at least one argument") if args.empty?

      result_set = ActiveRepository::ResultSet.new(self)

      result_set.where(args)

      # if repository?
      #   args = args.first if args.respond_to?(:size) && args.size == 1
      #   query_executor = SqlQueryExecutor::Base.new(all)
      #   query_executor.where(args)
      # else
      #   objects = PersistenceAdapter.where(self, sanitize_args(args)).map do |object|
      #     self.serialize!(object.attributes)
      #   end

      #   objects
      # end
    end

    def persistence_class
      self.class.persistence_class
    end

    def get_model_class
      puts '[deprecation warning] This method is going to be deprecated, use "persistence_class" instead.'
      self.class.persistence_class
    end

    # Persists the object using the class defined on the model_class attribute, if none defined it 
    # is saved in memory.
    def persist
      if self.valid?
        save_in_memory? ? save : self.convert.present?
      end
    end

    # Gathers the persisted object from database and updates self with it's attributes.
    def reload
      object = self.id.present? ? 
                 persistence_class.where(id: self.id).first_or_initialize : 
                 self

      serialize! object.attributes
    end

    def save(force=false)
      execute_callbacks(before_save_methods)
      result = true

      if self.class == persistence_class
        object = persistence_class.where(id: self.id).first_or_initialize

        result = if force || self.id.nil?
          self.id = nil if self.id.nil?
          super
        elsif self.valid?
          object.attributes = self.attributes.select{ |key, value| self.class.serialized_attributes.include?(key.to_s) }
          object.save(true)
        end
      else
        result = self.persist
      end

      execute_callbacks(after_save_methods)
      # (after_save_methods || []).each { |method| self.send(method) }

      result
    end

    # Updates attributes from self with the attributes from the parameters
    def serialize!(attributes)
      unless attributes.nil?
        attributes.each do |key, value|
          key = "id" if key == "_id"
          self.send("#{key}=", (value.dup rescue value))
        end
      end

      self.dup
    end

    protected
      # Find related object on the database and updates it with attributes in self, if it didn't
      # find it on database it creates a new one.
      def convert(attribute="id")
        klass = persistence_class
        object = klass.where(attribute.to_sym => self.send(attribute)).first

        object ||= persistence_class.new

        attributes = self.attributes.select{ |key, value| self.class.serialized_attributes.include?(key.to_s) }

        attributes.delete(:id)

        object.attributes = attributes

        object.save

        self.id = object.id

        object
      end

    private
      def self.add_callbacks(kind, methods, options)
        methods = methods.map { |method| {method: method, options: options} }
        current_callbacks = (self.send("#{kind}_methods") || [])
        self.send("#{kind}_methods=", (current_callbacks + methods)).flatten
      end
      private_class_method :set_callback

      def execute_callbacks(callbacks)
        Array(callbacks).each do |callback|
          method = callback[:method]
          options = callback[:options]

          if_option = !!options[:if].try(:call, self)
          else_option = options[:else].try(:call, self)

          if if_option || !(else_option.nil? ? true : else_option)
            self.send(method)
          end
        end
      end

      # Updates created_at and updated_at
      def set_timestamps
        if self.errors.empty?
          self.created_at = DateTime.now.utc if self.respond_to?(:created_at=) && self.created_at.nil?
          self.updated_at = DateTime.now.utc if self.respond_to?(:updated_at=)
        end
      end
  end
end
