# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
  module WebDriver
    module PointerActions
      attr_writer :default_move_duration

      #
      # The overridable duration for movement used by methods in this module
      #

      def default_move_duration
        @default_move_duration ||= 0.25 # 250 milliseconds
      end

      #
      # Presses (without releasing) at the current location of the PointerInput device. This is equivalent to:
      #
      #   driver.action.click_and_hold(nil)
      #
      # @example Clicking and holding at the current location
      #
      #    driver.action.pointer_down(:left).perform
      #
      # @param [Selenium::WebDriver::Interactions::PointerPress::BUTTONS] button the button to press.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be pressed
      # @return [W3CActionBuilder] A self reference.
      #

      def pointer_down(button, device: nil)
        button_action(button, action: :create_pointer_down, device: device)
      end

      #
      # Releases the pressed mouse button at the current mouse location of the PointerInput device.
      #
      # @example Releasing a button after clicking and holding
      #
      #    driver.action.pointer_down(:left).pointer_up(:left).perform
      #
      # @param [Selenium::WebDriver::Interactions::PointerPress::BUTTONS] button the button to release.
      # @param [Symbol || String] device optional name of the PointerInput device with the button that will
      #   be released
      # @return [W3CActionBuilder] A self reference.
      #

      def pointer_up(button, device: nil)
        button_action(button, action: :create_pointer_up, device: device)
      end

      #
      # Moves the mouse to the middle of the given element. The element is scrolled into
      # view and its location is calculated using getBoundingClientRect.  Then the
      # mouse is moved to optional offset coordinates from the element.
      #
      # This is adapted to be backward compatible from non-W3C actions. W3C calculates offset from the center point
      # of the element
      #
      # Note that when using offsets, both coordinates need to be passed.
      #
      # @example Scroll element into view and move the mouse to it
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.move_to(el).perform
      #
      # @example
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.move_to(el, 100, 100).perform
      #
      # @param [Selenium::WebDriver::Element] element to move to.
      # @param [Integer] right_by Optional offset from the top-left corner. A negative value means
      #   coordinates to the left of the element.
      # @param [Integer] down_by Optional offset from the top-left corner. A negative value means
      #   coordinates above the element.
      # @param [Symbol || String] device optional name of the PointerInput device to move.
      # @return [W3CActionBuilder] A self reference.
      #

      def move_to(element, right_by = nil, down_by = nil, device: nil)
        pointer = get_pointer(device)
        # New actions offset is from center of element
        if right_by || down_by
          size = element.size
          left_offset = (size[:width] / 2).to_i
          top_offset = (size[:height] / 2).to_i
          left = -left_offset + (right_by || 0)
          top = -top_offset + (down_by || 0)
        else
          left = 0
          top = 0
        end
        pointer.create_pointer_move(duration: default_move_duration,
                                    x: left,
                                    y: top,
                                    element: element)
        tick(pointer)
        self
      end

      #
      # Moves the mouse from its current position by the given offset.
      # If the coordinates provided are outside the viewport (the mouse will
      # end up outside the browser window) then the viewport is scrolled to
      # match.
      #
      # @example Move the mouse to a certain offset from its current position
      #
      #    driver.action.move_by(100, 100).perform
      #
      # @param [Integer] right_by horizontal offset. A negative value means moving the mouse left.
      # @param [Integer] down_by vertical offset. A negative value means moving the mouse up.
      # @param [Symbol || String] device optional name of the PointerInput device to move
      # @return [W3CActionBuilder] A self reference.
      # @raise [MoveTargetOutOfBoundsError] if the provided offset is outside the document's boundaries.
      #

      def move_by(right_by, down_by, device: nil)
        pointer = get_pointer(device)
        pointer.create_pointer_move(duration: default_move_duration,
                                    x: Integer(right_by),
                                    y: Integer(down_by),
                                    origin: Interactions::PointerMove::POINTER)
        tick(pointer)
        self
      end

      #
      # Moves the mouse to a given location in the viewport.
      # If the coordinates provided are outside the viewport (the mouse will
      # end up outside the browser window) then the viewport is scrolled to
      # match.
      #
      # @example Move the mouse to a certain position in the viewport
      #
      #    driver.action.move_to_location(100, 100).perform
      #
      # @param [Integer] x horizontal position. Equivalent to a css 'left' value.
      # @param [Integer] y vertical position. Equivalent to a css 'top' value.
      # @param [Symbol || String] device optional name of the PointerInput device to move
      # @return [W3CActionBuilder] A self reference.
      # @raise [MoveTargetOutOfBoundsError] if the provided x or y value is outside the document's boundaries.
      #

      def move_to_location(x, y, device: nil)
        pointer = get_pointer(device)
        pointer.create_pointer_move(duration: default_move_duration,
                                    x: Integer(x),
                                    y: Integer(y),
                                    origin: Interactions::PointerMove::VIEWPORT)
        tick(pointer)
        self
      end

      #
      # Clicks (without releasing) in the middle of the given element. This is
      # equivalent to:
      #
      #   driver.action.move_to(element).click_and_hold
      #
      # @example Clicking and holding on some element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click_and_hold(el).perform
      #
      # @param [Selenium::WebDriver::Element] element the element to move to and click.
      # @param [Symbol || String] device optional name of the PointerInput device to click with
      # @return [W3CActionBuilder] A self reference.
      #

      def click_and_hold(element = nil, device: nil)
        move_to(element, device: device) if element
        pointer_down(:left, device: device)
        self
      end

      #
      # Releases the depressed left mouse button at the current mouse location.
      #
      # @example Releasing an element after clicking and holding it
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click_and_hold(el).release.perform
      #
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be released
      # @return [W3CActionBuilder] A self reference.
      #

      def release(device: nil)
        pointer_up(:left, device: device)
        self
      end

      #
      # Clicks in the middle of the given element. Equivalent to:
      #
      #   driver.action.move_to(element).click
      #
      # When no element is passed, the current mouse position will be clicked.
      #
      # @example Clicking on an element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click(el).perform
      #
      # @example Clicking at the current mouse position
      #
      #    driver.action.click.perform
      #
      # @param [Selenium::WebDriver::Element] element An optional element to click.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be clicked
      # @return [W3CActionBuilder] A self reference.
      #

      def click(element = nil, device: nil)
        move_to(element, device: device) if element
        pointer_down(:left, device: device)
        pointer_up(:left, device: device)
        self
      end

      #
      # Performs a double-click at middle of the given element. Equivalent to:
      #
      #   driver.action.move_to(element).double_click
      #
      # When no element is passed, the current mouse position will be double-clicked.
      #
      # @example Double-click an element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.double_click(el).perform
      #
      # @example Double-clicking at the current mouse position
      #
      #    driver.action.double_click.perform
      #
      # @param [Selenium::WebDriver::Element] element An optional element to move to.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be double-clicked
      # @return [W3CActionBuilder] A self reference.
      #

      def double_click(element = nil, device: nil)
        move_to(element, device: device) if element
        click(device: device)
        click(device: device)
        self
      end

      #
      # Performs a context-click at middle of the given element. First performs
      # a move_to to the location of the element.
      #
      # When no element is passed, the current mouse position will be context-clicked.
      #
      # @example Context-click at middle of given element
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.context_click(el).perform
      #
      # @example Context-clicking at the current mouse position
      #
      #    driver.action.context_click.perform
      #
      # @param [Selenium::WebDriver::Element] element An element to context click.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be context-clicked
      # @return [W3CActionBuilder] A self reference.
      #

      def context_click(element = nil, device: nil)
        move_to(element, device: device) if element
        pointer_down(:right, device: device)
        pointer_up(:right, device: device)
        self
      end

      #
      # A convenience method that performs click-and-hold at the location of the
      # source element, moves to the location of the target element, then
      # releases the mouse.
      #
      # @example Drag and drop one element onto another
      #
      #   el1 = driver.find_element(id: "some_id1")
      #   el2 = driver.find_element(id: "some_id2")
      #   driver.action.drag_and_drop(el1, el2).perform
      #
      # @param [Selenium::WebDriver::Element] source element to emulate button down at.
      # @param [Selenium::WebDriver::Element] target element to move to and release the
      #   mouse at.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will perform the drag and drop
      # @return [W3CActionBuilder] A self reference.
      #

      def drag_and_drop(source, target, device: nil)
        click_and_hold(source, device: device)
        move_to(target, device: device)
        release(device: device)
        self
      end

      #
      # A convenience method that performs click-and-hold at the location of
      # the source element, moves by a given offset, then releases the mouse.
      #
      # @example Drag and drop an element by offset
      #
      #   el = driver.find_element(id: "some_id1")
      #   driver.action.drag_and_drop_by(el, 100, 100).perform
      #
      # @param [Selenium::WebDriver::Element] source Element to emulate button down at.
      # @param [Integer] right_by horizontal move offset.
      # @param [Integer] down_by vertical move offset.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will perform the drag and drop
      # @return [W3CActionBuilder] A self reference.
      #

      def drag_and_drop_by(source, right_by, down_by, device: nil)
        click_and_hold(source, device: device)
        move_by(right_by, down_by, device: device)
        release(device: device)
        self
      end

      private

      def button_action(button, action: nil, device: nil)
        pointer = get_pointer(device)
        pointer.send(action, button)
        tick(pointer)
        self
      end

      def get_pointer(device = nil)
        get_device(device) || pointer_inputs.first
      end
    end # PointerActions
  end # WebDriver
end # Selenium
