// <copyright file="Actions.cs" company="Selenium Committers">
// 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.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace OpenQA.Selenium.Interactions;

/// <summary>
/// Provides a mechanism for building advanced interactions with the browser.
/// </summary>
public class Actions : IAction
{
    private readonly TimeSpan duration;
    private ActionBuilder actionBuilder = new ActionBuilder();
    private PointerInputDevice? activePointer;
    private KeyInputDevice? activeKeyboard;
    private WheelInputDevice? activeWheel;

    /// <summary>
    /// Initializes a new instance of the <see cref="Actions"/> class.
    /// </summary>
    /// <param name="driver">The <see cref="IWebDriver"/> object on which the actions built will be performed.</param>
    /// <exception cref="ArgumentException">If <paramref name="driver"/> does not implement <see cref="IActionExecutor"/>.</exception>
    public Actions(IWebDriver driver)
        : this(driver, TimeSpan.FromMilliseconds(250))
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="Actions"/> class.
    /// </summary>
    /// <param name="driver">The <see cref="IWebDriver"/> object on which the actions built will be performed.</param>
    /// <param name="duration">How long durable action is expected to take.</param>
    /// <exception cref="ArgumentException">If <paramref name="driver"/> does not implement <see cref="IActionExecutor"/>.</exception>
    public Actions(IWebDriver driver, TimeSpan duration)
    {
        IActionExecutor actionExecutor = GetDriverAs<IActionExecutor>(driver)
            ?? throw new ArgumentException("The IWebDriver object must implement or wrap a driver that implements IActionExecutor.", nameof(driver));

        this.ActionExecutor = actionExecutor;
        this.duration = duration;
    }

    /// <summary>
    /// Returns the <see cref="IActionExecutor"/> for the driver.
    /// </summary>
    protected IActionExecutor ActionExecutor { get; }

    /// <summary>
    /// Sets the active pointer device for this Actions class.
    /// </summary>
    /// <param name="kind">The kind of pointer device to set as active.</param>
    /// <param name="name">The name of the pointer device to set as active.</param>
    /// <returns>A self-reference to this Actions class.</returns>
    /// <exception cref="InvalidOperationException">If a device with this name exists but is not a pointer.</exception>
    [MemberNotNull(nameof(activePointer))]
    public Actions SetActivePointer(PointerKind kind, string name)
    {
        InputDevice? device = FindDeviceById(name);

        this.activePointer = device switch
        {
            null => new PointerInputDevice(kind, name),
            PointerInputDevice pointerDevice => pointerDevice,
            _ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}"),
        };

