require 'dm-core'
require 'dm-core/adapters/abstract_adapter'       
require 'rbridge'
module DataMapper::Adapters

  class MnesiaAdapter < AbstractAdapter      


    #Initialize the adapter with the given options
    # 1. hostname: location of the rbridge server as an Ip or url, default is localhost
    # 2. port: the port to comunicate with rbridge, default is 9900
    # 3. mod: rbridge offers several modes of operation. 
    # 4. ids_table: the table in mnesia that will handle the ids of each record created
    # 5. id_function: in case an erlang function is wished to be used for record id generations, it can be set here. If the id is a BinaryString, this is required
    def initialize(name, options)
      super                                       

      @hosname = @options[:hostname] ||= "localhost"         
      @port = @options[:port]        ||= 9900 
      @mod = @options[:mod] 
      @ids_table = @options[:ids_table]
      @id_function = @options[:id_function]
      @con = con
      @table_attributes = nil

      ## Refreshes the spec table so that spec test run properly
      con.erl("mnesia:clear_table(heffalump).") if name == :default
    end

    # Initializes the connection to the rbridge server
    def con(&blk)
      # connect to the server
      if @con.nil?
        @con = RBridge.new(@mod,@hostname, @port)
        if @con.erl("1*1.") != 1
           raise "Error opening connection to database: #{@con.erl("1*1.")}"
        end
      end
      @con
    end

    # Create function required by the adapter spec
    def create(resources)
        resources.each do |resource|
          initialize_identity_field(resource,get_key(resource))
          save(con, parse_resource(resource))
      end
    end
    
    # Read function required by the adapter spec
    #TODO change query so that the mnesia filters the results
    # and does not bring all records on the table every time
    def read(query)
        temp_records = erl_read(query)
        records = []
        temp_records.each do |value|
          records << value if value
        end
      result = query.filter_records(records.reverse)
      result
    end
    
    # Update function required by the adapter spec
    def update(attributes, collection)
      attributes = attributes_as_fields(attributes)
      collection.each do |resource|
          attributes = resource.attributes(:field).merge(attributes)
          save(con, parse_resource(resource))
        end
    end
    
    # Delete function required by the adapter spec
    def delete(collection)
        collection.each do |resource|
          erl_delete(resource.model,resource.key.first)
        end
    end
    

    protected

    # On record creation the primary id must be generated
    # If the id_function option is set and the key for the model is a binary string, an erlan function is used.
    # otherwise, and mnesia unique id table can be used or a random number
    def get_key resource
      model = resource.model
      identity_field = get_identity_field resource
      if !@id_function.nil? and identity_field.is_a? DataMapper::Property::BinaryString
        con.erl("#{@id_function}.")  
      elsif @ids_tables.nil?
        con.erl("mnesia:dirty_update_counter(#{@ids_table},#{model.to_s.downcase},1).") 
      else
        rand(2**32)
      end
    end
    

    # Before update or create the resource must be parse to be properly pass to rbridge
    def parse_resource(resource)
      model = resource.model
      fields = []
      types = Hash.new 
      # Get all properties defined on the datamapper model
      resource.send(:properties).map do |property|
        if property.is_a? DataMapper::Property::Serial 
          types[property.name] = 0
       elsif property.is_a? DataMapper::Property::Integer 
          types[property.name] = 0
       elsif property.is_a? DataMapper::Property::Boolean 
          types[property.name] = false
       elsif property.is_a? DataMapper::Property::Record 
          types[property.name] = {}
       elsif property.is_a? DataMapper::Property::List 
          types[property.name] = []
       elsif property.is_a? DataMapper::Property::BinaryString 
          types[property.name] = 0b100
       elsif property.is_a? DataMapper::Property::DateTime 
          types[property.name] = Time.now
       elsif property.is_a? DataMapper::Property::String 
          types[property.name] = ""
      end


      end

      # Set the value for the identity field
     identity_field = get_identity_field resource
     fields.push(identity_field.dump(resource.key.first))

     # Loop through the attributes defined in the mnesia record
     # and set the value for each one except the identity field
     table_attributes(model).each do |p|
        if p.to_s!= identity_field.field
          if types[p.to_sym].is_a? Integer
             value = resource[p.to_sym] == nil ? 'undefined' : resource[p.to_sym] 
             fields.push(value)
          elsif types[p.to_sym].is_a? Array
            field  = resource.model.properties.detect{|f|f.field == p.to_s}
            value = resource[p.to_sym]
      		 if resource[p.to_sym].length == 0
        	    fields.push("'undefined'")
	         else
	            fields.push(field.dump(value)) 
	         end
          elsif types[p.to_sym].is_a? Hash
            value = resource[p.to_sym]
      		 if resource[p.to_sym].nil?
        	    fields.push("'undefined'")
	         else
	            fields.push(value) 
	         end
          else
            if resource[p.to_sym].nil?
              fields.push("'undefined'")
            else
              fields.push("\"" + resource[p.to_sym].to_s.gsub('"','\"') + "\"")
            end
          end
          
        end
      end
      "#{model.to_s.downcase},{#{model.to_s.downcase},#{fields.join(',').to_s}}"
    end

    
    # Function called by create and update
    def save(con, record)
      if !erl_save(con,record)
        raise WriteError, erl_save(con,record)
      end
    end

    # Retrived the attribues defined for the table in mnesia
    def table_attributes model
      if @table_attribute.nil?
        @table_attributes = con.erl("mnesia:table_info(" + model.to_s.downcase + ",attributes).")
      end
      @table_attributes
    end

    # Set the initial value for the identity field on record creation
    def initialize_identity_field resource, value
        identity_field = get_identity_field resource
        identity_field.set(resource,value)
    end

    # Returns the field object defined as a key in the model
    def get_identity_field resource
       resource.model.key(name).detect{|p|p.key?}
    end

    # Gets the id value from the conditions passed in the query 
    def get_id_from_conditions conditions
      conditions.map do |condition|
            return condition.loaded_value if condition.subject.name == :id
          end
      nil
    end

    # ERLAND FUNCTIONS
    def erl_save(con,record)
      con.erl("mnesia:dirty_write(#{record}).")
    end

    # Reads the recuested records from mnesia and parses them to be pased to the Read function
    def erl_read query
       model = query.model  
       if query.limit.nil?
        records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:q([ X || X <- mnesia:table(#{model.to_s.downcase}) ]))end ).")[1]
       elsif query.limit == 1 && !get_id_from_conditions(query.conditions).nil?
             id = get_id_from_conditions(query.conditions)
             if id.is_a? Integer
                records = con.erl("mnesia:dirty_read({#{model.to_s.downcase},#{id}}).")
             else
                records = con.erl("mnesia:dirty_read({#{model.to_s.downcase},<<\"" + id.to_s + "\">>}).")
             end
       elsif query.limit > 1
         records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:next_answers(qlc:cursor(qlc:q([ X || X <- mnesia:table(#{query.model.to_s.downcase}) ])),#{query.limit})    )end ).")[1]
       else
          records = con.erl("mnesia:transaction(fun() ->qlc:eval( qlc:q([ X || X <- mnesia:table(#{model.to_s.downcase}) ]))end ).")[1]
       end
        
       result = []
       attributes = table_attributes model
       records.each do | r |
         r.delete(model.to_s.downcase)
         h = Hash.new
         r.each_with_index do |item,i|
           h[attributes[i]] = r[i] == 'undefined' ? nil : r[i]
         end
         result << h
       end
       result.reverse
    end

    # Sends the delete command to erland.
    def erl_delete model, id
      command = ""
      if id.is_a? Integer
        command = "mnesia:dirty_delete({#{model.to_s.downcase},#{id.to_s}})."
      else
        command = "mnesia:dirty_delete({#{model.to_s.downcase},<<\"" + id.to_s + "\">>})."
      end
      if !con.erl(command)
        raise WriteError, con.erl(command)
      end
      
    end

    
    

    class ConnectError < StandardError
    end

    class WriteError < StandardError
    end

    class ReadError < StandardError
    end


  end

end