require 'simple_model/exceptions'
module SimpleModel
  module Attributes
    include ExtendCore
    extend ActiveSupport::Concern
    include ActiveModel::AttributeMethods 
    
    def initialize(*attrs)     
      attrs = attrs.extract_options! 
      set(attributes_with_for_init(attrs))
    end
    
    # Returns true if attribute has been initialized
    def initialized?(attr)
      attributes.key?(attr.to_sym)
    end
    
    def attributes
      @attributes ||= HashWithIndifferentAccess.new
    end
      
    def attributes=attrs
      @attributes = attrs
    end
  
    def get(attr)
      self.send(attr)
    end 
    alias :read :get

    # Accepts a hash where the keys are methods and the values are values to be set.
    # set(:foo => "bar", :dime => 0.1)
    def set(*attrs)
      attrs.extract_options!.each do |attr,val|
        self.send("#{attr.to_s}=",val)
      end
    end
    alias :set_attributes :set
    
    private
    
    def fetch_default_value(arg)
      return self.send(arg) if (arg.is_a?(Symbol) && self.respond_to?(arg))
      arg
    end
    
    # Returns attribute that have defaults in a hash: {:attrbute => "default value"}
    def attributes_with_for_init(attrs)
      d = attrs.with_indifferent_access
      self.class.defined_attributes.each do |k,v|
        d[k] = fetch_default_value(v[:default]) if (d[k].blank? && v[:default] && v[:initialize])
      end
      d
    end
    
    module ClassMethods    
      # Creates a new instance where the attributes store is set to object
      # provided, which allows one to pass a session store hash or any other
      # hash-like object to be used for persistance. Typically used for modeling
      # session stores for authorization or shopping carts
      # EX: 
      #     class ApplicationController < ActionController::Base
      #       def session_user
      #         session[:user] ||= {}
      #         @session_user ||= SessionUser.new_with_store(session[:user])
      #       end
      #       helper_method :session_user
      #     end
      #
      def new_with_store(session_hash)
        new = self.new()
        new.attributes = session_hash
        new.set(new.send(:attributes_with_for_init,session_hash))
        new
      end
     
      
      def defined_attributes
        @defined_attributes ||= {}
      end
      
      def defined_attributes=defined_attributes
        @defined_attributes = defined_attributes
      end
      
      # The default settings for a SimpeModel class
      # Options:
      # * :on_set - accepts a lambda that is run when an attribute is set
      # * :on_get - accepts a lambda that is run when you get/read an attribute
      # * :default - the default value for the attribute, can be a symbol that is sent for a method
      # * :initialize - informations the object whether or not it should initialize the attribute with :default value, defaults to true
      # ** If :intialize is set to false you must set :allow_blank to false or it will never set the default value
      # * :allow_blank - when set to false, if an attributes value is blank attempts to set the default value, defaults to true
      def default_attribute_settings
        @default_attribute_settings ||= {:attributes_method => :attributes,
          :on_set => lambda {|obj,attr| attr},
          :on_get => lambda {|obj,attr| attr},
          :allow_blank => true,
          :initialize => true
        }
      end
      
      def default_attribute_settings=default_attribute_settings
        @default_attribute_settings = default_attribute_settings
      end
    
      def add_defined_attribute(attr,options)
        self.defined_attributes[attr] = options
        define_attribute_methods self.defined_attributes.keys
      end
      
      # builds the setter and getter methods
      def create_attribute_methods(attributes,options)
        unless attributes.blank?  
          attributes.each do |attr|
            define_reader_with_options(attr,options)
            define_setter_with_options(attr,options)
          end
        end
      end
      
      def define_reader_with_options(attr,options)
        add_defined_attribute(attr,options)
        options = default_attribute_settings.merge(options) if options[:on_get].blank?
        define_method(attr) do
          if (options.key?(:default) && (!self.initialized?(attr) || (!options[:allow_blank] && self.attributes[attr].blank?)))
            self.attributes[attr] = fetch_default_value(options[:default])
          end
          options[:on_get].call(self,self.attributes[attr])
        end
        define_method("#{attr.to_s}?") do
          val = self.send(attr)
          if val.respond_to?(:to_b)
            val = val.to_b 
          else
            val = !val.blank? if val.respond_to?(:blank?)
          end
          val
        end
      end
          
      def define_setter_with_options(attr,options)
        add_defined_attribute(attr,options)
        options = default_attribute_settings.merge(options) if (options[:on_set].blank? || options[:after_set].blank?) 
        define_method("#{attr.to_s}=") do |val|
          val = fetch_default_value(options[:default]) if (!options[:allow_blank] && options.key?(:default) && val.blank?)
          begin   
            val = options[:on_set].call(self,val)
          rescue NoMethodError => e
            raise ArgumentError, "#{val} could not be set for #{attr}: #{e.message}"
          end
          will_change = "#{attr}_will_change!".to_sym
          self.send(will_change) if (self.respond_to?(will_change) && val != self.attributes[attr])          
          self.attributes[attr] = val
          options[:after_set].call(self,val) if options[:after_set] 
        end
      end
    
      AVAILABLE_ATTRIBUTE_METHODS = {
        :has_attribute => {:alias => :has_attributes},
        :has_boolean  => {:cast_to => :to_b, :alias => :has_booleans},
        :has_currency => {:cast_to => :to_d, :alias => :has_currencies},
        :has_date => {:cast_to => :to_date, :alias => :has_dates}, 
        :has_decimal  => {:cast_to => :to_d, :alias => :has_decimals},    
        :has_float => {:cast_to => :to_f, :alias => :has_floats}, 
        :has_int => {:cast_to => :to_i, :alias => :has_ints},
        :has_time => {:cast_to => :to_time, :alias => :has_times}  
      }
      
      AVAILABLE_ATTRIBUTE_METHODS.each do |method,method_options|   
        define_method(method) do |*attributes|
          options = default_attribute_settings.merge(attributes.extract_options!)
          options[:on_set] = lambda {|obj,val| val.send(method_options[:cast_to]) } if method_options[:cast_to]
          create_attribute_methods(attributes,options)
        end
        module_eval("alias #{method_options[:alias]} #{method}")
      end
      
      # Creates alias setter and getter for the supplied attribute using the supplied alias
      # See spec for example.
      def alias_attribute(new_alias,attribute)
        define_method(new_alias) do 
          self.send(attribute)
        end
        define_method("#{new_alias.to_s}=") do |*args, &block|
          self.send("#{attribute.to_s}=",*args, &block)
        end
      end
    end
    
    def self.included(base)
      base.extend(Attributes::ClassMethods)
      base.send(:include, ActiveModel::Dirty) if base.is_a?(Class) # Add Dirty to the class
    end
  end
end