module ReactiveRecord
  class Base
    include BackingRecordInspector
    include Setters
    include Getters
    extend  LookupTables

    # 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 (the default value for the attribute,
    # 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 :being_destroyed
    attr_accessor :updated_during
    attr_accessor :synced_attributes
    attr_accessor :virgin
    attr_reader   :attributes

    # While data is being loaded from the server certain internal behaviors need to change
    # for example 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.find_locally(model, attrs, new_only: nil)
      if (id_to_find = attrs[model.primary_key])
        !new_only && lookup_by_id(model, id_to_find)
      else
        @records[model.base_class].detect do |r|
          (r.new? || !new_only) &&
            !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
        end
      end
    end

    def self.find_by_id(model, id)
      find(model, model.primary_key => id)
    end

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

      # add the inheritance column if this is an STI subclass

      attrs = model.__hyperstack_preprocess_attrs(attrs)

      model = model.base_class
      primary_key = model.primary_key

      # already have a record with these attribute-value pairs?

      record = find_locally(model, attrs)

      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 !attrs.key?(primary_key) && (id = find_in_db(model, attrs))
          record = lookup_by_id(model, id) # @records[model].detect { |record| record.id == id}
          attrs = attrs.merge primary_key => id
        end
        # if we don't have a record then create one
        record ||= set_vector_lookup(new(model), [model, *find_by_vector(attrs)])
        record.sync_attributes(attrs)
      end
      # finally initialize and return the ar_instance
      record.set_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.

      return DummyPolymorph.new(vector) unless model

      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 }
      record = lookup_by_vector(vector)
      unless record
        record = new model
        set_vector_lookup(record, vector)
      end

      record.set_ar_instance!

      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.base_class] << self
      Base.set_object_id_lookup(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 && (existing_record = records[@model].detect { |record| record.attributes[primary_key] == value}))
      if !(value && (existing_record = Base.lookup_by_id(model, value)))
        @attributes[primary_key] = value
        Base.set_id_lookup(self)
      else
        @ar_instance.instance_variable_set(:@backing_record, existing_record)
        existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
      end
      @id = value
      value
    end

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

    def changed_attributes_and_values
      Hash[changed_attributes.collect do |attr|
        [attr, @attributes[attr]] if column_type(attr)
      end.compact]
    end

    def changes
      Hash[changed_attributes.collect do |attr|
        [attr, [@synced_attributes[attr], @attributes[attr]]] if column_type(attr)
      end.compact]
    end

    def errors
      @errors ||= ActiveModel::Errors.new(ar_instance)
    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 != ''
        Base.set_vector_lookup(self, [@model, *find_by_vector(@model.primary_key => id)])
      end
      Base.load_data do
        @model.reflect_on_all_associations.each do |assoc|
          next if !assoc.collection? || @attributes[assoc.attribute]
          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 = {}
      hash.each { |attr, value| sync_attribute(attr, convert(attr, value)) }
      @changed_attributes = []
      @saving = false
      errors.clear
      # 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._internal_push ar_instance
      @synced_with_unscoped = !@synced_with_unscoped
    end

    def sync_attributes(attrs)
      attrs.each { |attr, value| sync_attribute(attr, value) }
      self
    end

    def sync_attribute(attribute, value)
      @synced_attributes[attribute] = @attributes[attribute] = value
      Base.set_id_lookup(self) if attribute == primary_key

      #@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)) && (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)
      Base.lookup_by_id(model, id)
    end

    def id_loaded?
      @id
    end

    def loaded_id=(id)
      @id = 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.clear
    end

    def saving!
      Hyperstack::Internal::State::Variable.set(self, self, :saving) unless data_loading?
      @saving = true
    end

    def errors!(hash, saving = false)
      @errors_at_last_sync = hash if saving
      notify_waiting_for_save
      errors.clear && return unless hash
      errors.non_reactive_clear
      hash.each do |attribute, messages|
        messages.each do |message|
          errors.add(attribute, message)
        end
      end
    end

    def revert_errors!
      errors!(@errors_at_last_sync)
    end

    def saved!(save_only = nil) # sets saving to false AND notifies
      notify_waiting_for_save
      return self if save_only
      if errors.empty?
        Hyperstack::Internal::State::Variable.set(self, self, :saved)
      elsif !data_loading?
        Hyperstack::Internal::State::Variable.set(self, self, :error)
      end
      self
    end

    def self.when_not_saving(model, &block)
      if @records[model].detect(&:saving?)
        wait_for_save(model, &block)
      else
        yield model
      end
    end

    def notify_waiting_for_save
      @saving = false
      self.class.notify_waiting_for_save(model)
    end

    def self.notify_waiting_for_save(model)
      waiters = waiting_for_save(model)
      return if waiters.empty? || @records[model].detect(&:saving?)
      waiters.each { |waiter| waiter.call model }
      clear_waiting_for_save(model)
    end

    def saving?
      Hyperstack::Internal::State::Variable.get(self, self)
      @saving
    end

    def new?
      !id && !vector
    end

    def set_ar_instance!
      klass = self.class.infer_type_from_hash(model, @attributes)
      @ar_instance = klass._new_without_sti_type_cast(self) unless @ar_instance.class == klass
      @ar_instance
    end

    class << self
      def 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 #{klass} as #{type}.  Perhaps #{type} class has not been required. Exception: #{e}"
          `console.error(#{message})`
        end unless !type || type == ''
        klass
      end

      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

      # 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 DbRequestMade < RuntimeError; end

      def catch_db_requests(return_val = nil)
        @catch_db_requests = true
        yield
      rescue DbRequestMade => e
        Hyperstack::Component::IsomorphicHelpers.log "Warning: request for server side data during scope evaluation: #{e.message}", :warning
        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
      @being_destroyed = true
      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
