# frozen_string_literal: true

require 'concurrent'

module Memorb
  module Integration
    class << self

      def integrate_with!(target)
        unless target.is_a?(::Class)
          raise InvalidIntegrationError, 'integration target must be a class'
        end
        INTEGRATIONS.fetch(target) do
          new(target).tap do |integration|
            target.singleton_class.prepend(IntegratorClassMethods)
            target.prepend(integration)
          end
        end
      end

      def integrated?(target)
        INTEGRATIONS.has?(target)
      end

      def [](integrator)
        INTEGRATIONS.read(integrator)
      end

      private

      INTEGRATIONS = KeyValueStore.new

      def new(integrator)
        mixin = ::Module.new do
          def initialize(*)
            agent = Integration[self.class].create_agent(self)
            define_singleton_method(:memorb) { agent }
            super
          end

          class << self

            def register(*names, &block)
              names_present = !names.empty?
              block_present = !block.nil?

              if names_present && block_present
                raise ::ArgumentError,
                  'register may not be called with both a method name and a block'
              elsif names_present
                names.flatten.each { |n| _register_from_name(_identifier(n)) }
              elsif block_present
                _register_from_block(&block)
              else
                raise ::ArgumentError,
                  'register must be called with either a method name or a block'
              end

              nil
            end

            def registered_methods
              _identifiers_to_symbols(_registrations.keys)
            end

            def registered?(name)
              _registered?(_identifier(name))
            end

            def enable(name)
              _enable(_identifier(name))
            end

            def disable(name)
              _disable(_identifier(name))
            end

            def enabled_methods
              _identifiers_to_symbols(_overrides.keys)
            end

            def disabled_methods
              registered_methods - enabled_methods
            end

            def enabled?(name)
              _enabled?(_identifier(name))
            end

            def auto_register?
              _auto_registration.value > 0
            end

            def auto_register!(&block)
              raise ::ArgumentError, 'a block must be provided' if block.nil?
              _auto_registration.update { |v| [0, v].max + 1 }
              begin
                block.call
              ensure
                _auto_registration.update { |v| [0, v - 1].max }
              end
            end

            def prepended(target)
              _check_target!(target)
              super
            end

            def included(*)
              raise InvalidIntegrationError,
                'an integration must be applied with `prepend`, not `include`'
            end

            def name
              [:name, :inspect, :object_id].each do |m|
                next unless integrator.respond_to?(m)
                base_name = integrator.public_send(m)
                return "Memorb:#{ base_name }" if base_name
              end
            end

            alias_method :inspect, :name

            def create_agent(integrator_instance)
              Agent.new(integrator_instance.object_id)
            end

            private

            def _check_target!(target)
              unless target.equal?(integrator)
                raise MismatchedTargetError
              end
            end

            def _identifier(name)
              MethodIdentifier.new(name)
            end

            def _identifiers_to_symbols(method_ids)
              method_ids.map(&:to_sym)
            end

            def _register_from_name(method_id)
              _registrations.write(method_id, nil)
              _enable(method_id)
            end

            def _register_from_block(&block)
              auto_register! do
                integrator.class_eval(&block)
              end
            end

            def _registered?(method_id)
              _registrations.keys.include?(method_id)
            end

            def _enable(method_id)
              return unless _registered?(method_id)

              visibility = _integrator_instance_method_visibility(method_id)
              return if visibility.nil?

              _overrides.fetch(method_id) do
                _define_override(method_id)
                _set_visibility(visibility, method_id.to_sym)
              end
            end

            def _disable(method_id)
              _overrides.forget(method_id)
              _remove_override(method_id)
            end

            def _enabled?(method_id)
              _overrides.keys.include?(method_id)
            end

            def _remove_override(method_id)
              # Ruby will raise an exception if the method doesn't exist.
              # Catching it is the safest thing to do for thread-safety.
              # The alternative would be to check the list if it were
              # present or not, but the read could be outdated by the time
              # that we tried to remove the method and this exception
              # wouldn't be caught.
              remove_method(method_id.to_sym)
            rescue ::NameError => e
              # If this exception was for something else, it should be re-raised.
              unless RubyCompatibility.name_error_matches(e, method_id, self)
                raise e
              end
            end

            def _define_override(method_id)
              define_method(method_id.to_sym) do |*args, &block|
                memorb.method_store
                  .fetch(method_id) { KeyValueStore.new }
                  .fetch(args.hash) { super(*args, &block) }
              end
            end

            def _integrator_instance_method_visibility(method_id)
              [:public, :protected, :private].find do |visibility|
                methods = integrator.send(:"#{ visibility }_instance_methods")
                methods.include?(method_id.to_sym)
              end
            end

            def _set_visibility(visibility, name)
              send(visibility, name)
              visibility
            end

            def _registrations
              RubyCompatibility.module_constant(self, :registrations)
            end

            def _overrides
              RubyCompatibility.module_constant(self, :overrides)
            end

            def _auto_registration
              RubyCompatibility.module_constant(self, :auto_registration)
            end

          end
        end

        RubyCompatibility.module_constant_set(mixin, :registrations, KeyValueStore.new)
        RubyCompatibility.module_constant_set(mixin, :overrides, KeyValueStore.new)
        RubyCompatibility.module_constant_set(mixin,
          :auto_registration,
          ::Concurrent::AtomicFixnum.new,
        )

        RubyCompatibility.define_method(mixin.singleton_class, :integrator) do
          integrator
        end

        mixin
      end

    end
  end
end