require 'eloqua/api/service'
require 'eloqua/helper/attribute_map'
require 'active_model'
require 'active_support/core_ext/hash'

module Eloqua

  class RemoteObject

    include ActiveModel::MassAssignmentSecurity
    include ActiveModel::Naming
    include ActiveModel::Validations
    include ActiveModel::Conversion
    include ActiveModel::AttributeMethods
    include ActiveSupport::Callbacks

    define_callbacks :save, :update, :create

    # Because we never absolutely know what attributes are defined
    # We do not use define_attribute_method for dirty meaning #{attr}_changed? will not work
    # instead use the private methods provided by dirty IE: attribute_changed?(:attr)
    include ActiveModel::Dirty
    include Eloqua::Helper::AttributeMap

    DIRTY_PRIVATE_METHODS = [:attribute_was, :attribute_changed?, :attribute_change]
    DIRTY_PRIVATE_METHODS.each {|method| public method }

    class_attribute :primary_key, :remote_type, :attribute_types, :remote_group

    attr_reader :attributes

    self.attribute_types = {}.with_indifferent_access

    self.primary_key = 'id'
    self.remote_type = nil

    Eloqua.delegate_with_args(
        self, Eloqua::Api::Service, Eloqua::Api::Service.group_methods, [:remote_group]
    )

    Eloqua.delegate_with_args(
        self, Api::Service, Api::Service.group_type_methods, [:remote_group, :remote_type]
    )
    Eloqua.delegate_with_args(
        self, Api::Service, Api::Service.type_methods, [:remote_type]
    )

    delegate :api, :to => self

    # If the remote flag is set to :remote (or true) the object
    # assumes that the attributes are from eloqua directly in their format (IE: C_EmailAddress)
    # it will then format them to a more ruby-ish key (:email_address) and then store the original name
    # This means if you do not have a #map for the object when you are creating it for the first time
    # the object cannot determine the original eloqua name
    def initialize(attr = {}, remote = false)
      @instance_reverse_keys = attribute_map_reverse.clone
      if(remote)
        @_persisted = true
        attr = map_attributes(attr)
      end
      @attributes = convert_attribute_values(attr).with_indifferent_access
      if(@attributes.has_key?(primary_key) && @attributes[primary_key])
        @_persisted = true
      end
    end

    def reload
      if(persisted?)
        attr = self.class.find_object(id)
        attr = map_attributes(attr)
        attr = convert_attribute_values(attr)
        @attributes.update(attr)
        changed_attributes.update({}) if changed_attributes
        previous_changes.update({}) if previous_changes
        true
      end
    end

    def persisted?
      @_persisted ||= false
    end

    def convert_attribute_values(attributes, convert_type = :import)
      attributes = attributes.clone
      attributes.each do |key, value|
        attributes[key] = self.send(attribute_types[key][convert_type], key, value) if(attribute_types.has_key?(key))
      end
      attributes
    end


    private :map_attributes, :reverse_map_attributes

    # Persistence

    def create
      run_callbacks :create do
        attrs = convert_attribute_values(attributes, :export)
        attrs = reverse_map_attributes(attrs)
        result = self.class.create_object(attrs)
        if(result)
          @_persisted = true
          write_attribute(:id, result[:id])
          true
        else
          false
        end
      end
    end

    def update
      run_callbacks :update do
        update_attributes = changed.inject({}) do |map, attr|
          map[attr] = send(attr.to_sym)
          map
        end
        attrs = convert_attribute_values(update_attributes, :export)
        attrs = reverse_map_attributes(attrs)
        self.class.update_object(self.attributes[primary_key].to_i, attrs)
      end
    end

    def save(options = {})
      if(valid?)
        run_callbacks :save do
          (persisted?) ? update : create
        end
        true
      else
        false
      end
    end

    # For factory girl
    alias_method :save!, :save

    # Updates the attributes in the record with given
    # hash and then saves the object.
    #
    # By default uses assignment security provided by ActiveModel.
    # by using ignore_security you can turn this off
    #
    # @param [Hash] attributes to write
    # @param [Boolean] when true ignores assignment security
    # @return [Boolean] Result of the save
    def update_attributes(attrs, ignore_security = false)
      attrs = sanitize_for_mass_assignment(attrs) unless ignore_security
      attrs.each do |key, value|
        write_attribute(key, value)
      end
      save
    end

    # Magic

    # Monkey Patch. Rails uses a normal array for changed_attributes and
    # relys on method missing to provide the same type all the time
    def changed_attributes
      @changed_attributes ||= {}.with_indifferent_access
    end

    def read_attribute(attr)
      attributes[attr]
    end

    def write_attribute(attr, value)
      attribute_will_change!(attr) unless read_attribute(attr) == value
      attributes[attr] = value
    end

    def is_attribute_method?(method)
      attr = method.to_s.gsub(/\=$/, '')
      if(attributes.has_key?(attr) || attribute_map_reverse.has_key?(attr))
        attr_type = (method.to_s =~ /\=$/)? :write : :read
      else
        false
      end
    end

    def id
      read_attribute(:id)
    end

    def method_missing(method, *args)
      attr_method = is_attribute_method?(method)
      attr = method.to_s.gsub(/\=$/, '')
      if(attr_method)
        case attr_method
          when :write then write_attribute(attr, *args)
          when :read then read_attribute(attr)
        end
      else
        super
      end
    end

    def respond_to?(method, *args)
      if(is_attribute_method?(method))
        true
      else
        super
      end
    end

    # Column type setting

    protected

    def export_boolean_checkbox(attr, value)
      if(!!value)
        'Yes'
      else
        'No'
      end
    end

    def import_boolean_checkbox(attr, value)
      if(value =~ /yes/i)
        value = true
      elsif(value =~ /no/i)
        value = false
      end
      value
    end

    class << self

      # Attribute types

      def attr_type_hash(name)
        {
          :type => name.to_sym,
          :import => "import_#{name}".to_sym,
          :export => "export_#{name}".to_sym
        }
      end

      def attr_checkbox(*attrs)
        options = attrs.extract_options!
        attrs.each do |column|
          attribute_types[column] = attr_type_hash(:boolean_checkbox)
        end
      end

      def api
        Eloqua::Api::Service
      end

      def find(id)
        result = find_object(id)
        if(result)
          self.new(result, :remote)
        else
          result
        end
      end

      [:save, :update, :create].each do |callback_type|
        [:before, :after].each do |callback_state|
          module_eval(<<-RUBY, __FILE__, (__LINE__ - 2))
            def #{callback_state}_#{callback_type}(*args, &block)
              args.unshift(:#{callback_state})
              args.unshift(:#{callback_type})
              set_callback(*args, &block)
            end
          RUBY
        end
      end

    end

  end

end