module ReactiveRecord
  class Base

    # Its all about lazy loading. This prevents us from grabbing enormous association collections, or large attributes
    # unless they are explicitly requested.

    # During prerendering we get each attribute as its requested and fill it in both on the javascript side, as well as
    # remember that the attribute needs to be part of the download to client.

    # On the client we fill in the record data with empty values (nil, or one element collections) but only as the attribute
    # is requested.  Each request queues up a request to get the real data from the server.

    # The ReactiveRecord class serves two purposes.  First it is the unique data corresponding to the last known state of a
    # database record.  This means All records matching a specific database record are unique.  This is unlike AR but is
    # important both for the lazy loading and also so that when values change react can be informed of the change.

    # Secondly it serves as name space for all the ReactiveRecord specific methods, so every AR Instance has a ReactiveRecord

    # Because there is no point in generating a new ar_instance everytime a search is made we cache the first ar_instance created.
    # Its possible however during loading to create a new ar_instances that will in the end point to the same record.

    # VECTORS... are an important concept.  They are the substitute for a primary key before a record is loaded.
    # Vectors have the form [ModelClass, method_call, method_call, method_call...]

    # Each method call is either a simple method name or an array in the form [method_name, param, param ...]
    # Example [User, [find, 123], todos, active, [due, "1/1/2016"], title]
    # Roughly corresponds to this query: User.find(123).todos.active.due("1/1/2016").select(:title)

    attr_accessor :ar_instance
    attr_accessor :vector
    attr_accessor :model
    attr_accessor :changed_attributes
    attr_accessor :aggregate_owner
    attr_accessor :aggregate_attribute
    attr_accessor :destroyed
    attr_accessor :updated_during
    attr_accessor :synced_attributes
    attr_accessor :virgin

    # While data is being loaded from the server certain internal behaviors need to change
    # for example records all record changes are synced as they happen.
    # This is implemented this way so that the ServerDataCache class can use pure active
    # record methods in its implementation

    def self.data_loading?
      @data_loading
    end

    def data_loading?
      self.class.data_loading?
    end

    def self.load_data(&block)
      current_data_loading, @data_loading = [@data_loading, true]
      yield
    ensure
      @data_loading = current_data_loading
    end

    def self.load_from_json(json, target = nil)
      load_data { ServerDataCache.load_from_json(json, target) }
    end

    def self.class_scopes(model)
      @class_scopes[model.base_class]
    end

    # def self.sync_blocks
    #   # @sync_blocks[watch_model][sync_model][scope_name][...array of blocks...]
    #   @sync_blocks ||= Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = [] } } }
    # end


    def self.find(model, attribute, value)
      # will return the unique record with this attribute-value pair
      # value cannot be an association or aggregation

      model = model.base_class
      # already have a record with this attribute-value pair?
      record = @records[model].detect { |record| record.attributes[attribute] == value}
      unless record
        # if not, and then the record may be loaded, but not have this attribute set yet,
        # so find the id of of record with the attribute-value pair, and see if that is loaded.
        # find_in_db returns nil if we are not prerendering which will force us to create a new record
        # because there is no way of knowing the id.
        if attribute != model.primary_key and id = find_in_db(model, attribute, value)
          record = @records[model].detect { |record| record.id == id}
        end
        # if we don't have a record then create one
        (record = new(model)).vector = [model, ["find_by_#{attribute}", value]] unless record
        # and set the value
        record.sync_attribute(attribute, value)
        # and set the primary if we have one
        record.sync_attribute(model.primary_key, id) if id
      end
      # finally initialize and return the ar_instance
      record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
    end

    def self.find_by_object_id(model, object_id)
      @records[model].detect { |record| record.object_id == object_id }.ar_instance
    end

    def self.new_from_vector(model, aggregate_owner, *vector)
      # this is the equivilent of find but for associations and aggregations
      # because we are not fetching a specific attribute yet, there is NO communication with the
      # server.  That only happens during find.
      model = model.base_class

      # do we already have a record with this vector?  If so return it, otherwise make a new one.

      record = @records[model].detect { |record| record.vector == vector }
      unless record
        record = new model
        record.vector = vector
      end

      record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)

      if aggregate_owner
        record.aggregate_owner = aggregate_owner
        record.aggregate_attribute = vector.last
        aggregate_owner.attributes[vector.last] = record.ar_instance
      end

      record.ar_instance

    end

    def initialize(model, hash = {}, ar_instance = nil)
      @model = model
      @ar_instance = ar_instance
      @synced_attributes = {}
      @attributes = {}
      @changed_attributes = []
      @virgin = true
      records[model] << self
    end

    def find(*args)
      self.class.find(*args)
    end

    def new_from_vector(*args)
      self.class.new_from_vector(*args)
    end

    def primary_key
      @model.primary_key
    end

    def id
      attributes[primary_key]
    end

    def id=(value)
      # value can be nil if we are loading an aggregate otherwise check if it already exists
      if !(value and existing_record = records[@model].detect { |record| record.attributes[primary_key] == value})
        attributes[primary_key] = value
      else
        @ar_instance.instance_variable_set(:@backing_record, existing_record)
        existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
      end
      value
    end

    def attributes
      @last_access_at = Time.now
      @attributes
    end

    def reactive_get!(attribute, reload = nil)
      @virgin = false unless data_loading?
      unless @destroyed
        if @attributes.has_key? attribute
          attributes[attribute].notify if @attributes[attribute].is_a? DummyValue
          apply_method(attribute) if reload
        else
          apply_method(attribute)
        end
        React::State.get_state(self, attribute) unless data_loading?
        attributes[attribute]
      end
    end

    def reactive_set!(attribute, value)
      @virgin = false unless data_loading?
      return value if @destroyed || dont_update_attribute?(attribute, value)
      return attributes[attribute] if update_aggregate(attribute, value)
      value = update_relationships(attribute, value)
      update_attribute(attribute, value)
      value
    end

    def dont_update_attribute?(attribute, value)
      return false if attributes[attribute].is_a?(DummyValue)
      return false unless attributes.key?(attribute)
      return false if attributes[attribute] != value
      true
    end

    def update_attribute(attribute, *args)
      value = args[0]
      if args.count != 0 and data_loading?
        if (aggregation = model.reflect_on_aggregation(attribute)) and !(aggregation.klass < ActiveRecord::Base)
          @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
        else
          @synced_attributes[attribute] = value
        end
      end
      if @virgin
        attributes[attribute] = value if args.count != 0
        return
      end
      changed = if args.count == 0
        if (association = @model.reflect_on_association(attribute)) and association.collection?
          attributes[attribute] != @synced_attributes[attribute]
        else
          !attributes[attribute].backing_record.changed_attributes.empty?
        end
      elsif (association = @model.reflect_on_association(attribute)) and association.collection?
        value != @synced_attributes[attribute]
      else
        !@synced_attributes.has_key?(attribute) or @synced_attributes[attribute] != value
      end
      empty_before = changed_attributes.empty?
      if !changed
        changed_attributes.delete(attribute)
      elsif !changed_attributes.include?(attribute)
        changed_attributes << attribute
      end
      had_key = attributes.has_key? attribute
      current_value = attributes[attribute]
      attributes[attribute] = value if args.count != 0
      if !data_loading?
        React::State.set_state(self, attribute, value)
      elsif on_opal_client? and had_key and current_value.loaded? and current_value != value and args.count > 0  # this is to handle changes in already loaded server side methods
        React::State.set_state(self, attribute, value, true)
      end
      if empty_before != changed_attributes.empty?
        React::State.set_state(self, "!CHANGED!", !changed_attributes.empty?, true) unless on_opal_server? or data_loading?
        aggregate_owner.update_attribute(aggregate_attribute) if aggregate_owner
      end
    end

    def changed?(*args)
      if args.count == 0
        React::State.get_state(self, "!CHANGED!")
        !changed_attributes.empty?
      else
        React::State.get_state(self, args[0])
        changed_attributes.include? args[0]
      end
    end

    def errors
      @errors ||= ActiveModel::Error.new
    end

    # called when we have a newly created record, to initialize
    # any nil collections to empty arrays.  We can do this because
    # if its a brand new record, then any collections that are still
    # nil must not have any children.
    def initialize_collections
      if (!vector || vector.empty?) && id && id != ''
        @vector = [@model, ["find_by_#{@model.primary_key}", id]]
      end
      @model.reflect_on_all_associations.each do |assoc|
        if assoc.collection? && attributes[assoc.attribute].nil?
          ar_instance.send("#{assoc.attribute}=", [])
        end
      end
    end

    # sync! now will also initialize any nil collections
    def sync!(hash = {}) # does NOT notify (see saved! for notification)
      hash.each do |attr, value|
        @attributes[attr] = convert(attr, value)
      end
      @synced_attributes = {}
      @synced_attributes.each { |attribute, value| sync_attribute(key, value) }
      @changed_attributes = []
      @saving = false
      @errors = nil
      # set the vector and clear collections - this only happens when a new record is saved
      initialize_collections if (!vector || vector.empty?) && id && id != ''
      self
    end

    # this keeps the unscoped collection up to date.
    # @destroy_sync and @create_sync prevent multiple insertions
    # to collections that just have a count
    def sync_unscoped_collection!
      if destroyed
        return if @destroy_sync
        @destroy_sync = true
      else
        return if @create_sync
        @create_sync = true
      end
      model.unscoped << ar_instance
      @synced_with_unscoped = !@synced_with_unscoped
    end

    def sync_attribute(attribute, value)

      @synced_attributes[attribute] = attributes[attribute] = value

      #@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection

      if value.is_a? Collection
        @synced_attributes[attribute] = value.dup_for_sync
      elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
        value.backing_record.sync!
      elsif aggregation
        @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
      elsif !model.reflect_on_association(attribute)
        @synced_attributes[attribute] = JSON.parse(value.to_json)
      end

      @changed_attributes.delete(attribute)
      value
    end

    # helper so we can tell if model exists.  We need this so we can detect
    # if a record has local changes that are out of sync.
    def self.exists?(model, id)
      @records[model].detect { |record| record.attributes[model.primary_key] == id }
    end

    def revert
      @changed_attributes.dup.each do |attribute|
        @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
        @attributes.delete(attribute) unless @synced_attributes.key?(attribute)
      end
      @changed_attributes = []
      @errors = nil
    end

    def saving!
      React::State.set_state(self, self, :saving) unless data_loading?
      @saving = true
    end

    def errors!(errors)
      @saving = false
      @errors = errors and ActiveModel::Error.new(errors)
    end

    def saved!  # sets saving to false AND notifies
      @saving = false
      if !@errors or @errors.empty?
        React::State.set_state(self, self, :saved)
      elsif !data_loading?
        React::State.set_state(self, self, :error)
      end
      self
    end

    def saving?
      React::State.get_state(self, self)
      @saving
    end

    def new?
      !id and !vector
    end

    def find_association(association, id)
      inverse_of = association.inverse_of
      instance = if id
        find(association.klass, association.klass.primary_key, id)
      else
        new_from_vector(association.klass, nil, *vector, association.attribute)
      end
      instance_backing_record_attributes = instance.backing_record.attributes
      inverse_association = association.klass.reflect_on_association(inverse_of)
      if inverse_association.collection?
        instance_backing_record_attributes[inverse_of] = if id and id != ""
          Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
        else
          Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
        end unless instance_backing_record_attributes[inverse_of]
        instance_backing_record_attributes[inverse_of].replace [@ar_instance]
      else
        instance_backing_record_attributes[inverse_of] = @ar_instance
      end unless association.through_association? || instance_backing_record_attributes.key?(inverse_of)
      instance
    end

    def apply_method(method)
      # Fills in the value returned by sending "method" to the corresponding server side db instance
      if on_opal_server? and changed?
        log("Warning fetching virtual attributes (#{model.name}.#{method}) during prerendering on a changed or new model is not implemented.", :warning)
        # to implement this we would have to sync up any changes during prererendering with a set the cached models (see server_data_cache)
        # right now server_data cache is read only, BUT we could change this.  However it seems like a tails case.  Why would we create or update
        # a model during prerendering???
      end
      if !new?
        new_value = if association = @model.reflect_on_association(method)
          if association.collection?
            Collection.new(association.klass, @ar_instance, association, *vector, method)
          else
            find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
          end
        elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
          new_from_vector(aggregation.klass, self, *vector, method)
        elsif id and id != ''
          self.class.fetch_from_db([@model, [:find, id], *method]) || self.class.load_from_db(self, *(vector ? vector : [nil]), method)
        else  # its a attribute in an aggregate or we are on the client and don't know the id
          self.class.fetch_from_db([*vector, *method]) || self.class.load_from_db(self, *(vector ? vector : [nil]), method)
        end
        new_value = @attributes[method] if new_value.is_a? DummyValue and @attributes.has_key?(method)
        sync_attribute(method, new_value)
      elsif association = @model.reflect_on_association(method) and association.collection?
        @attributes[method] = Collection.new(association.klass, @ar_instance, association)
      elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
        @attributes[method] = aggregation.klass.new.tap do |aggregate|
          backing_record = aggregate.backing_record
          backing_record.aggregate_owner = self
          backing_record.aggregate_attribute = method
        end
      elsif !aggregation and method != model.primary_key
        if model.columns_hash[method]
          new_value = convert(method, model.columns_hash[method][:default])
        else
          unless @attributes.key?(method)
            log("Warning: reading from new #{model.name}.#{method} before assignment.  Will fetch value from server.  This may not be what you expected!!", :warning)
          end
          new_value = self.class.load_from_db(self, *(vector ? vector : [nil]), method)
          new_value = @attributes[method] if new_value.is_a?(DummyValue) && @attributes.key?(method)
        end
        sync_attribute(method, new_value)
      end
    end

    def self.infer_type_from_hash(klass, hash)
      klass = klass.base_class
      return klass unless hash
      type = hash[klass.inheritance_column]
      begin
        return Object.const_get(type)
      rescue Exception => e
        message = "Could not subclass #{@model_klass.model_name} as #{type}.  Perhaps #{type} class has not been required. Exception: #{e}"
        `console.error(#{message})`
      end if type
      klass
    end

    class << self
      attr_reader :outer_scopes

      def default_scope
        @class_scopes[:default_scope]
      end

      def unscoped
        @class_scopes[:unscoped]
      end

      def add_to_outer_scopes(item)
        @outer_scopes << item
      end
    end

    # when_not_saving will wait until reactive-record is not saving a model.
    # Currently there is no easy way to do this without polling.
    def self.when_not_saving(model)
      if @records[model].detect(&:saving?)
        poller = every(0.1) do
          unless @records[model].detect(&:saving?)
            poller.stop
            yield model
          end
        end
      else
        yield model
      end
    end

    # While evaluating scopes we want to catch any requests
    # to the server.  Once we catch any requests to the server
    # then all the further scopes in that chain will be made
    # at the server.

    class << self
      class DbRequestMade < RuntimeError; end

      def catch_db_requests(return_val = nil)
        @catch_db_requests = true
        yield
      rescue DbRequestMade => e
        puts "Warning request for server side data during scope evaluation: #{e.message}"
        return_val
      ensure
        @catch_db_requests = false
      end

      alias pre_synchromesh_load_from_db load_from_db

      def load_from_db(*args)
        raise DbRequestMade, args if @catch_db_requests
        pre_synchromesh_load_from_db(*args)
      end
    end

    def destroy_associations
      @destroyed = false
      model.reflect_on_all_associations.each do |association|
        if association.collection?
          attributes[association.attribute].replace([]) if attributes[association.attribute]
        else
          @ar_instance.send("#{association.attribute}=", nil)
        end
      end
      @destroyed = true
    end

  end
end