module Playwright
  class PlaywrightApi
    # Wrap ChannelOwner / ApiImplementation.
    # Playwright::ChannelOwners::XXXXX will be wrapped as Playwright::XXXXX
    # Playwright::YYYYImpl will be wrapped as Playwright::YYYY
    # Playwright::XXXXX is automatically generated by development/generate_api
    #
    # @param channel_owner [ChannelOwner|ApiImplementation]
    # @note Intended for internal use only.
    def self.wrap(channel_owner_or_api_implementation)
      case channel_owner_or_api_implementation
      when ChannelOwner
        ChannelOwnerWrapper.new(channel_owner_or_api_implementation).wrap
      when ApiImplementation
        ApiImplementationWrapper.new(channel_owner_or_api_implementation).wrap
      else
        channel_owner_or_api_implementation
      end
    end

    # Unwrap ChannelOwner / ApiImplementation.
    # @note Intended for internal use only.
    def self.unwrap(api)
      case api
      when PlaywrightApi
        api.instance_variable_get(:@impl)
      else
        api
      end
    end

    class ChannelOwnerWrapper
      def initialize(impl)
        impl_class_name = impl.class.name
        unless impl_class_name.include?("::ChannelOwners::")
          raise "#{impl_class_name} is not ChannelOwners"
        end

        @impl = impl
      end

      def wrap
        api_class = detect_class_for(@impl.class)
        if api_class
          @impl._api ||= api_class.new(@impl)
        else
          raise NotImplementedError.new("Playwright::#{expected_class_name_for(@impl.class)} is not implemented")
        end
      end

      private

      def expected_class_name_for(klass)
        klass.name.split("::ChannelOwners::").last
      end

      def superclass_exist?(klass)
        ![::Playwright::ChannelOwner, Object].include?(klass.superclass)
      end

      def detect_class_for(klass)
        class_name = expected_class_name_for(klass)
        if ::Playwright.const_defined?(class_name)
          ::Playwright.const_get(class_name)
        elsif superclass_exist?(klass)
          detect_class_for(klass.superclass)
        else
          nil
        end
      end
    end

    class ApiImplementationWrapper
      def initialize(impl)
        impl_class_name = impl.class.name
        unless impl_class_name.end_with?("Impl")
          raise "#{impl_class_name} is not Impl"
        end

        @impl = impl
      end

      def wrap
        api_class = detect_class_for(@impl.class)
        if api_class
          api_class.new(@impl)
        else
          raise NotImplementedError.new("Playwright::#{expected_class_name_for(@impl.class)} is not implemented")
        end
      end

      private

      def expected_class_name_for(klass)
        # KeyboardImpl -> Keyboard
        # MouseImpl -> Mouse
        klass.name[0...-4].split("::").last
      end

      def detect_class_for(klass)
        class_name = expected_class_name_for(klass)
        if ::Playwright.const_defined?(class_name)
          ::Playwright.const_get(class_name)
        else
          nil
        end
      end
    end

    # @param impl [Playwright::ChannelOwner|Playwright::ApiImplementation]
    def initialize(impl)
      @impl = impl
    end

    # @param block [Proc]
    private def wrap_block_call(block)
      return nil unless block.is_a?(Proc)

      -> (*args) {
        wrapped_args = args.map { |arg| wrap_impl(arg) }
        block.call(*wrapped_args)
      }
    end

    private def wrap_impl(object, visited: {})
      if object.is_a?(Array)
        unless visited[object]
          visited[object] = []
          object.each { |obj| visited[object] << wrap_impl(obj) }
        end
        visited[object]
      elsif object.is_a?(Hash)
        unless visited[object]
          visited[object] = {}
          object.each do |key, obj|
            visited[object][key] = wrap_impl(obj, visited: visited)
          end
        end
        visited[object]
      else
        ::Playwright::PlaywrightApi.wrap(object)
      end
    end

    private def unwrap_impl(object, visited: {})
      if object.is_a?(Array)
        unless visited[object]
          visited[object] = []
          object.each { |obj| visited[object] << unwrap_impl(obj) }
        end
        visited[object]
      elsif object.is_a?(Hash)
        unless visited[object]
          visited[object] = {}
          object.each do |key, obj|
            visited[object][key] = unwrap_impl(obj, visited: visited)
          end
        end
        visited[object]
      elsif object.is_a?(PlaywrightApi)
        ::Playwright::PlaywrightApi.unwrap(object)
      else
        object
      end
    end
  end
end
