module ActiveRecord
  module InstanceMethods

    # if methods are missing, then they must be a column, which we look up
    # in the columns_hash.

    # For effeciency all attributes will by default have all the methods defined,
    # when the class is loaded.  See define_attribute_methods class method.
    # However a model may override the attribute methods definition, but then call
    # super.  Which will result in the method missing call.

    # When loading data from the server we do NOT want to call overridden methods
    # so we also define a _hyperstack_internal_setter_... method for each attribute
    # as well as for belongs_to relationships, server_methods, and the special
    # type and model_name methods.  See the ClassMethods module for details.

    # meanwhile in Opal 1.0 there is currently an issue where the name of the method
    # does not get passed to method_missing from super.
    # https://github.com/opal/opal/issues/2165
    # So the following hack works around that issue until its fixed.

    %x{
      Opal.orig_find_super_dispatcher = Opal.find_super_dispatcher
      Opal.find_super_dispatcher = function(obj, mid, current_func, defcheck, allow_stubs) {
        Opal.__name_of_super = mid;
        return Opal.orig_find_super_dispatcher(obj, mid, current_func, defcheck, allow_stubs)
      }
    }

    def method_missing(missing, *args, &block)
      missing ||= `Opal.__name_of_super`
      column = self.class.columns_hash.detect { |name, *| missing =~ /^#{name}/ }
      if column
        name = column[0]
        case missing
        when /\!\z/ then @backing_record.get_attr_value(name, true)
        when /\=\z/ then @backing_record.set_attr_value(name, *args)
        when /\_changed\?\z/ then @backing_record.changed?(name)
        when /\?/ then @backing_record.get_attr_value(name, nil).present?
        else @backing_record.get_attr_value(name, nil)
        end
      else
        super
      end
    end

    # the system assumes that there is "virtual" model_name and type attribute so
    # we define the internal setter here.  If the user defines some other attributes
    # or uses these names no harm is done since the exact same method would have been
    # defined by the define_attribute_methods class method anyway.
    %i[model_name type].each do |attr|
      define_method("_hyperstack_internal_setter_#{attr}") do |val|
        @backing_record.set_attr_value(:model_name, val)
      end
    end

    def inspect
      "<#{model_name}:#{ReactiveRecord::Operations::Base::FORMAT % to_key} "\
      "(#{ReactiveRecord::Operations::Base::FORMAT % object_id}) "\
      "#{backing_record.inspection_details} >"
    end

    attr_reader :backing_record

    def attributes
      @backing_record.attributes
    end

    def changed_attributes
      backing_record.changed_attributes_and_values
    end

    def changes
      backing_record.changes
    end

    def initialize(hash = {})
      if hash.is_a? ReactiveRecord::Base
        @backing_record = hash
      else
        # standard active_record new -> creates a new instance, primary key is ignored if present
        # we have to build the backing record first then initialize it so associations work correctly
        @backing_record = ReactiveRecord::Base.new(self.class, {}, self)
        if self.class.inheritance_column && !hash.key?(self.class.inheritance_column)
          hash[self.class.inheritance_column] = self.class.name
        end
        @backing_record.instance_eval do
          h = {}
          hash.each do |a, v|
            a = model._dealias_attribute(a)
            h[a] = convert(a, v).itself
          end
          self.class.load_data do
            h.each do |attribute, value|
              next if attribute == :id
              @ar_instance[attribute] = value
              changed_attributes << attribute
            end
          end
        end
      end
    end

    def primary_key
      self.class.primary_key
    end

    def id
      @backing_record.get_primary_key_value
    end

    def id=(value)
      @backing_record.id = value
    end

    def id?
      id.present?
    end

    def model_name
      # in reality should return ActiveModel::Name object, blah blah
      self.class.model_name
    end

    def revert
      @backing_record.revert
    end

    def changed?(attr = nil)
      @backing_record.changed?(*attr)
    end

    def dup
      self.class.new(self.attributes)
    end

    def ==(ar_instance)
      return true  if @backing_record == ar_instance.instance_eval { @backing_record }
      return false unless ar_instance.is_a?(ActiveRecord::Base)
      return false if ar_instance.new_record?
      return false unless self.class.base_class == ar_instance.class.base_class
      id == ar_instance.id
    end

    def [](attr)
      send(attr)
    end

    def []=(attr, val)
      send("#{attr}=", val)
    end

    def itself
      # this is useful when you just want to get a handle on record instance
      # in the ReactiveRecord.load method
      id # force load of id...
      # if self.class.columns_hash.keys.include?(self.class.inheritance_column) &&
      #    (klass = self[self.class.inheritance_column]).loaded?
      #   Object.const_get(klass).new(attributes)
      # else
        self
      # end
    end

    def increment!(attr)
      load(attr).then { |current_value| update(attr => current_value + 1) }
    end

    def decrement!(attr)
      load(attr).then { |current_value| update(attr => current_value - 1) }
    end

    def load(*attributes, &block)
      first_time = true
      ReactiveRecord.load do
        results = attributes.collect { |attr| send("#{attr}#{'!' if first_time}") }
        results = yield(*results) if block
        first_time = false
        block.nil? && results.count == 1 ? results.first : results
      end
    end

    def save(opts = {}, &block)
      @backing_record.save_or_validate(true, opts.has_key?(:validate) ? opts[:validate] : true, opts[:force], &block)
    end

    def validate(opts = {}, &block)
      @backing_record.save_or_validate(false, true, opts[:force]).then do
        if block
          yield @backing_record.ar_instance
        else
          @backing_record.ar_instance
        end
      end
    end

    def valid?
      errors.reactive_empty?
    end

    def saving?
      @backing_record.saving?
    end

    def destroy(&block)
      @backing_record.destroy(&block)
    end

    def destroyed?
      @backing_record.destroyed
    end

    def new_record?
      @backing_record.new?
    end

    alias new? new_record?

    def errors
      Hyperstack::Internal::State::Variable.get(@backing_record, @backing_record)
      @backing_record.errors
    end

    def to_key
      @backing_record.object_id
    end

    def update_attribute(attr, value, &block)
      send("#{attr}=", value)
      save(validate: false, &block)
    end

    def update(attrs = {}, &block)
      attrs.each { |attr, value| send("#{attr}=", value) }
      save(&block)
    end

    def <=>(other)
      id.to_i <=> other.id.to_i
    end

    def becomes(klass)
      klass._new_without_sti_type_cast(backing_record)
    end

    def becomes!(klass)
      self[self.class.inheritance_column] = klass.name
      becomes(klass)
    end

    def cast_to_current_sti_type
      @backing_record.set_ar_instance!
    end
  end
end
