module Architect4r
  
  module Model
    
    module Properties
      extend ActiveSupport::Concern
      
      included do
        class_attribute(:properties) unless respond_to?(:properties)
        self.properties ||= {}
      end
      
      module InstanceMethods
        def parse_properties(properties = {})
          @properties_data = {}
          update_attributes_without_saving(properties)
        end
        
        # Return a hash of all properties which can be transformed into json
        # Remove all nil values, as those cannot be stored in the graph
        def _to_database_hash
          @properties_data.merge('architect4r_type' => self.class.name).
                           reject { |key, value| value.nil? }
        end

        # Read the casted value of an attribute defined with a property.
        #
        # ==== Returns
        # Object:: the casted attibutes value.
        def read_attribute(property, locale = nil)
          property = "#{property}_#{locale}" if locale
          @properties_data[property.to_s]
        end

        # Store a casted value in the current instance of an attribute defined
        # with a property and update dirty status
        def write_attribute(property, value, locale = nil)
          # retrieve options for the attribute
          opts = self.class.properties[property.to_s]
          
          # Check if we should store a localized version of the data
          property = "#{property}_#{locale}" if locale
          
          # TODO: Mark dirty attribute tracking
          
          # Cast the value before storing it
          cast_to = opts && opts[:cast_to] || Object
          
          @properties_data[property.to_s] = if value.nil?
            nil
          elsif cast_to == String
            value.to_s
          elsif cast_to == Integer
            value.to_i
          elsif cast_to == Float
            value.to_f
          elsif cast_to == DateTime
            value.to_datetime
          elsif cast_to == TrueClass or cast_to == FalseClass
            if value.kind_of?(Integer)
              value == 1
            else
              %w[ true 1 t ].include?(value.to_s.downcase)
            end
          else
            value
          end
        end
        
        def update_attributes_without_saving(hash)
          return if hash.nil?
          hash.each do |key, value|
            if self.respond_to?("#{key}=")
              self.send("#{key}=", value)
            else
              @properties_data[key] = value
            end
          end
        end
        alias :attributes= :update_attributes_without_saving
        
        
      end
      
      module ClassMethods
        
        # Allow setting properties
        def property(name, options = {})
          unless self.properties.keys.find { |p| p == name.to_s }
            define_property(name, options)
          end
        end
        
        def timestamps!
          property(:updated_at, :cast_to => DateTime)
          property(:created_at, :cast_to => DateTime)
          
          set_callback :create, :before do |object|
            write_attribute('created_at', DateTime.now)
          end
          
          set_callback :save, :before do |object|
            write_attribute('updated_at', DateTime.now)
          end
        end
        
        protected
        
        # This is not a thread safe operation, if you have to set new properties at runtime
        # make sure a mutex is used.
        def define_property(name, options={})
          # read only flag
          read_only = options.delete(:read_only) || false
          
          # Store the property and its options
          self.properties[name.to_s] = options
          
          # Create getter and setter methods
          create_property_getter(name)
          create_property_setter(name) unless read_only == true
          
          # Return property name just in case ;)
          name
        end
        
        # defines the getter for the property (and optional aliases)
        def create_property_getter(name)
          define_method(name) do
            # Get property configuration
            localize = self.class.properties[name.to_s][:localize]
            
            # Determine current locale
            locale = localize ? I18n.locale : nil
            
            # Fetch property value
            result = read_attribute(name, locale)
            
            # If there is a fallback locale, fetch its value if appropriate
            if result.nil? && localize && localize != true && localize.to_s != locale.to_s
              result = read_attribute(name, localize)
            end
            
            # Finally return some data
            result
          end
          
          opts = self.properties[name.to_s]
          
          if opts[:cast_to].presence && opts[:cast_to] == TrueClass
            define_method("#{name}?") do
              value = read_attribute(name)
              !(value.nil? || value == false)
            end
          end
        end
        
        # defines the setter for the property (and optional aliases)
        def create_property_setter(name)
          define_method("#{name}=") do |value|
            locale = self.class.properties[name.to_s][:localize] ? I18n.locale : nil
            write_attribute(name, value, locale)
          end
        end
        
      end
      
    end
    
  end
  
end