require "active_support"
require File.dirname(__FILE__) +'/uuid.rb'
require File.dirname(__FILE__) +'/property_description.rb'
require File.dirname(__FILE__) +"/reference.rb"
require File.dirname(__FILE__) +"/index_description.rb"
require File.dirname(__FILE__) +"/lazy_loading_text.rb"
require File.dirname(__FILE__) +"/repository_factory.rb"
module NotRelational
  class DomainModel
    @@items_to_commit=nil
    @@transaction_depth=0
   attr_accessor :sdb_key
    def initialize(options={})
      @attribute_values=HashWithIndifferentAccess.new

      copy_attributes(options)

      @accessor_cache={}

    end


    def copy_attributes(hash)
      self.class.attribute_descriptions.each do |attribute_name,description|
          if hash.has_key?(attribute_name) or hash.has_key?(attribute_name.intern)
            value=hash[attribute_name]
            value=value || hash[attribute_name.intern]
            if !description.is_collection && value.respond_to?(:flatten) && value.length==1
              value=value[0]
            end
            @attribute_values[attribute_name]=value
          else
            unless @attribute_values.has_key?(attribute_name)
              if description.is_collection
                @attribute_values[attribute_name]=[]
              else
                @attribute_values[attribute_name]=nil unless @attribute_values.has_key?(attribute_name)
              end
            end 
          end


      end


    end

    def self.primary_key_attribute_names
      return @primary_key_attribute_names
    end


    def id
      return @attribute_values[:id] if self.class.attribute_descriptions[:id]
      return base.id
    end
    def set_map(keys,values)
      (0..keys.length-1).each do |i|
        key=keys[i]
        value=values[i]
        @attribute_values[key]=value
      end


    end


    def self.index_names
      []
    end

    def self.index(index_name,columns,options={})
      @index_descriptions||={}
      return if self.index_descriptions[index_name]
      is_index_encrypted=@is_encrypted
      is_index_encrypted=options[:is_encrypted] if options.has_key?(:is_encrypted)

      $columns_xxx=columns
      class_eval <<-GETTERDONE

      unless @index_descriptions.has_key?(:#{index_name})
      @index_names||=[]
      @index_names<<:#{index_name}

    def self.index_names
        @index_names
    end

    attribute_description=NotRelational::PropertyDescription.new(:#{index_name},:string,{})
    @index_descriptions[:#{index_name}]=NotRelational::IndexDescription.new(:#{index_name},$columns_xxx,#{is_index_encrypted.to_s})
   end
      GETTERDONE
     
      getter_params=[]
      finder_code=""
     
      params=[]
      columns.each do |column|
        if column.respond_to?(:transform)
          finder_code<<"h[:#{column.name}]=#{column.name.to_s.downcase}\n"
          getter_params<<"@attribute_values[:#{column.source_column}]"
       
          params<<"#{column.name.to_s.downcase}"
        else
          finder_code<<"h[:#{column}]=#{column.to_s.downcase}\n"
          getter_params<<"@attribute_values[:#{column}]"
          params<<"#{column.to_s.downcase}"
       
        end
       
      end
      find_scope=":all"
      if options[:unique]
        find_scope=":first"
      end
      class_eval <<-GETTERDONE
        def #{index_name}              
            return	self.class.calculate_#{index_name}(#{getter_params.join(",")})
        end

       def self.calculate_#{index_name}(#{params.join(",")})
            index_description=index_descriptions[:#{index_name}]
            h={}
            #{finder_code}
            index_description.format_index_entry(@@attribute_descriptions,h)
      end
      def self.find_by_#{index_name}(#{params.join(",")},options={})
          options[:params]={:#{index_name}=>self.calculate_#{index_name}(#{params.join(",")})}
          options[:index]=:#{index_name}
          options[:index_value]=self.calculate_#{index_name}(#{params.join(",")})
           find(#{find_scope},options)
      end
      GETTERDONE
    end


    @index_descriptions ||={}

    def self.index_descriptions
      @index_descriptions || {}
    end

    def self.is_encrypted?
      return @is_encrypted 
    end
    def self.encrypt_me
      class_eval <<-GETTERDONE
         @is_encrypted=true
      GETTERDONE
    end
    def self.property(name,type,options={})
      class_eval <<-GETTERDONE
      
    @@attribute_descriptions||=HashWithIndifferentAccess.new 
    
    @@non_clob_attribute_names||=[]
    @@clob_attribute_names||=[]
    @@on_destroy_blocks||=[]
    def self.on_destroy_blocks
      @@on_destroy_blocks || []
    end
     def self.attribute_descriptions
      @@attribute_descriptions
    end
    
    def self.non_clob_attribute_names
      @@non_clob_attribute_names
    end
    def self.clob_attribute_names
      @@clob_attribute_names
    end
    def is_dirty(attribute_name)
             if @attribute_values[attribute_name]!= nil && 
                @attribute_values[attribute_name].is_a?(LazyLoadingText)
                return @attribute_values[attribute_name].is_dirty
            else
                return true
            end
    end
      GETTERDONE
      @is_encrypted||=false
      is_prop_encrypted=@is_encrypted
      is_prop_encrypted=options[:is_encrypted] if options.has_key?(:is_encrypted)
      return if self.attribute_descriptions.has_key?(name)
  
      attribute_description=PropertyDescription.new(name,type,is_prop_encrypted,options)
      self.attribute_descriptions[name] = attribute_description
      @primary_key_attribute_names||=[]
      if attribute_description.is_primary_key 
        @primary_key_attribute_names<<name
        pk_code="[:#{@primary_key_attribute_names.join(",:")}]"
        class_eval <<-GETTERDONE
       def self.primary_key_attribute_names
            #{pk_code}
       end
      def self.exists?(primary_key)
            return self.find(primary_key)!=nil
       end
        
        GETTERDONE
      end
      if attribute_description.value_type==:text
        clob_attribute_names<<attribute_description.name
      else
        non_clob_attribute_names<<attribute_description.name
      end

      scope=":all"
      if(options[:unique] && options[:unique]==true)
        scope=":first"
      end
      if type==:reference_set
        
    
        class_eval <<-GETTERDONE
        def #{attribute_description.name}
              result=@attribute_values[:#{attribute_description.name}] || []
              result=result.sort_by{|item|item.index}
          
	end
        def add_to_#{attribute_description.name}(item,index=-1)
		@attribute_values[:#{attribute_description.name}] ||=[]
                @attribute_values[:#{attribute_description.name}] <<NotRelational::Reference.new(:target=> item ,:index=>index)
	end
        def remove_from_#{attribute_description.name}(item)
		@attribute_values[:#{attribute_description.name}] ||=[]
                @attribute_values.each do |reference|
                      if reference.targets?(item)
                        @attribute_values.remove(reference)
                        return
                      end
                end
	end
        
        GETTERDONE
    
      elsif type==:text
        class_eval <<-GETTERDONE
         
        def #{attribute_description.name}
            if @attribute_values[:#{attribute_description.name}]!= nil && 
                            @attribute_values[:#{attribute_description.name}].respond_to?(:value)
		return @attribute_values[:#{attribute_description.name}].value 
            else
                return @attribute_values[:#{attribute_description.name}] 
            end
	end
        
        GETTERDONE
      else
     
        class_eval <<-GETTERDONE
        def #{attribute_description.name}
		return @attribute_values[:#{attribute_description.name}] 
	end
       
        GETTERDONE
      end
      class_eval <<-GETTERDONE
    
	 def #{attribute_description.name}=(xvalue)
                @attribute_values[:#{attribute_description.name}] =xvalue
	end
	def #{attribute_description.name}?
		return @attribute_values[:#{attribute_description.name}] 
	end
	
	def self.find_by_#{attribute_description.name}(#{attribute_description.name.to_s.downcase},options={})
          options[:params]={:#{attribute_description.name}=>#{attribute_description.name.to_s.downcase}}
               find(#{scope},options)
	end
      GETTERDONE

      if options.has_key?(:enum)
        attribute_name=attribute_description.name.to_s
        x=""
        enum_val=1
         options[:enum].each { |enum|
           x<<"#{enum.to_s.upcase} = #{enum_val}\n"
           enum_val+=1
        class_eval <<-GETTERDONE
        def  is_#{attribute_name}_#{enum.to_s.downcase}?
          #{attribute_name} == #{attribute_name.capitalize}::#{enum.to_s.upcase}
        end
	      GETTERDONE

         }
        class_eval <<-GETTERDONE
        class #{attribute_name.capitalize}
          #{x}
        end
	      GETTERDONE


      end


    end
  
    def self.belongs_to(domain_class,foreign_key_attribute=nil,accesser_attribute_name=nil,options={})




      foreign_key_attribute ||= "#{domain_class.to_s.downcase}_id".to_sym
      accesser_attribute_name ||=domain_class.to_s.underscore.downcase
      if foreign_key_attribute==:without_prefix
        # #send all attributes and let the other class figure it out}

        class_eval <<-GETTERDONE
        def #{accesser_attribute_name}

		#{module_name+domain_class.to_s}.find(@attribute_values)
	end
        GETTERDONE

  
      elsif foreign_key_attribute==:with_prefix
        # #the column names that start with the name of the other class are the
        # fkeys
        fkeys=[]
        self.class.attribute_descriptions.each do |attribute_name,description|
          if attribute_name.index(domain_class.to_s.downcase)==0
            fkeys<<attribute_name
            fkey_names<<attribute_name.to_s
          end
        end
        index(fkey_names.join("_"),keys)

        class_eval <<-GETTERDONE
	def #{accesser_attribute_name}
      mapped_values={}
      prefix='#{domain_class.to_s.downcase}'
      ['#{fkey_names.join("','")}'].each |key|
              mapped_values[key.splice(prefix.length)]=@attribute_values[key]
      end
      #{module_name+domain_class.to_s}.find(mapped_values)

	end
        GETTERDONE
      else
        class_eval <<-GETTERDONE
	def #{accesser_attribute_name}
    #{module_name+domain_class.to_s}.find(self.#{foreign_key_attribute})
	end
        GETTERDONE
        if foreign_key_attribute.is_a?(Array)
          index("#{foreign_key_attribute}_index".to_sym,[foreign_key_attribute])
        end
      end

    end
    def self.many_to_many(domain_class,
        through_class,
        reflecting_key_attributes=nil,
        foriegn_key_attribute=nil,
        accesser_attribute_name=nil,
        options={})
      reflecting_key_attributes ||= primary_key_attribute_names.map{|x|self.name+"_"+x.to_s}
      reflecting_key_attributes =arrayify(reflecting_key_attributes)
      reflecting_array_code="[:"+reflecting_key_attributes.join(",:")+"]"

      accesser_attribute_name ||=domain_class.to_s.underscore.pluralize
      order=""
      if options[:order_by]
        if options[:order]
          order="result=DomainModel.sort_result(result,:#{options[:order_by]},:#{options[:order]})"
        else
          order="result=DomainModel.sort_result(result,:#{options[:order_by]},:ascending)"
        end
      end
   
       
      foriegn_key_attributes=options[:foriegn_key_attribute] || "#{domain_class.to_s.downcase}_id".to_sym
      foriegn_key_attributes=arrayify(foriegn_key_attributes)
      fkey_array_code="[:"+foriegn_key_attributes.join(",:")+"]"
      class_eval <<-XXDONE

	def #{accesser_attribute_name}
            #unless @accessor_cache.has_key? :#{accesser_attribute_name}
      		through_results= #{module_name+through_class.to_s}.find(:all,:map=>{:keys=>#{reflecting_array_code},:values=>DomainModel.arrayify(self.primary_key)})
                result=[]
                through_results.each do |through_result|
                  item= #{module_name+domain_class.to_s}.find(through_result.#{foriegn_key_attribute}) 
                  result<< item if item
                end
                #{order}
                
              #  @accessor_cache[:#{accesser_attribute_name}]=result
                return result
            #end
            #return @accessor_cache[:#{accesser_attribute_name}]
            
	end
        def connect_#{domain_class.to_s.downcase}(#{domain_class.to_s.downcase})
            connector=#{module_name+through_class.to_s}.new
            connector.set_map(#{fkey_array_code},DomainModel.arrayify(#{domain_class.to_s.downcase}.primary_key))
            connector.set_map(#{reflecting_array_code},DomainModel.arrayify(self.primary_key))
            connector.save
        end
      XXDONE
       
   
    end


    def DomainModel.transaction
      @@items_to_commit||=[]
      @@transaction_depth+=1
      begin
        yield
      rescue # catch all
        raise $! # rethrow
      ensure
        @@transaction_depth-=1
      
      end
    
    
      if @@transaction_depth==0
     
        @@items_to_commit.uniq.each do |item |
          item.save!
        end
        @@items_to_commit=[]
      end
    
    end
    def DomainModel.sort_result(results,sort_by,order=:ascending)
      non_null_results=[]
      null_results=[]
      results.each { |i|
        sorter_value=i.get_attribute(sort_by)
        if sorter_value
          non_null_results<<i
        else
          null_results<<i
        end
      }
      sorted_results=non_null_results.sort{ |a,b|
        a_val= a.get_sortable_attribute(sort_by)
        b_val= b.get_sortable_attribute(sort_by)
        
        a_val <=> b_val
      }
        
      if order!=:ascending
        sorted_results.reverse!
      end
      sorted_results.concat( null_results)
      sorted_results
    end
    def get_sortable_attribute(attr_name)
      a_val= self.get_attribute(attr_name)
      return 0 unless a_val
      if a_val.class== Time
        return a_val.to_f
      else
        return a_val
      end
    end
    def self.has_many(domain_class,
        reflecting_key_attributes=nil,
        accesser_attribute_name=nil,
        options={})
      if reflecting_key_attributes==:without_prefix
        reflecting_key_attributes = primary_key_attribute_names.map{|x|x.to_s}

      end

      reflecting_key_attributes ||= primary_key_attribute_names.map{|x|self.name.downcase+"_"+x.to_s}
      reflecting_key_attributes =arrayify(reflecting_key_attributes)
      reflecting_array_code="[:"+reflecting_key_attributes.join(",:")+"]"

      accesser_attribute_name ||= domain_class.to_s.underscore.pluralize
      order=""
      if options[:order_by]
        order="{:order_by=>:#{options[:order_by]}"
      end
      if options[:order]
        if order.length>0
          order << ","
        else
          order<<"{"
        end
        order<<":order=>:#{options[:order]}"
      end
      if order.length==0
        order << "{}"
      else
        order<<"}"
      end


      if options[:dependent]
        
        class_eval <<-XXDONE
          
            on_destroy_blocks<<"#{accesser_attribute_name}.each{|item|item.destroy}"
        XXDONE
      end
      class_eval <<-XXDONE
      def create_child_#{domain_class.to_s.downcase}(options=nil)

        result=#{module_name+domain_class.to_s}.new(options)
        result.copy_attributes(self.primary_key_hash)
        result
      end
      XXDONE
      #  if options[:tracking]
      #    #add add_xxx method
      #    #add to list of tracker attributes
      #
      #      class_eval <<-XXDONE
      #
      #    @@attribute_descriptions[ :#{accesser_attribute_name}_tracker]=TrackerDescription.new(:#{accesser_attribute_name}_tracker,:#{domain_class},:#{reflecting_key_attribute})
      # # def #{accesser_attribute_name} # 	find_tracked_list(:#{accesser_attribute_name}_tracker,#{domain_class},:#{reflecting_key_attribute})
      # # end
      #        def add_to_#{accesser_attribute_name}(item)
      # # 	item.save!
      #                add_to_tracked_list(:#{accesser_attribute_name}_tracker,#{domain_class.to_s},:#{reflecting_key_attribute},item.primary_key)
      #                item.set_attribute(:#{reflecting_key_attribute},self.primary_key)
      #
      #
      # # end
      #    XXDONE
      #  else
      class_eval <<-XXDONE
	def #{accesser_attribute_name}

            #{}if !@accessor_cache.has_key? :#{accesser_attribute_name}
              h={}
              pkey=DomainModel::arrayify(self.primary_key)
              #{reflecting_array_code}.each do |key|
                h[key]=pkey.shift
              end
              result= #{module_name+domain_class.to_s}.find_by_index(h,#{order})
              return result
             #{}   @accessor_cache[:#{accesser_attribute_name}]=result || []
            #{}end
            #return @accessor_cache[:#{accesser_attribute_name}]
	end
      XXDONE
      # end
    end
    def self.AttributeDescription(name)
      return attribute_descriptions[name]
    end
    def self.ColumnDescription(name)
      return column_descriptions[name]
    end
    def primary_key

      result=[]
      self.class.primary_key_attribute_names.each do |key_part|
        result<<@attribute_values[key_part ]
      end
      return result[0] if result.length==1
      return result
    end
    def primary_key_hash

      result={}
      self.class.primary_key_attribute_names.each do |key_part|
        result[key_part]=@attribute_values[key_part ]
      end
      return result
    end
    def primary_key=(value)
      key=DomainModel::arrayify(value)
      self.class.primary_key_attribute_names.each do |key_part|

        @attribute_values[ key_part]=key.shift
      end
    end
    def method_missing(method_symbol, *arguments) #:nodoc:
      method_name = method_symbol.to_s

      case method_name.last
      when "="
        if @attribute_values.has_key?(method_name.first(-1))
          @attribute_values[method_name.first(-1)] = arguments.first
        else
          super
        end
      when "?"
        @attribute_values[method_name.first(-1)]
      else
        @attribute_values.has_key?(method_name) ? @attribute_values[method_name] : super
      end
    end
    def index_values
      result={}
      self.class.index_names.each do |name|
              
        result[name]=  self.send("#{name}")
   
      end
      result
    end
    def set_attribute(name,value)
      @attribute_values[name]=value
    end
    def get_attribute(name)
      @attribute_values[name]
    end
    def to_xml
      result= "<#{self.class.table_name}>"
      attributes.each do |key,value|
        if value.respond_to?(:flatten)
          result<<"<#{key}>"
          value.each do |sub_value|
            result<<"<value>#{h sub_value.to_s}</value>"
          end
          result<<"</#{key}>"
        else
          result<<"<#{key}>#{h value.to_s}</#{key}>"
        end
      end
      result<< "</#{self.class.table_name}>"
      result
    end
    def destroy(options={})
      if self.class.on_destroy_blocks
        self.class.on_destroy_blocks.each do |block|
          instance_eval <<-XXDONE
                    #{block}

          XXDONE

        end
      end
      if self.primary_key
        self.repository(options).destroy(self.table_name,self.primary_key)
      end
    end
    def attributes
      result={}
      # #todo refactor to avoid this copy
      self.class.attribute_descriptions.values.each do |description|
        if !description.is_text? || is_dirty(description.name)
          result[description.name]=@attribute_values[description.name]
        end
      end
      return result
    end

    def find_tracked_list(tracking_attribute,other_class,reflecting_attribute)
      # #if the attribute has no  value do a query
      list=@attribute_values[tracking_attribute]
      if !list
        list=other_class.query_ids(:params=>{reflecting_attribute=>self.primary_key})
        @attribute_values[tracking_attribute]=list
        self.save!
      end
      
      return other_class.find_list(list)
    
    end
  
    def add_to_tracked_list(tracking_attribute,other_class,reflecting_attribute,value)
      list=@attribute_values[tracking_attribute]
      if !list
        list=other_class.query_ids(:params=>{reflecting_attribute=>self.primary_key})
      
      end
      list<<value
      @attribute_values[tracking_attribute]=list.uniq
    
    end
    def save!(options={})
      save(options)
    end
    def save(options={})
      if !self.primary_key
        self.primary_key= NotRelational::UUID.generate
      end
        
      if @@transaction_depth>0
        # #we are in a transaction.  wait to commit
        @@items_to_commit<<self
     
      else
        temp_accessor_cache=@accessor_cache
        @accessor_cache=nil
   
        #     $service.put_attributes(class_object.table_name,item.send(name_column),attributes,true)
        attributes={}
        # #todo refactor to avoid this copy
        self.class.attribute_descriptions.values.each do |description|
          if !description.is_text? || is_dirty(description.name)
            value=@attribute_values[description.name]
            attributes[description]=value
          end
   
                
           
        end
        self.index_values.each do |name,value|
      
          attributes[self.class.index_descriptions[name]]=value
        end
  
        self.repository(options).save(self.table_name,self.primary_key,attributes,self.class.index_descriptions)
        @accessor_cache=temp_accessor_cache
      end
    
    end
    def table_name
      return self.class.table_name
    end
    def DomainModel.table_name
      return self.name
    end
    #    def find
    #
    #       attributes=repository.find(self.class.name,self.primary_key,@@non_clob_attribute_names,@@clob_attribute_names)
    #       attributes.each do |key,value|
    #           @attribute_values[key]=value
    #       end
    #    end
    def repository(options=nil)
    
      if options and options.has_key?(:repository)
        return options[:repository]
      end
      if @repository
        return @repository
      end

      return self.class.repository
    end

    class << self
      def repository(options={})
    
        if options.has_key?(:repository)
          return options[:repository]
        end
      
        return RepositoryFactory.instance(options)
    
      end
      def find_by_index(values,more_options)
        index_descriptions.each do |name,desc|
          if desc.keys_match?(values)
            options={}
            options[:params]={desc.name=>desc.format_index_entry(self.attribute_descriptions,values)}
            options[:index]=desc.name
            options[:index_value]=desc.format_index_entry(self.attribute_descriptions,values)
            options.merge!(more_options)
            return find(:all,options)
          end
        end
        options={}

        options[:params]={}
        options.merge!(more_options)
        values.each{|key,value|options[:params][key]=value}
        return find_every(options)
      end
      def find(*arguments)
        scope   = arguments.slice!(0)
        options = arguments.slice!(0) || {}
        
        case scope
        when :all   then return find_every(options)
        when :first then return find_one(options)
        else             return find_single(scope, options)
        end
      end
      def find_one(options={})
        options[:limit]=1
        find_every(options).first
      end
      def find_every(options={})

        results=[]
        untyped_results=self.repository.query(self.table_name,attribute_descriptions,options)
        untyped_results.each do |item_attributes|

          results<<istantiate(item_attributes,self.repository(options))
        end
        return results

      end
      def find_list(primary_keys,options={})
        result=[]
        primary_keys.each do |key|
          item=find_single(key,options)
          result<<item if item
        end
        return result
      end
      def query_ids(options={})
        self.repository.query_ids(self.table_name,attribute_descriptions,options)
      end
      def istantiate(sdb_attributes,repository)
        this_result=new(sdb_attributes||{})
        get_text_proc=nil
        clob_attribute_names.each do |clob_attribute_name|
          get_text_proc= proc {
            repository.get_text(self.table_name,this_result.primary_key,clob_attribute_name)
          }
                  
          this_result.set_attribute(clob_attribute_name, LazyLoadingText.new(get_text_proc))
        end
        this_result
      end
      def arrayify(x)
        unless x.is_a?(Array)
          x=[x]
        end
        return x

      end
      def find_single(id, options)
        return nil if id==nil
        if id.is_a?(Hash)
          id_as_array=[]
          @primary_key_attribute_names.each do |key_name|
            id_as_array << id[key_name]
          end
          id=id_as_array
        end
        id=arrayify(id)
        attributes=self.repository(options).find_one(self.table_name,id,attribute_descriptions)
        if attributes && attributes.length>0
          @primary_key_attribute_names.each do |key_name|
            attributes[key_name]=id.shift
          end
          attributes[:repository]=self.repository(options)
          return istantiate(attributes,self.repository(options))
        end
        return nil
      end
      def find_recent_with_exponential_expansion(time_column,how_many,options={})
        found=[]
        tries=0
        timespan=options[:timespan_hint] || 60*60*4
      
        params = options[:params] || {}
      
        while found.length<how_many && tries<4
          time=Time.now.gmtime-timespan
          params[time_column]  =AttributeRange.new(:greater_than=>time)
          found= find(:all,:limit=>how_many,:order=>:descending,:order_by => time_column,
            :params=>params)
        
          tries=tries+1
          timespan=timespan*3
        end
        return found
          
      end

    def all(options={})
      return find_every(options)
    end
    end
    def self.module_name
      result=""
      result=self.name.slice(0, self.name.rindex(":")+1) if self.name.rindex(":")
      result
    end
  end
end