/*
Copyright 2024 New Vector Ltd.
Copyright 2016 Jani Mustonen

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
 */

import React, {
    type JSX,
    type ComponentProps,
    type ComponentPropsWithoutRef,
    type ReactElement,
    type KeyboardEvent,
    type Ref,
} from "react";
import classnames from "classnames";
import { Tooltip } from "@vector-im/compound-web";

import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";

export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;

/**
 * The kind of button, similar to how Bootstrap works.
 */
export type AccessibleButtonKind =
    | "primary"
    | "primary_outline"
    | "primary_sm"
    | "secondary"
    | "content_inline"
    | "danger"
    | "danger_outline"
    | "danger_sm"
    | "danger_inline"
    | "link"
    | "link_inline"
    | "link_sm"
    | "icon"
    | "icon_primary"
    | "icon_primary_outline";

type ElementType = keyof HTMLElementTagNameMap;
const defaultElement = "div";

type TooltipProps = ComponentProps<typeof Tooltip>;

/**
 * Type of props accepted by {@link AccessibleButton}.
 *
 * Extends props accepted by the underlying element specified using the `element` prop.
 */
type Props<T extends ElementType = "div"> = {
    /**
     * The base element type. "div" by default.
     */
    element?: T;
    /**
     * The kind of button, similar to how Bootstrap works.
     */
    kind?: AccessibleButtonKind;
    /**
     * Whether the button should be disabled.
     */
    disabled?: boolean;
    /**
     * Whether the button should trigger on mousedown event instead of on click event. Defaults to false (click event).
     */
    triggerOnMouseDown?: boolean;
    /**
     * Event handler for button activation. Should be implemented exactly like a normal `onClick` handler.
     */
    onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
    /**
     * The tooltip to show on hover or focus.
     */
    title?: string;
    /**
     * The caption is a secondary text displayed under the `title` of the tooltip.
     * Only valid when used in conjunction with `title`.
     */
    caption?: TooltipProps["caption"];
    /**
     * The placement of the tooltip.
     */
    placement?: TooltipProps["placement"];
    /**
     * Callback for when the tooltip is opened or closed.
     */
    onTooltipOpenChange?: TooltipProps["onOpenChange"];

    /**
     * Whether the tooltip should be disabled.
     */
    disableTooltip?: TooltipProps["disabled"];

    ref?: Ref<HTMLElementTagNameMap[T]>;
};

export type ButtonProps<T extends ElementType> = Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>;

/**
 * Type of the props passed to the element that is rendered by AccessibleButton.
 */
type RenderedElementProps<T extends ElementType> = React.InputHTMLAttributes<Element> & RefProp<T>;

/**
 * AccessibleButton is a generic wrapper for any element that should be treated
 * as a button.  Identifies the element as a button, setting proper tab
 * indexing and keyboard activation behavior.
 *
 * If a ref is passed, it will be forwarded to the rendered element as specified using the `element` prop.
 *
 * @param {Object} props  react element properties
 * @returns {Object} rendered react
 */
const AccessibleButton = function AccessibleButton<T extends ElementType = typeof defaultElement>({
    element,
    onClick,
    children,
    kind,
    disabled,
    className,
    onKeyDown,
    onKeyUp,
    triggerOnMouseDown,
    title,
    caption,
    placement = "right",
    onTooltipOpenChange,
    disableTooltip,
    role = "button",
    tabIndex = 0,
    ref,
    ...restProps
}: ButtonProps<T>): JSX.Element {
    const newProps = {
        ...restProps,
        tabIndex,
        role,
        "aria-label": restProps["aria-label"] ?? title,
    } as RenderedElementProps<T>;

    if (disabled) {
        newProps["aria-disabled"] = true;
        newProps["disabled"] = true;
    } else {
        if (triggerOnMouseDown) {
            newProps.onMouseDown = onClick ?? undefined;
        } else {
            newProps.onClick = onClick ?? undefined;
        }

        if (element !== "button") {
            // We need to consume enter onKeyDown and space onKeyUp
            // otherwise we are risking also activating other keyboard focusable elements
            // that might receive focus as a result of the AccessibleButtonClick action
            // It's because we are using html buttons at a few places e.g. inside dialogs
            // And divs which we report as role button to assistive technologies.
            // Browsers handle space and enter key presses differently and we are only adjusting to the
            // inconsistencies here
            newProps.onKeyDown = (e: KeyboardEvent<never>) => {
                const action = getKeyBindingsManager().getAccessibilityAction(e);

                switch (action) {
                    case KeyBindingAction.Enter:
                        e.stopPropagation();
                        e.preventDefault();
                        return onClick?.(e);
                    case KeyBindingAction.Space:
                        e.stopPropagation();
                        e.preventDefault();
                        break;
                    default:
                        onKeyDown?.(e);
                }
            };
            newProps.onKeyUp = (e: KeyboardEvent<never>) => {
                const action = getKeyBindingsManager().getAccessibilityAction(e);

                switch (action) {
                    case KeyBindingAction.Enter:
                        e.stopPropagation();
                        e.preventDefault();
                        break;
                    case KeyBindingAction.Space:
                        e.stopPropagation();
                        e.preventDefault();
                        return onClick?.(e);
                    default:
                        onKeyUp?.(e);
                        break;
                }
            };
        }
    }

    // Pass through the ref - used for keyboard shortcut access to some buttons
    newProps.ref = ref;

    newProps.className = classnames("mx_AccessibleButton", className, {
        mx_AccessibleButton_hasKind: kind,
        [`mx_AccessibleButton_kind_${kind}`]: kind,
        mx_AccessibleButton_disabled: disabled,
    });

    // React.createElement expects InputHTMLAttributes
    const button = React.createElement(element ?? defaultElement, newProps, children);

    if (title) {
        return (
            <Tooltip
                description={title}
                caption={caption}
                isTriggerInteractive={true}
                placement={placement}
                onOpenChange={onTooltipOpenChange}
                disabled={disableTooltip}
            >
                {button}
            </Tooltip>
        );
    }
    return button;
};

interface RefProp<T extends ElementType> {
    ref?: Ref<HTMLElementTagNameMap[T]>;
}

interface ButtonComponent {
    // With the explicit `element` prop
    <C extends ElementType>(props: { element?: C } & ButtonProps<C> & RefProp<C>): ReactElement;
    // Without the explicit `element` prop
    (props: ButtonProps<"div"> & RefProp<"div">): ReactElement;
}

export default AccessibleButton as ButtonComponent;
