/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { batch, useSelector } from "react-redux";
import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import {
  PREF_CLOCKS_SIZE,
  PREF_WIDGETS_CLOCKS_ENABLED,
  WIDGET_REGISTRY,
} from "common/WidgetsRegistry.mjs";
import { useWidgetTelemetry } from "../useWidgetTelemetry";
import { AddClockForm } from "./AddClockForm";
import { ClocksRow } from "./ClocksRow";
import { EditClocksPanel } from "./EditClocksPanel";
import { SizeSubmenu } from "../SizeSubmenu";
import { WidgetMenuFooter } from "../WidgetMenuFooter";
import {
  backfillClockLabelColors,
  buildNextClockZones,
  MAX_CLOCK_COUNT,
  buildDefaultZones,
  getSupportedTimeZones,
  parseClockZonesPref,
  removeClockZoneAtIndex,
  shouldUse12HourTimeFormat,
} from "./ClocksHelpers";

const USER_ACTION_TYPES = {
  ADD_CLOCK: "add_clock",
  ADD_NICKNAME: "add_nickname",
  CHANGE_HOUR_FORMAT: "change_hour_format",
  CHANGE_SIZE: "change_size",
  COLLAPSE: "collapse",
  EDIT_CLOCK: "edit_clock",
  EXPAND: "expand",
  LEARN_MORE: "learn_more",
  REMOVE_CLOCK: "remove_clock",
};

const PREF_CLOCKS_HOUR_FORMAT = "widgets.clocks.hourFormat";
const PREF_CLOCKS_ZONES = "widgets.clocks.zones";
const CLOCKS_PANEL = {
  FORM: "form",
  EDIT: "edit",
};
const CLOCK_WIDGET_SOURCE = {
  CONTEXT_MENU: "context_menu",
  MANAGE: "manage",
  ROW: "row",
  TOOLBAR: "toolbar",
};

const CLOCKS_WIDGET = WIDGET_REGISTRY.find(widget => widget.id === "clocks");

function getClockWidgetDisplayState({ activePanel, hourFormatPref, size }) {
  const currentSize = size || "medium";
  const locale =
    typeof navigator !== "undefined" ? navigator.language : undefined;
  return {
    currentSize,
    locale,
    panelDisplaySize: activePanel ? "large" : currentSize,
    use12HourFormat: shouldUse12HourTimeFormat({
      prefValue: hourFormatPref,
      locale,
    }),
  };
}

/**
 * Nova-only World Clocks widget. Up to four clocks with a minute-aligned
 * tick, hover toolbar, and context menu.
 *
 * @param {object} props
 * @param {Function} props.dispatch
 * @param {"small"|"medium"|"large"} [props.size] Defaults to "medium".
 */
