# frozen_string_literal: true
module ActiveModelCachers
  module ActiveRecord
    class Cacher
      @defined_map = {}

      class << self
        def get_cacher_klass(klass)
          @defined_map[klass] ||= create_cacher_klass_at(klass)
        end

        def define_cacher_method(attr, primary_key, service_klasses)
          cacher_klass = get_cacher_klass(attr.klass)
          method = attr.column
          return cacher_klass.define_find_by(attr, primary_key, service_klasses) if method == nil
          cacher_klass.send(:define_methods, method, {
            method            => ->{ exec_by(attr, primary_key, service_klasses, :get) },
            "peek_#{method}"  => ->{ exec_by(attr, primary_key, service_klasses, :peek) },
            "clean_#{method}" => ->{ exec_by(attr, primary_key, service_klasses, :clean_cache) },
          })
        end

        def define_find_by(attr, primary_key, service_klasses)
          if @find_by_mapping == nil
            @find_by_mapping = {}
            define_methods(:find_by, {
              :find_by   => ->(args){ exec_find_by(args, :get) },
              :peek_by   => ->(args){ exec_find_by(args, :peek) },
              :clean_by  => ->(args){ exec_find_by(args, :clean_cache) },
            })
          end
          @find_by_mapping[primary_key] = [attr, service_klasses]
        end

        private

        def define_methods(attribute, methods_mapping)
          if attributes.include?(attribute)
            methods_mapping.keys.each{|s| undef_method(s) }
          else
            attributes << attribute
          end
          methods_mapping.each{|method, block| define_method(method, &block) }
        end

        def get_data_from_find_by_mapping(primary_key)
          return if @find_by_mapping == nil
          return @find_by_mapping[primary_key]
        end

        def create_cacher_klass_at(target)
          cacher_klass = Class.new(self)
          cacher_klass.instance_variable_set(:@find_by_mapping, nil) # to remove warning: instance variable @find_by_mapping not initialized
          cacher_klass.define_singleton_method(:attributes){ @attributes ||= [] }
          cacher_klass.send(:define_method, 'peek'){|column| send("peek_#{column}") }
          cacher_klass.send(:define_method, 'clean'){|column| send("clean_#{column}") }

          target.define_singleton_method(:cacher_at){|id| cacher_klass.new(id: id) }
          target.define_singleton_method(:cacher){ cacher_klass.new }
          target.send(:define_method, :cacher){ cacher_klass.new(model: self) }
          return cacher_klass
        end
      end

      def initialize(id: nil, model: nil)
        @id = id
        @model = model
      end

      private

      def exec_find_by(args, method) # e.g. args = {course_id: xx}
        primary_key = args.keys.sort.first # Support only one key now.
        attr, service_klasses = self.class.send(:get_data_from_find_by_mapping, primary_key)
        return if service_klasses == nil
        return exec_by(attr, primary_key, service_klasses, method, data: args[primary_key])
      end

      def exec_by(attr, primary_key, service_klasses, method, data: nil)
        bindings = [@model]
        reflects = (attr.belongs_to? ? [] : [attr.reflect])
        if @model and attr.association?
          if attr.belongs_to? and method != :clean_cache # no need to load binding when just cleaning cache
            association = @model.association(attr.column)
            bindings << association.load_target if association.loaded?
          end
        end
        data ||= (@model ? @model.send(primary_key) : nil) || @id
        service_klasses.each_with_index do |service_klass, index|
          data = service_klass.instance(data).send(method, binding: bindings[index], reflect: reflects[index])
          return if data == nil
        end
        return data
      end
    end
  end
end