        return this;
    }

    /// <summary>
    /// Sets the active keyboard device for this Actions class.
    /// </summary>
    /// <param name="name">The name of the keyboard device to set as active.</param>
    /// <returns>A self-reference to this Actions class.</returns>
    /// <exception cref="InvalidOperationException">If a device with this name exists but is not a keyboard.</exception>
    [MemberNotNull(nameof(activeKeyboard))]
    public Actions SetActiveKeyboard(string name)
    {
        InputDevice? device = FindDeviceById(name);

        this.activeKeyboard = device switch
        {
            null => new KeyInputDevice(name),
            KeyInputDevice keyDevice => keyDevice,
            _ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a keyboard. Actual input type: {device.DeviceKind}"),
        };

        return this;
    }

    /// <summary>
    /// Sets the active wheel device for this Actions class.
    /// </summary>
    /// <param name="name">The name of the wheel device to set as active.</param>
    /// <returns>A self-reference to this Actions class.</returns>
    /// <exception cref="InvalidOperationException">If a device with this name exists but is not a wheel.</exception>
    [MemberNotNull(nameof(activeWheel))]
    public Actions SetActiveWheel(string name)
    {
        InputDevice? device = FindDeviceById(name);

        this.activeWheel = device switch
        {
            null => new WheelInputDevice(name),
            WheelInputDevice wheelDevice => wheelDevice,
            _ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a wheel. Actual input type: {device.DeviceKind}"),
        };

        return this;
    }

    private InputDevice? FindDeviceById(string? name)
    {
        foreach (var sequence in this.actionBuilder.ToActionSequenceList())
        {
            Dictionary<string, object> actions = sequence.ToDictionary();

            string id = (string)actions["id"];

            if (id == name)
            {
                return sequence.InputDevice;
            }
        }

        return null;
    }

    /// <summary>
    /// Gets the active pointer device for this Actions class.
    /// </summary>
    /// <returns>The active pointer device for this Actions class.</returns>
    public PointerInputDevice GetActivePointer()
    {
        if (this.activePointer == null)
        {
            SetActivePointer(PointerKind.Mouse, "default mouse");
        }

        return this.activePointer;
    }

    /// <summary>
    /// Gets the active keyboard device for this Actions class.
    /// </summary>
    /// <returns>The active keyboard device for this Actions class.</returns>
    public KeyInputDevice GetActiveKeyboard()
    {
        if (this.activeKeyboard == null)
        {
            SetActiveKeyboard("default keyboard");
        }

        return this.activeKeyboard;
    }

    /// <summary>
    /// Gets the active wheel device for this Actions class.
    /// </summary>
    /// <returns>The active wheel device for this Actions class.</returns>
    public WheelInputDevice GetActiveWheel()
    {
        if (this.activeWheel == null)
        {
            SetActiveWheel("default wheel");
        }

        return this.activeWheel;
    }

    /// <summary>
    /// Sends a modifier key down message to the browser.
    /// </summary>
    /// <param name="theKey">The key to be sent.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If the key sent is not is not one
    /// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
    /// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
    /// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
    public Actions KeyDown(string theKey)
    {
        return this.KeyDown(null, theKey);
    }

    /// <summary>
    /// Sends a modifier key down message to the specified element in the browser.
    /// </summary>
    /// <param name="element">The element to which to send the key command.</param>
    /// <param name="theKey">The key to be sent.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If the key sent is not is not one
    /// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
    /// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
    /// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
    public Actions KeyDown(IWebElement? element, string theKey)
    {
        if (string.IsNullOrEmpty(theKey))
        {
            throw new ArgumentException("The key value must not be null or empty", nameof(theKey));
        }

        ILocatable? target = GetLocatableFromElement(element);
        if (element != null)
        {
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        }

        this.actionBuilder.AddAction(this.GetActiveKeyboard().CreateKeyDown(theKey[0]));
        this.actionBuilder.AddAction(new PauseInteraction(this.GetActiveKeyboard(), TimeSpan.FromMilliseconds(100)));
        return this;
    }

    /// <summary>
    /// Sends a modifier key up message to the browser.
    /// </summary>
    /// <param name="theKey">The key to be sent.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If the key sent is not is not one
    /// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
    /// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
    /// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
    public Actions KeyUp(string theKey)
    {
        return this.KeyUp(null, theKey);
    }

    /// <summary>
    /// Sends a modifier up down message to the specified element in the browser.
    /// </summary>
    /// <param name="element">The element to which to send the key command.</param>
    /// <param name="theKey">The key to be sent.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If the key sent is not is not one
    /// of <see cref="Keys.Shift"/>, <see cref="Keys.Control"/>, <see cref="Keys.Alt"/>,
    /// <see cref="Keys.Meta"/>, <see cref="Keys.Command"/>,<see cref="Keys.LeftAlt"/>,
    /// <see cref="Keys.LeftControl"/>,<see cref="Keys.LeftShift"/>.</exception>
    public Actions KeyUp(IWebElement? element, string theKey)
    {
        if (string.IsNullOrEmpty(theKey))
        {
            throw new ArgumentException("The key value must not be null or empty", nameof(theKey));
        }

        ILocatable? target = GetLocatableFromElement(element);
        if (element != null)
        {
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        }

        this.actionBuilder.AddAction(this.GetActiveKeyboard().CreateKeyUp(theKey[0]));
        return this;
    }

    /// <summary>
    /// Sends a sequence of keystrokes to the browser.
    /// </summary>
    /// <param name="keysToSend">The keystrokes to send to the browser.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If <paramref name="keysToSend"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public Actions SendKeys(string keysToSend)
    {
        return this.SendKeys(null, keysToSend);
    }

    /// <summary>
    /// Sends a sequence of keystrokes to the specified element in the browser.
    /// </summary>
    /// <param name="element">The element to which to send the keystrokes.</param>
    /// <param name="keysToSend">The keystrokes to send to the browser.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If <paramref name="keysToSend"/> is <see langword="null"/> or <see cref="string.Empty"/>.</exception>
    public Actions SendKeys(IWebElement? element, string keysToSend)
    {
        if (string.IsNullOrEmpty(keysToSend))
        {
            throw new ArgumentException("The key value must not be null or empty", nameof(keysToSend));
        }

        ILocatable? target = GetLocatableFromElement(element);
        if (element != null)
        {
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(element, 0, 0, duration));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
            this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        }

        foreach (char key in keysToSend)
        {
            this.actionBuilder.AddAction(this.GetActiveKeyboard().CreateKeyDown(key));
            this.actionBuilder.AddAction(this.GetActiveKeyboard().CreateKeyUp(key));
        }

        return this;
    }

    /// <summary>
    /// Clicks and holds the mouse button down on the specified element.
    /// </summary>
    /// <param name="onElement">The element on which to click and hold.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
    public Actions ClickAndHold(IWebElement onElement)
    {
        this.MoveToElement(onElement).ClickAndHold();
        return this;
    }

    /// <summary>
    /// Clicks and holds the mouse button at the last known mouse coordinates.
    /// </summary>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions ClickAndHold()
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
        return this;
    }

    /// <summary>
    /// Releases the mouse button on the specified element.
    /// </summary>
    /// <param name="onElement">The element on which to release the button.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
    public Actions Release(IWebElement onElement)
    {
        this.MoveToElement(onElement).Release();
        return this;
    }

    /// <summary>
    /// Releases the mouse button at the last known mouse coordinates.
    /// </summary>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions Release()
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        return this;
    }

    /// <summary>
    /// Clicks the mouse on the specified element.
    /// </summary>
    /// <param name="onElement">The element on which to click.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
    public Actions Click(IWebElement onElement)
    {
        this.MoveToElement(onElement).Click();
        return this;
    }

    /// <summary>
    /// Clicks the mouse at the last known mouse coordinates.
    /// </summary>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions Click()
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        return this;
    }

    /// <summary>
    /// Double-clicks the mouse on the specified element.
    /// </summary>
    /// <param name="onElement">The element on which to double-click.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
    public Actions DoubleClick(IWebElement onElement)
    {
        this.MoveToElement(onElement).DoubleClick();
        return this;
    }

    /// <summary>
    /// Double-clicks the mouse at the last known mouse coordinates.
    /// </summary>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions DoubleClick()
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Left));
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Left));
        return this;
    }

    /// <summary>
    /// Moves the mouse to the specified element.
    /// </summary>
    /// <param name="toElement">The element to which to move the mouse.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="toElement"/> is null.</exception>
    public Actions MoveToElement(IWebElement toElement)
    {
        if (toElement == null)
        {
            throw new ArgumentException("MoveToElement cannot move to a null element with no offset.", nameof(toElement));
        }

        return this.MoveToElement(toElement, 0, 0);
    }

    /// <summary>
    /// Moves the mouse to the specified offset of the top-left corner of the specified element.
    /// In Selenium 4.3 the origin for the offset will be the in-view center point of the element.
    /// </summary>
    /// <param name="toElement">The element to which to move the mouse.</param>
    /// <param name="offsetX">The horizontal offset to which to move the mouse.</param>
    /// <param name="offsetY">The vertical offset to which to move the mouse.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions MoveToElement(IWebElement toElement, int offsetX, int offsetY)
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(toElement, offsetX, offsetY, duration));
        return this;
    }

    /// <summary>
    /// Moves the mouse to the specified offset of the last known mouse coordinates.
    /// </summary>
    /// <param name="offsetX">The horizontal offset to which to move the mouse.</param>
    /// <param name="offsetY">The vertical offset to which to move the mouse.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions MoveByOffset(int offsetX, int offsetY)
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(CoordinateOrigin.Pointer, offsetX, offsetY, duration));
        return this;
    }

    /// <summary>
    /// Moves the mouse from the upper left corner of the current viewport by the provided offset.
    /// </summary>
    /// <param name="offsetX">The horizontal offset to which to move the mouse.</param>
    /// <param name="offsetY">The vertical offset to which to move the mouse.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions MoveToLocation(int offsetX, int offsetY)
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerMove(CoordinateOrigin.Viewport, offsetX, offsetY, duration));
        return this;
    }

    /// <summary>
    /// Right-clicks the mouse on the specified element.
    /// </summary>
    /// <param name="onElement">The element on which to right-click.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="onElement"/> is null.</exception>
    public Actions ContextClick(IWebElement onElement)
    {
        this.MoveToElement(onElement).ContextClick();
        return this;
    }

    /// <summary>
    /// Right-clicks the mouse at the last known mouse coordinates.
    /// </summary>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions ContextClick()
    {
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerDown(MouseButton.Right));
        this.actionBuilder.AddAction(this.GetActivePointer().CreatePointerUp(MouseButton.Right));
        return this;
    }

    /// <summary>
    /// Performs a drag-and-drop operation from one element to another.
    /// </summary>
    /// <param name="source">The element on which the drag operation is started.</param>
    /// <param name="target">The element on which the drop is performed.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="source"/> or <paramref name="target"/> are null.</exception>
    public Actions DragAndDrop(IWebElement source, IWebElement target)
    {
        this.ClickAndHold(source).MoveToElement(target).Release(target);
        return this;
    }

    /// <summary>
    /// Performs a drag-and-drop operation on one element to a specified offset.
    /// </summary>
    /// <param name="source">The element on which the drag operation is started.</param>
    /// <param name="offsetX">The horizontal offset to which to move the mouse.</param>
    /// <param name="offsetY">The vertical offset to which to move the mouse.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="source"/> is null.</exception>
    public Actions DragAndDropToOffset(IWebElement source, int offsetX, int offsetY)
    {
        this.ClickAndHold(source).MoveByOffset(offsetX, offsetY).Release();
        return this;
    }

    /// <summary>
    /// If the element is outside the viewport, scrolls the bottom of the element to the bottom of the viewport.
    /// </summary>
    /// <param name="element">Which element to scroll into the viewport.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="element"/> is null.</exception>
    public Actions ScrollToElement(IWebElement element)
    {
        this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(element, 0, 0, 0, 0, duration));

        return this;
    }

    /// <summary>
    /// Scrolls by provided amounts with the origin in the top left corner of the viewport.
    /// </summary>
    /// <param name="deltaX">Distance along X axis to scroll using the wheel. A negative value scrolls left.</param>
    /// <param name="deltaY">Distance along Y axis to scroll using the wheel. A negative value scrolls up.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    public Actions ScrollByAmount(int deltaX, int deltaY)
    {
        this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(deltaX, deltaY, duration));

        return this;
    }

    /// <summary>
    /// Scrolls by provided amount based on a provided origin.
    /// </summary>
    /// <remarks>
    /// The scroll origin is either the center of an element or the upper left of the viewport plus any offsets.
    /// If the origin is an element, and the element is not in the viewport, the bottom of the element will first
    /// be scrolled to the bottom of the viewport.
    /// </remarks>
    /// <param name="scrollOrigin">Where scroll originates (viewport or element center) plus provided offsets.</param>
    /// <param name="deltaX">Distance along X axis to scroll using the wheel. A negative value scrolls left.</param>
    /// <param name="deltaY">Distance along Y axis to scroll using the wheel. A negative value scrolls up.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="MoveTargetOutOfBoundsException">If the origin with offset is outside the viewport.</exception>
    /// <exception cref="ArgumentNullException">If <paramref name="scrollOrigin"/> is null.</exception>
    /// <exception cref="ArgumentException">If both or either of Viewport and Element are set.</exception>
    public Actions ScrollFromOrigin(WheelInputDevice.ScrollOrigin scrollOrigin, int deltaX, int deltaY)
    {
        if (scrollOrigin is null)
        {
            throw new ArgumentNullException(nameof(scrollOrigin));
        }

        if (scrollOrigin.Viewport && scrollOrigin.Element != null)
        {
            throw new ArgumentException("viewport can not be true if an element is defined.", nameof(scrollOrigin));
        }

        if (scrollOrigin.Viewport)
        {
            this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(CoordinateOrigin.Viewport,
                scrollOrigin.XOffset, scrollOrigin.YOffset, deltaX, deltaY, duration));
        }
        else
        {
            this.actionBuilder.AddAction(this.GetActiveWheel().CreateWheelScroll(scrollOrigin.Element!,
                scrollOrigin.XOffset, scrollOrigin.YOffset, deltaX, deltaY, duration));
        }

        return this;
    }

    /// <summary>
    /// Performs a Pause.
    /// </summary>
    /// <param name="duration">How long to pause the action chain.</param>
    /// <returns>A self-reference to this <see cref="Actions"/>.</returns>
    /// <exception cref="ArgumentException">If <paramref name="duration"/> is negative.</exception>
    public Actions Pause(TimeSpan duration)
    {
        this.actionBuilder.AddAction(new PauseInteraction(this.GetActivePointer(), duration));
        return this;
    }

    /// <summary>
    /// Builds the sequence of actions.
    /// </summary>
    /// <returns>A composite <see cref="IAction"/> which can be used to perform the actions.</returns>
    public IAction Build()
    {
        return this;
    }

    /// <summary>
    /// Performs the currently built action.
    /// </summary>
    public void Perform()
    {
        this.ActionExecutor.PerformActions(this.actionBuilder.ToActionSequenceList());
        this.actionBuilder.ClearSequences();
    }

    /// <summary>
    /// Clears the list of actions to be performed.
    /// </summary>
    public void Reset()
    {
        this.actionBuilder = new ActionBuilder();
    }

    /// <summary>
    /// Gets the <see cref="ILocatable"/> instance of the specified <see cref="IWebElement"/>.
    /// </summary>
    /// <param name="element">The <see cref="IWebElement"/> to get the location of.</param>
    /// <returns>The <see cref="ILocatable"/> of the <see cref="IWebElement"/>.</returns>
    [return: NotNullIfNotNull(nameof(element))]
    protected static ILocatable? GetLocatableFromElement(IWebElement? element)
    {
        if (element == null)
        {
            return null;
        }

        ILocatable? target = null;
        IWrapsElement? wrapper = element as IWrapsElement;
        while (wrapper != null)
        {
            target = wrapper.WrappedElement as ILocatable;
            wrapper = wrapper.WrappedElement as IWrapsElement;
        }

        if (target == null)
        {
            target = element as ILocatable;
        }

        if (target == null)
        {
            throw new ArgumentException("The IWebElement object must implement or wrap an element that implements ILocatable.", nameof(element));
        }

        return target;
    }

    private static T? GetDriverAs<T>(IWebDriver? driver) where T : class
    {
        T? driverAsType = driver as T;
        if (driverAsType == null)
        {
            IWrapsDriver? wrapper = driver as IWrapsDriver;
            while (wrapper != null)
            {
                driverAsType = wrapper.WrappedDriver as T;
                if (driverAsType != null)
                {
                    driver = wrapper.WrappedDriver;
                    break;
                }

                wrapper = wrapper.WrappedDriver as IWrapsDriver;
            }
        }

        return driverAsType;
    }
}
