require File.dirname(__FILE__) + "/observer"

class ModelBinding
  include Observer

  attr_reader :property_type, :binding_options
  @@property_type_converters = {
    :undefined => lambda { |value| value },
    :fixnum => lambda { |value| value.to_i },
    :array => lambda { |value| value.to_a }
  }
  def initialize(base_model, property_name_expression, property_type = :undefined, binding_options = nil)
    property_type = :undefined if property_type.nil?
    @base_model = base_model
    @property_name_expression = property_name_expression
    @property_type = property_type
    @binding_options = binding_options || {}
  end
  def model
    nested_property? ? nested_model : base_model
  end
  # e.g. person.address.state returns [person, person.address]
  def nested_models
    @nested_models = [base_model]
    model_property_names.reduce(base_model) do |reduced_model, nested_model_property_name|
      if reduced_model.nil?
        nil
      else
        if nested_model_property_name.start_with?('[')
          property_method = '[]'
          property_argument = nested_model_property_name[1...-1]
          property_argument = property_argument.to_i if property_argument.match(/\d+/)
          new_reduced_model = reduced_model.send(property_method, property_argument)
        else
          new_reduced_model = reduced_model.send(nested_model_property_name)
        end
        @nested_models << new_reduced_model
        new_reduced_model
      end
    end
    @nested_models
  end
  def nested_model
    nested_models.last
  end
  def base_model
    @base_model
  end
  def property_name
    nested_property? ? nested_property_name : property_name_expression
  end
  # All nested property names
  # e.g. property name expression "address.state" gives ['address', 'state']
  # If there are any indexed property names, this returns indexes as properties.
  # e.g. property name expression "addresses[1].state" gives ['addresses', '[1]', 'state']
  def nested_property_names
    @nested_property_names ||= property_name_expression.split(".").map {|pne| pne.match(/([^\[]+)(\[[^\]]+\])?/).to_a.drop(1)}.flatten.compact
  end
  # Final nested property name
  # e.g. property name expression "address.state" gives :state
  def nested_property_name
    nested_property_names.last
  end
  # Model representing nested property names
  # e.g. property name expression "address.state" gives [:address]
  def model_property_names
    nested_property_names[0...-1]
  end
  def nested_property?
    property_name_expression.include?(".")
  end
  def property_name_expression
    @property_name_expression
  end
  def nested_property_observers_for(observer)
    @nested_property_observers_collection ||= {}
    unless @nested_property_observers_collection.has_key?(observer)
      @nested_property_observers_collection[observer] = nested_property_names.reduce({}) do |output, property_name|
        output.merge(
          property_name => BlockObserver.new do |changed_value|
            # Ensure reattaching observers when a higher level nested property is updated (e.g. person.address changes reattaches person.address.street observer)
            add_observer(observer)
            observer.update(evaluate_property)
          end
        )
      end
    end
    @nested_property_observers_collection[observer]
  end
  def add_observer(observer)
    if nested_property?
      nested_property_observers = nested_property_observers_for(observer)
      nested_models.zip(nested_property_names).each do |model, property_name|
        unless model.nil?
          if property_name.start_with?('[')
            model.extend ObservableArray unless model.is_a?(ObservableArray)
            model.add_array_observer(nested_property_observers[property_name]) unless model.has_array_observer?(nested_property_observers[property_name])
          else
            model.extend ObservableModel unless model.is_a?(ObservableModel)
            model.add_observer(property_name, nested_property_observers[property_name]) unless model.has_observer?(property_name, nested_property_observers[property_name])
          end
        end
      end
    else
      model.extend ObservableModel unless model.is_a?(ObservableModel)
      model.add_observer(property_name, observer)
    end
  end
  def update(value)
    return if model.nil?
    converted_value = @@property_type_converters[@property_type].call(value)
    model.send(property_name + "=", converted_value) unless evaluate_property == converted_value
  end
  def evaluate_property
    model.send(property_name) unless model.nil?
  end
  def evaluate_options_property
    model.send(property_name + "_options") unless model.nil?
  end
  def options_property_name
    self.property_name + "_options"
  end
end