module LightIO
  module Monkey
    class PatchError < StandardError
    end

    IO_PATCH_CONSTANTS = %w{IO File Socket Socket::Ifaddr TCPServer TCPSocket BasicSocket Addrinfo IPSocket UDPSocket UNIXSocket UNIXServer OpenSSL::SSL::SSLSocket}.freeze
    THREAD_PATCH_CONSTANTS = %w{Thread ThreadGroup Queue SizedQueue ConditionVariable Mutex ThreadsWait}.freeze

    @patched

    class << self
      def patch_all!
        patch_thread!
        patch_io!
        patch_kernel!
        nil
      end

      def unpatch_all!
        unpatch_thread!
        unpatch_io!
        unpatch_kernel!
        nil
      end

      def patched?(obj)
        patched.key?(obj) && !patched[obj]&.empty?
      end

      def patch_thread!
        require 'thread'
        THREAD_PATCH_CONSTANTS.each {|klass_name| patch!(klass_name)}
        patch_method!(Timeout, :timeout, LightIO::Timeout.method(:timeout))
        nil
      end

      def unpatch_thread!
        require 'thread'
        THREAD_PATCH_CONSTANTS.each {|klass_name| unpatch!(klass_name)}
        unpatch_method!(Timeout, :timeout)
        nil
      end

      def patch_io!
        require 'socket'
        IO_PATCH_CONSTANTS.each {|klass_name| patch!(klass_name)}
        patch_method!(Process, :spawn, LightIO.method(:spawn).to_proc)
        nil
      end

      def unpatch_io!
        require 'socket'
        IO_PATCH_CONSTANTS.each {|klass_name| unpatch!(klass_name)}
        unpatch_method!(Process, :spawn)
        nil
      end

      def patch_kernel!
        patch_kernel_method!(:sleep, LightIO.method(:sleep))
        patch_kernel_method!(:select, LightIO::Library::IO.method(:select))
        patch_kernel_method!(:open, LightIO::Library::File.method(:open).to_proc)
        patch_kernel_method!(:spawn, LightIO.method(:spawn).to_proc)
        patch_kernel_method!(:`, LightIO.method(:`).to_proc)
        patch_kernel_method!(:system, LightIO.method(:system).to_proc)
        %w{gets readline readlines}.each do |method|
          patch_kernel_method!(method.to_sym, LightIO.method(method.to_sym).to_proc)
        end
        nil
      end

      def unpatch_kernel!
        unpatch_kernel_method!(:sleep)
        unpatch_kernel_method!(:select)
        unpatch_kernel_method!(:open)
        unpatch_kernel_method!(:spawn)
        unpatch_kernel_method!(:`)
        unpatch_kernel_method!(:system)
        %w{gets readline readlines}.each do |method|
          unpatch_kernel_method!(method.to_sym, LightIO.method(method.to_sym).to_proc)
        end
        nil
      end

      private
      def patch!(klass_name)
        klass = Object.const_get(klass_name)
        raise PatchError, "already patched constant #{klass}" if patched?(klass)
        patched[klass] = {}
        class_methods_module = find_class_methods_module(klass_name)
        methods = class_methods_module && find_monkey_patch_class_methods(klass_name)
        return unless class_methods_module && methods
        methods.each do |method_name|
          method = class_methods_module.instance_method(method_name)
          patch_method!(klass, method_name, method)
        end
      rescue
        patched.delete(klass)
        raise
      end

      def unpatch!(klass_name)
        klass = Object.const_get(klass_name)
        raise PatchError, "can't find patched constant #{klass}" unless patched?(klass)
        unpatch_method!(klass, :new)
        find_monkey_patch_class_methods(klass_name).each do |method_name|
          unpatch_method!(klass, method_name)
        end
        patched.delete(klass)
      end

      def patch_kernel_method!(method_name, method)
        patch_method!(Kernel, method_name, method)
        patch_instance_method!(Kernel, method_name, method)
      end

      def unpatch_kernel_method!(method_name)
        unpatch_method!(Kernel, method_name)
        unpatch_instance_method!(Kernel, method_name)
      end

      def find_class_methods_module(klass_name)
        LightIO::Module.const_get("#{klass_name}::ClassMethods", false)
      rescue NameError
        nil
      end

      def find_monkey_patch_class_methods(klass_name)
        find_class_methods_module(klass_name).instance_methods
      end

      def patched_method?(obj, method)
        patched?(obj) && patched[obj].key?(method)
      end

      def patched_methods(const)
        patched[const] ||= {}
      end

      def patch_method!(const, method, patched_method)
        raise PatchError, "already patched method #{const}.#{method}" if patched_method?(const, method)
        patched_methods(const)[method] = patched_method
        const.send(:define_singleton_method, method, patched_method)
        nil
      end

      def unpatch_method!(const, method)
        raise PatchError, "can't find patched method #{const}.#{method}" unless patched_method?(const, method)
        origin_method = patched_methods(const).delete(method)
        const.send(:define_singleton_method, method, origin_method)
        nil
      end

      def patched_instance_method?(obj, method)
        patched_instance_methods(obj).key?(method)
      end

      def patched_instance_methods(const)
        (patched[:instance_methods] ||= {})[const] ||= {}
      end

      def patch_instance_method!(const, method, patched_method)
        raise PatchError, "already patched method #{const}.#{method}" if patched_instance_method?(const, method)
        patched_instance_methods(const)[method] = patched_method
        const.send(:define_method, method, patched_method)
        nil
      end

      def unpatch_instance_method!(const, method)
        raise PatchError, "can't find patched method #{const}.#{method}" unless patched_instance_method?(const, method)
        origin_method = patched_instance_methods(const).delete(method)
        const.send(:define_method, method, origin_method)
        nil
      end

      def patched
        @patched ||= {}
      end
    end
  end
end