require 'moosex/types'
require 'moosex/attribute/modifiers'

module MooseX
  class Attribute
    include MooseX::Types

    attr_reader :attr_symbol, :methods, :attribute_map

    def is       ; @attribute_map[:is] ; end
    def writter  ; @attribute_map[:writter] ; end
    def reader   ; @attribute_map[:reader] ; end
    def override ; @attribute_map[:override] ; end
    def doc      ; @attribute_map[:doc] ; end
    def default  ; @attribute_map[:default] ; end

    @@LIST_OF_PARAMETERS = [ 
      :is,        #MooseX::AttributeModifiers::Is       ],
      :isa,       #MooseX::AttributeModifiers::Isa      ],
      :default,   #MooseX::AttributeModifiers::Default  ],
      :required,  #MooseX::AttributeModifiers::Required ],
      :predicate, #MooseX::AttributeModifiers::Predicate],
      :clearer,   #MooseX::AttributeModifiers::Clearer  ],
      :traits,    #MooseX::AttributeModifiers::Traits   ],        
      :handles,   #MooseX::AttributeModifiers::Handles  ], 
      :lazy,      #MooseX::AttributeModifiers::Lazy     ], 
      :reader,    #MooseX::AttributeModifiers::Reader   ], 
      :writter,   #MooseX::AttributeModifiers::Writter  ], 
      :builder,   #MooseX::AttributeModifiers::Builder  ], 
      :init_arg,  #MooseX::AttributeModifiers::Init_arg ], 
      :trigger,   #MooseX::AttributeModifiers::Trigger  ], 
      :coerce,    #MooseX::AttributeModifiers::Coerce   ], 
      :weak,      #MooseX::AttributeModifiers::Weak     ], 
      :doc,       #MooseX::AttributeModifiers::Doc      ], 
      :override,  #MooseX::AttributeModifiers::Override ],
    ]

    def initialize(attr_symbol, options ,klass)
      @attr_symbol   = attr_symbol
      @attribute_map = {}

      init_internal_modifiers(options.clone, klass.__moosex__meta.plugins, klass)
    end

    def init_internal_modifiers(options, plugins, klass)
      list = @@LIST_OF_PARAMETERS.map do |parameter|
        MooseX::AttributeModifiers::const_get(parameter.capitalize).new(self)
      end
      
      list.each do |plugin|
        plugin.prepare(options)
      end
        
      plugins.sort.uniq.each do |plugin_klass|
        begin         
          plugin_klass.new(self).prepare(options)
        rescue => e
          raise "Unexpected Error in #{klass} #{plugin_klass} #{@attr_symbol}: #{e}"  
        end  
      end
      
      list.each do |plugin|
        plugin.process(options)
      end
      
      generate_all_methods
      
      plugins.sort.uniq.each do |plugin_klass|
        begin         
          plugin_klass.new(self).process(options)
        rescue NameError => e
          next
        rescue => e
          raise "Unexpected Error in #{klass} #{plugin_klass} #{@attr_symbol}: #{e}"  
        end  
      end

      MooseX.warn "Unused attributes #{options} for attribute #{@attr_symbol} @ #{klass} #{klass.class}",caller() if ! options.empty?  
    end

    def generate_all_methods
      @methods       = {}

      if @attribute_map[:reader] 
        @methods[@attribute_map[:reader]] = generate_reader
      end
      
      if @attribute_map[:writter] 
        @methods[@attribute_map[:writter]] = generate_writter
      end

      inst_variable_name = "@#{@attr_symbol}".to_sym
      if @attribute_map[:predicate]
        @methods[@attribute_map[:predicate]] = ->(this) do
          this.instance_variable_defined? inst_variable_name
        end
      end

      if @attribute_map[:clearer]
        @methods[@attribute_map[:clearer]] = ->(this) do
          if this.instance_variable_defined? inst_variable_name
            this.remove_instance_variable inst_variable_name
          end
        end
      end

      generate_handles @attr_symbol
    end

    def generate_handles(attr_symbol)

      delegator = ->(this) { this.__send__(attr_symbol) }

      @attribute_map[:handles].each_pair do | method, target_method |
        if target_method.is_a? Array
          original_method, currying = target_method

          @methods[method] = generate_handles_with_currying(delegator, original_method, currying)         
        else  
          @methods[method] = Proc.new do |this, *args, &proc|
            delegator.call(this).__send__(target_method, *args, &proc)
          end
        end 
      end  
    end    

    def generate_handles_with_currying(delegator, original_method, currying)
      Proc.new do |this, *args, &proc|

        a1 = [ currying ]

        if currying.is_a?Proc
          a1 = currying[]
        elsif currying.is_a? Array
          a1 = currying.map{|c| (c.is_a?(Proc)) ? c[] : c }
        end

        delegator.call(this).__send__(original_method, *a1, *args, &proc)
      end
    end   
    
    def init(object, args)
      value  = nil
      value_from_default = false
      
      if args.has_key? @attribute_map[:init_arg]
        value = args.delete(@attribute_map[:init_arg])
      elsif @attribute_map[:default]
        value = @attribute_map[:default].call
        value_from_default = true
      elsif @attribute_map[:required]
        raise InvalidAttributeError, "attr \"#{@attr_symbol}\" is required"
      else
        return
      end

      value = @attribute_map[:coerce].call(value)   
      begin
        @attribute_map[:isa].call( value )
      rescue MooseX::Types::TypeCheckError => e
        raise MooseX::Types::TypeCheckError, "isa check for field #{attr_symbol}: #{e}"
      end
      unless value_from_default
        @attribute_map[:trigger].call(object, value)
      end
      value = @attribute_map[:traits].call(value)
      inst_variable_name = "@#{@attr_symbol}".to_sym
      object.instance_variable_set inst_variable_name, value
    end
    
    def generate_reader
      inst_variable_name = "@#{@attr_symbol}".to_sym
      
      builder    = @attribute_map[:builder]
      before_get = ->(object) { }

      if @attribute_map[:lazy]
        type_check = protect_isa(@attribute_map[:isa], "isa check for #{inst_variable_name} from builder")
        coerce     = @attribute_map[:coerce]
        trigger    = @attribute_map[:trigger]
        traits     = @attribute_map[:traits]
        before_get = ->(object) do
          return if object.instance_variable_defined? inst_variable_name
        
          value = builder.call(object)
          value = coerce.call(value)
          type_check.call( value )
          
          trigger.call(object, value)
          value = traits.call(value)
          object.instance_variable_set(inst_variable_name, value)         
        end
      end

      ->(this) do 
        before_get.call(this)
        this.instance_variable_get inst_variable_name 
      end
    end

    def protect_isa(type_check, message)
      ->(value) do
        begin
          type_check.call( value )
        rescue MooseX::Types::TypeCheckError => e
          raise MooseX::Types::TypeCheckError, "#{message}: #{e}"
        end
      end 
    end
    
    def generate_writter
      writter_name       = @attribute_map[:writter]
      inst_variable_name = "@#{@attr_symbol}".to_sym
      coerce             = @attribute_map[:coerce]
      type_check         = protect_isa(@attribute_map[:isa], "isa check for #{writter_name}")
      trigger            = @attribute_map[:trigger]
      traits             = @attribute_map[:traits]
      ->(this, value) do 
        value = coerce.call(value)
        type_check.call( value )
        trigger.call(this,value)
        value = traits.call(value)
        this.instance_variable_set inst_variable_name, value
      end
    end     
  end
end