function Clocks({ dispatch, size, widgetEnabledMap }) {
  const clocksZonesPref = useSelector(
    state => state.Prefs.values[PREF_CLOCKS_ZONES]
  );
  const hourFormatPref = useSelector(
    state => state.Prefs.values[PREF_CLOCKS_HOUR_FORMAT]
  );

  const [now, setNow] = useState(null);
  const contextMenuRef = useRef(null);
  const contextMenuButtonRef = useRef(null);
  // Suppress hover-reveal after a menu action; cleared on mouseleave.
  const [isDismissed, setIsDismissed] = useState(false);
  const [activePanel, setActivePanel] = useState(null);
  const [formSource, setFormSource] = useState(CLOCK_WIDGET_SOURCE.TOOLBAR);
  const [panelOpenSource, setPanelOpenSource] = useState(null);
  const [editingClockIndex, setEditingClockIndex] = useState(null);
  const addButtonRef = useRef(null);

  // Blur the trigger after hide() returns focus there; otherwise
  // :focus-within keeps the overlay open.
  const closeContextMenu = useCallback(() => {
    contextMenuRef.current?.hide?.();
    setIsDismissed(true);
    // Defer a frame so we don't race hide()'s synchronous focus return.
    requestAnimationFrame(() => {
      if (document.activeElement instanceof HTMLElement) {
        document.activeElement.blur();
      }
    });
  }, []);

  const { currentSize, locale, panelDisplaySize, use12HourFormat } =
    getClockWidgetDisplayState({
      activePanel,
      hourFormatPref,
      size,
    });

  const { impressionRef, recordUserAction } = useWidgetTelemetry({
    dispatch,
    widget: CLOCKS_WIDGET,
    widgetSize: currentSize,
  });

  // Each tick realigns to the next minute, so paused tabs or device sleep
  // can't compound drift. `now` starts null so the first render stays
  // stable for prerender/hydration.
  useEffect(() => {
    let timeoutId;
    const tick = () => {
      setNow(new Date());
      timeoutId = setTimeout(tick, 60_000 - (Date.now() % 60_000));
    };
    tick();
    return () => clearTimeout(timeoutId);
  }, []);

  const handleChangeSize = useCallback(
    newSize => {
      batch(() => {
        dispatch(
          ac.OnlyToMain({
            type: at.SET_PREF,
            data: { name: PREF_CLOCKS_SIZE, value: newSize },
          })
        );
        recordUserAction(USER_ACTION_TYPES.CHANGE_SIZE, {
          source: CLOCK_WIDGET_SOURCE.CONTEXT_MENU,
          value: newSize,
          size: newSize,
        });
      });
      closeContextMenu();
    },
    [dispatch, recordUserAction, closeContextMenu]
  );

  const handleToggleHourFormat = useCallback(() => {
    const nextFormat = use12HourFormat ? "24" : "12";
    batch(() => {
      dispatch(
        ac.OnlyToMain({
          type: at.SET_PREF,
          data: { name: PREF_CLOCKS_HOUR_FORMAT, value: nextFormat },
        })
      );
      recordUserAction(USER_ACTION_TYPES.CHANGE_HOUR_FORMAT, {
        source: CLOCK_WIDGET_SOURCE.CONTEXT_MENU,
        value: nextFormat,
      });
    });
    closeContextMenu();
  }, [use12HourFormat, dispatch, recordUserAction, closeContextMenu]);

  const handleLearnMore = useCallback(() => {
    recordUserAction(USER_ACTION_TYPES.LEARN_MORE, {
      source: CLOCK_WIDGET_SOURCE.CONTEXT_MENU,
    });
    closeContextMenu();
  }, [recordUserAction, closeContextMenu]);

  const clockZones = useMemo(
    () => parseClockZonesPref(clocksZonesPref) || buildDefaultZones(),
    [clocksZonesPref]
  );

  useEffect(() => {
    if (!clockZones.some(clock => clock.label && !clock.labelColor)) {
      return;
    }
    dispatch(
      ac.OnlyToMain({
        type: at.SET_PREF,
        data: {
          name: PREF_CLOCKS_ZONES,
          value: JSON.stringify(backfillClockLabelColors(clockZones)),
        },
      })
    );
  }, [clockZones, dispatch]);

  const canAddClock = clockZones.length < MAX_CLOCK_COUNT;
  const supportedTimeZones = useMemo(() => getSupportedTimeZones(), []);
  const resetAddClockForm = useCallback(() => {
    setEditingClockIndex(null);
  }, []);

  const handleShowAddClock = useCallback(
    (source = CLOCK_WIDGET_SOURCE.TOOLBAR) => {
      setActivePanel(CLOCKS_PANEL.FORM);
      setFormSource(source);
      setEditingClockIndex(null);
      setIsDismissed(false);
    },
    []
  );

  const handleShowEditClocks = useCallback(
    source => {
      setActivePanel(CLOCKS_PANEL.EDIT);
      setPanelOpenSource(source);
      setIsDismissed(false);
      recordUserAction(USER_ACTION_TYPES.EXPAND, { source });
    },
    [recordUserAction]
  );

  const handleCloseDisplayPanel = useCallback(() => {
    if (activePanel === CLOCKS_PANEL.EDIT) {
      recordUserAction(USER_ACTION_TYPES.COLLAPSE, {
        source: panelOpenSource,
      });
    }
    setActivePanel(null);
    resetAddClockForm();
    requestAnimationFrame(() => {
      (addButtonRef.current ?? contextMenuButtonRef.current)?.focus();
    });
  }, [activePanel, panelOpenSource, recordUserAction, resetAddClockForm]);

  const handleCloseClockForm = useCallback(() => {
    if (formSource === CLOCK_WIDGET_SOURCE.MANAGE) {
      setActivePanel(CLOCKS_PANEL.EDIT);
      resetAddClockForm();
      return;
    }
    handleCloseDisplayPanel();
  }, [formSource, handleCloseDisplayPanel, resetAddClockForm]);

  const handleShowEditClockForm = useCallback(
    (index, source = CLOCK_WIDGET_SOURCE.ROW) => {
      setActivePanel(CLOCKS_PANEL.FORM);
      setFormSource(source);
      setEditingClockIndex(index);
      setIsDismissed(false);
    },
    []
  );

  const handleSaveClock = useCallback(
    zone => {
      const existingClock =
        editingClockIndex !== null ? clockZones[editingClockIndex] : null;
      batch(() => {
        dispatch(
          ac.OnlyToMain({
            type: at.SET_PREF,
            data: {
              name: PREF_CLOCKS_ZONES,
              value: JSON.stringify(
                buildNextClockZones(clockZones, editingClockIndex, zone)
              ),
            },
          })
        );
        recordUserAction(
          editingClockIndex !== null
            ? USER_ACTION_TYPES.EDIT_CLOCK
            : USER_ACTION_TYPES.ADD_CLOCK,
          { source: formSource }
        );
        if (zone.label && !existingClock?.label) {
          recordUserAction(USER_ACTION_TYPES.ADD_NICKNAME, {
            source: formSource,
          });
        }
      });
      if (formSource === CLOCK_WIDGET_SOURCE.MANAGE) {
        setActivePanel(CLOCKS_PANEL.EDIT);
        resetAddClockForm();
        return;
      }
      handleCloseDisplayPanel();
    },
    [
      clockZones,
      formSource,
      editingClockIndex,
      handleCloseDisplayPanel,
      resetAddClockForm,
      dispatch,
      recordUserAction,
    ]
  );

  const handleRemoveClock = useCallback(
    (index, source = CLOCK_WIDGET_SOURCE.ROW) => {
      if (clockZones.length <= 1) {
        return;
      }
      batch(() => {
        dispatch(
          ac.OnlyToMain({
            type: at.SET_PREF,
            data: {
              name: PREF_CLOCKS_ZONES,
              value: JSON.stringify(removeClockZoneAtIndex(clockZones, index)),
            },
          })
        );
        recordUserAction(USER_ACTION_TYPES.REMOVE_CLOCK, { source });
      });
    },
    [clockZones, dispatch, recordUserAction]
  );

  const isClockFormOpen = activePanel === CLOCKS_PANEL.FORM;
  const isEditingClocks = activePanel === CLOCKS_PANEL.EDIT;
  const hasAnyLabel = clockZones.some(c => !!c.label);

  return (
    <article
      className={`clocks-widget col-4 ${panelDisplaySize}-widget${
        clockZones.length === 1 ? " is-hero" : ""
      }${isDismissed ? " is-dismissed" : ""}${
        isClockFormOpen ? " is-clock-form-open" : ""
      }${isEditingClocks ? " is-editing-clocks" : ""}${
        activePanel ? " is-panel-open" : ""
      }${hasAnyLabel ? "" : " has-no-labels"}`}
      data-clock-count={clockZones.length}
      onMouseLeave={() => setIsDismissed(false)}
      ref={impressionRef}
    >
      <div className="widget-toolbar" inert={!!activePanel}>
        {canAddClock && (
          <moz-button
            className="clocks-add-button"
            type="icon primary"
            size="small"
            iconSrc="chrome://global/skin/icons/plus.svg"
            data-l10n-id="newtab-clock-widget-button-add"
            onClick={() => handleShowAddClock()}
            ref={addButtonRef}
          />
        )}
        <moz-button
          className="clocks-context-menu-button"
          data-l10n-id="newtab-clock-widget-menu-button"
          iconSrc="chrome://global/skin/icons/more.svg"
          menuId="clocks-widget-context-menu"
          type="icon ghost"
          size="small"
          ref={contextMenuButtonRef}
        />
        <panel-list ref={contextMenuRef} id="clocks-widget-context-menu">
          <panel-item
            data-l10n-id="newtab-clock-widget-menu-edit"
            onClick={() => {
              handleShowEditClocks(CLOCK_WIDGET_SOURCE.CONTEXT_MENU);
              closeContextMenu();
            }}
          />
          <panel-item
            data-l10n-id={
              use12HourFormat
                ? "newtab-clock-widget-menu-switch-to-24h"
                : "newtab-clock-widget-menu-switch-to-12h"
            }
            onClick={handleToggleHourFormat}
          />
          <WidgetMenuFooter
            dispatch={dispatch}
            widgetId="clocks"
            widgetEnabledMap={widgetEnabledMap}
            widgetName="clocks"
            enabledPref={PREF_WIDGETS_CLOCKS_ENABLED}
            widgetSize={currentSize}
            learnMoreL10nId="newtab-clock-widget-menu-learn-more"
            onAfterHide={closeContextMenu}
            onLearnMore={handleLearnMore}
            sizeSubmenu={
              <SizeSubmenu
                submenuId="clocks-size-submenu"
                sizes={["small", "medium", "large"]}
                checkedSize={currentSize}
                onChangeSize={handleChangeSize}
              />
            }
          />
        </panel-list>
      </div>
      {isClockFormOpen && (
        <AddClockForm
          key={editingClockIndex ?? "add"}
          isEditing={editingClockIndex !== null}
          initialClock={
            editingClockIndex !== null ? clockZones[editingClockIndex] : null
          }
          canAddClock={canAddClock}
          supportedTimeZones={supportedTimeZones}
          locale={locale}
          onSave={handleSaveClock}
          onCancel={handleCloseClockForm}
        />
      )}
      {isEditingClocks && (
        <EditClocksPanel
          clockZones={clockZones}
          canAddClock={canAddClock}
          onShowAddClock={() => handleShowAddClock(CLOCK_WIDGET_SOURCE.MANAGE)}
          onEditClock={index =>
            handleShowEditClockForm(index, CLOCK_WIDGET_SOURCE.MANAGE)
          }
          onRemoveClock={index =>
            handleRemoveClock(index, CLOCK_WIDGET_SOURCE.MANAGE)
          }
          onClose={handleCloseDisplayPanel}
        />
      )}
      <ul className="clocks-list" inert={!!activePanel}>
        {clockZones.map((c, i) => {
          const showLabel = panelDisplaySize === "large" && !!c.label;
          // Medium columns too narrow at 3+ clocks; Small always abbreviates.
          const shouldAbbreviate =
            panelDisplaySize === "small" ||
            (panelDisplaySize === "medium" && clockZones.length >= 3);
          const showInlineActions = !activePanel && currentSize !== "small";
          const hideTimeOnInlineActions =
            showInlineActions && clockZones.length > 1;
          return (
            <ClocksRow
              key={`${c.timeZone}-${i}`}
              clock={c}
              locale={locale}
              now={now}
              onEdit={
                showInlineActions ? () => handleShowEditClockForm(i) : null
              }
              onRemove={
                showInlineActions && clockZones.length > 1
                  ? () => handleRemoveClock(i)
                  : null
              }
              shouldAbbreviate={shouldAbbreviate}
              showLabel={showLabel}
              hideTimeOnInlineActions={hideTimeOnInlineActions}
              showInlineActions={showInlineActions}
              use12HourFormat={use12HourFormat}
            />
          );
        })}
      </ul>
    </article>
  );
}

export { Clocks };
