/* 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 http://mozilla.org/MPL/2.0/. */

import React from "react";
import { connect } from "react-redux";
import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
// eslint-disable-next-line no-shadow
import { CSSTransition } from "react-transition-group";

const PREF_WALLPAPER_UPLOADED_PREVIOUSLY =
  "newtabWallpapers.customWallpaper.uploadedPreviously";

const PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE =
  "newtabWallpapers.customWallpaper.fileSize";

const PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE_ENABLED =
  "newtabWallpapers.customWallpaper.fileSize.enabled";

// Returns a function will not be continuously triggered when called. The
// function will be triggered if called again after `wait` milliseconds.
function debounce(func, wait) {
  let timer;
  return (...args) => {
    if (timer) {
      return;
    }

    let wakeUp = () => {
      timer = null;
    };

    timer = setTimeout(wakeUp, wait);
    func.apply(this, args);
  };
}

export class _WallpaperCategories extends React.PureComponent {
  constructor(props) {
    super(props);
    this.handleColorInput = this.handleColorInput.bind(this);
    this.debouncedHandleChange = debounce(this.handleChange.bind(this), 999);
    this.handleChange = this.handleChange.bind(this);
    this.handleReset = this.handleReset.bind(this);
    this.handleCategory = this.handleCategory.bind(this);
    this.focusCategory = this.focusCategory.bind(this);
    this.handleUpload = this.handleUpload.bind(this);
    this.handleBack = this.handleBack.bind(this);
    this.getRGBColors = this.getRGBColors.bind(this);
    this.prefersHighContrastQuery = null;
    this.prefersDarkQuery = null;
    this.categoryRef = []; // store references for wallpaper category list
    this.wallpaperRef = []; // store reference for wallpaper selection list
    this.customColorPickerRef = React.createRef(); // Used to determine contrast icon color for custom color picker
    this.customColorInput = React.createRef(); // Used to determine contrast icon color for custom color picker
    this.state = {
      activeCategory: null,
      activeCategoryFluentID: null,
      showColorPicker: false,
      inputType: "radio",
      activeId: null,
      customWallpaperErrorType: null,
      focusedCategoryIndex: 0,
    };
  }

  componentDidMount() {
    this.prefersDarkQuery = globalThis.matchMedia(
      "(prefers-color-scheme: dark)"
    );
  }

  componentDidUpdate(prevProps) {
    // Walllpaper category subpanel should close when parent menu is closed
    if (
      this.props.exitEventFired &&
      this.props.exitEventFired !== prevProps.exitEventFired
    ) {
      this.handleBack();
    }
  }

  handleColorInput(event) {
    let { id } = event.target;
    // Set ID to include hex value of custom color
    id = `solid-color-picker-${event.target.value}`;
    const rgbColors = this.getRGBColors(event.target.value);

    // Set background color to custom color
    event.target.style.backgroundColor = `rgb(${rgbColors.toString()})`;

    if (this.customColorPickerRef.current) {
      const colorInputBackground =
        this.customColorPickerRef.current.children[0].style.backgroundColor;
      this.customColorPickerRef.current.style.backgroundColor =
        colorInputBackground;
    }

    // Set icon color based on the selected color
    const isColorDark = this.isWallpaperColorDark(rgbColors);
    if (this.customColorPickerRef.current) {
      if (isColorDark) {
        this.customColorPickerRef.current.classList.add("is-dark");
      } else {
        this.customColorPickerRef.current.classList.remove("is-dark");
      }

      // Remove any possible initial classes
      this.customColorPickerRef.current.classList.remove(
        "custom-color-set",
        "custom-color-dark",
        "default-color-set"
      );
    }

    // Setting this now so when we remove v1 we don't have to migrate v1 values.
    this.props.setPref("newtabWallpapers.wallpaper", id);
  }

  // Note: There's a separate event (debouncedHandleChange) that fires the handleChange
  // event but is delayed so that it doesn't fire multiple events when a user
  // is selecting a custom color background
  handleChange(event) {
    let { id } = event.target;

    // Set ID to include hex value of custom color
    if (id === "solid-color-picker") {
      id = `solid-color-picker-${event.target.value}`;
    }

    this.props.setPref("newtabWallpapers.wallpaper", id);

    const uploadedPreviously =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY];

    this.handleUserEvent(at.WALLPAPER_CLICK, {
      selected_wallpaper: id,
      had_previous_wallpaper: !!this.props.activeWallpaper,
      had_uploaded_previously: !!uploadedPreviously,
    });
  }

  focusCategory(focusIndex) {
    if (!this.categoryRef) {
      return;
    }

    const el = this.categoryRef[focusIndex];
    if (el) {
      el.focus();
    }
  }

  // function implementing arrow navigation for wallpaper category selection
  handleCategoryKeyDown(event, category) {
    const getIndex = this.categoryRef.findIndex(cat => cat.id === category);
    if (getIndex === -1) {
      return; // prevents errors if wallpaper index isn't found when navigating with arrow keys
    }

    const isRTL = document.dir === "rtl"; // returns true if page language is right-to-left
    let eventKey = event.key;

    if (eventKey === "ArrowRight" || eventKey === "ArrowLeft") {
      if (isRTL) {
        eventKey = eventKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
      }
    }

    let nextIndex = getIndex;

    if (eventKey === "ArrowRight") {
      nextIndex =
        getIndex + 1 < this.categoryRef.length ? getIndex + 1 : getIndex;
    } else if (eventKey === "ArrowLeft") {
      nextIndex = getIndex - 1 >= 0 ? getIndex - 1 : getIndex;
    }

    this.setState({ focusedCategoryIndex: nextIndex }, () =>
      this.focusCategory(nextIndex)
    );
  }

  // function implementing arrow navigation for wallpaper selection
  handleWallpaperKeyDown(event, title) {
    if (event.key === "Tab") {
      if (event.shiftKey) {
        event.preventDefault();
        this.backToMenuButton?.focus();
      } else {
        event.preventDefault(); // prevent tabbing within wallpaper selection. We should only be using the Tab key to tab between groups
      }
      return;
    }

    const isRTL = document.dir === "rtl"; // returns true if page language is right-to-left
    let eventKey = event.key;

    if (eventKey === "ArrowRight" || eventKey === "ArrowLeft") {
      if (isRTL) {
        eventKey = eventKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
      }
    }

    const getIndex = this.wallpaperRef.findIndex(
      wallpaper => wallpaper.id === title
    );

    if (getIndex === -1) {
      return; // prevents errors if wallpaper index isn't found when navigating with arrow keys
    }

    // the set layout of columns per row for the wallpaper selection
    const columnCount = 3;
    let nextIndex = getIndex;

    if (eventKey === "ArrowRight") {
      nextIndex =
        getIndex + 1 < this.wallpaperRef.length ? getIndex + 1 : getIndex;
    } else if (eventKey === "ArrowLeft") {
      nextIndex = getIndex - 1 >= 0 ? getIndex - 1 : getIndex;
    } else if (eventKey === "ArrowDown") {
      nextIndex =
        getIndex + columnCount < this.wallpaperRef.length
          ? getIndex + columnCount
          : getIndex;
    } else if (eventKey === "ArrowUp") {
      nextIndex =
        getIndex - columnCount >= 0 ? getIndex - columnCount : getIndex;
    }

    this.wallpaperRef[nextIndex].tabIndex = 0;
    this.wallpaperRef[getIndex].tabIndex = -1;
    this.wallpaperRef[nextIndex].focus();
    this.wallpaperRef[nextIndex].click();
  }

  handleReset() {
    const uploadedPreviously =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY];

    const selectedWallpaper =
      this.props.Prefs.values["newtabWallpapers.wallpaper"];

    // If a custom wallpaper is set, remove it
    if (selectedWallpaper === "custom") {
      this.props.dispatch(
        ac.OnlyToMain({
          type: at.WALLPAPER_REMOVE_UPLOAD,
        })
      );
    }

    // Reset active wallpaper
    this.props.setPref("newtabWallpapers.wallpaper", "");

    // Fire WALLPAPER_CLICK telemetry event
    this.handleUserEvent(at.WALLPAPER_CLICK, {
      selected_wallpaper: "none",
      had_previous_wallpaper: !!this.props.activeWallpaper,
      had_uploaded_previously: !!uploadedPreviously,
    });
  }

  handleCategory = event => {
    this.setState({ activeCategory: event.target.id });

    this.handleUserEvent(at.WALLPAPER_CATEGORY_CLICK, event.target.id);

    // Notify parent menu when subpanel opens
    if (this.props.onSubpanelToggle) {
      this.props.onSubpanelToggle(true);
    }

    let fluent_id;
    switch (event.target.id) {
      case "abstracts":
        fluent_id = "newtab-wallpaper-category-title-abstract";
        break;
      case "celestial":
        fluent_id = "newtab-wallpaper-category-title-celestial";
        break;
      case "photographs":
        fluent_id = "newtab-wallpaper-category-title-photographs";
        break;
      case "solid-colors":
        fluent_id = "newtab-wallpaper-category-title-colors";
        break;
      case "firefox":
        fluent_id = "newtab-wallpaper-category-title-firefox";
        break;
    }

    this.setState({ activeCategoryFluentID: fluent_id });
  };

  // Custom wallpaper image upload
  async handleUpload() {
    const wallpaperUploadMaxFileSizeEnabled =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE_ENABLED];

    const wallpaperUploadMaxFileSize =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE];

    const uploadedPreviously =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOADED_PREVIOUSLY];

    // Create a file input since category buttons are radio inputs
    const fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = "image/*"; // only allow image files

    // Catch cancel events
    fileInput.oncancel = async () => {
      this.setState({ customWallpaperErrorType: null });
    };

    // Reset error state when user begins file selection
    this.setState({ customWallpaperErrorType: null });

    // Fire when user selects a file
    fileInput.onchange = async event => {
      const [file] = event.target.files;

      if (file) {
        // Validate file type: Only accept files with a valid image MIME type
        const isValidImage = file.type && file.type.startsWith("image/");
        if (!isValidImage) {
          console.error("Invalid file type");
          this.setState({ customWallpaperErrorType: "fileType" });
          return;
        }

        // Limit image uploaded to a maximum file size if enabled
        // Note: The max file size pref (customWallpaper.fileSize) is converted to megabytes (MB)
        // Example: if pref value is 5, max file size is 5 MB
        const maxSize = wallpaperUploadMaxFileSize * 1024 * 1024;
        if (wallpaperUploadMaxFileSizeEnabled && file.size > maxSize) {
          console.error("File size exceeds limit");
          this.setState({ customWallpaperErrorType: "fileSize" });
          return;
        }

        this.props.dispatch(
          ac.OnlyToMain({
            type: at.WALLPAPER_UPLOAD,
            data: { file },
          })
        );

        // Set active wallpaper ID to "custom"
        this.props.setPref("newtabWallpapers.wallpaper", "custom");

        // Update the uploadedPreviously pref to TRUE
        // Note: this pref used for telemetry. Do not reset to false.
        this.props.setPref(PREF_WALLPAPER_UPLOADED_PREVIOUSLY, true);

        this.handleUserEvent(at.WALLPAPER_CLICK, {
          selected_wallpaper: "custom",
          had_previous_wallpaper: !!this.props.activeWallpaper,
          had_uploaded_previously: !!uploadedPreviously,
        });
      }
    };

    fileInput.click();
  }

  handleBack() {
    this.setState({ activeCategory: null }, () => {
      // Notify parent menu when subpanel closes
      if (this.props.onSubpanelToggle) {
        this.props.onSubpanelToggle(false);
      }

      // Wait for the category grid to be back in the DOM
      requestAnimationFrame(() => {
        this.focusCategory(this.state.focusedCategoryIndex);
      });
    });
  }

  // Record user interaction when changing wallpaper and reseting wallpaper to default
  handleUserEvent(type, data) {
    this.props.dispatch(ac.OnlyToMain({ type, data }));
  }

  setActiveId = id => {
    this.setState({ activeId: id }); // Set the active ID
  };

  getRGBColors(input) {
    if (input.length !== 7) {
      return [];
    }

    const r = parseInt(input.substr(1, 2), 16);
    const g = parseInt(input.substr(3, 2), 16);
    const b = parseInt(input.substr(5, 2), 16);

    return [r, g, b];
  }

  isWallpaperColorDark([r, g, b]) {
    return 0.2125 * r + 0.7154 * g + 0.0721 * b <= 110;
  }

  sortWallpapersByOrder(wallpapers) {
    return wallpapers.sort((a, b) => {
      const aOrder = a.order || 0;
      const bOrder = b.order || 0;
      if (aOrder === 0 && bOrder === 0) {
        return 0;
      }
      if (aOrder === 0) {
        return 1;
      }
      if (bOrder === 0) {
        return -1;
      }
      return aOrder - bOrder;
    });
  }

  render() {
    const prefs = this.props.Prefs.values;
    const { wallpaperList, categories } = this.props.Wallpapers;
    const { activeWallpaper } = this.props;
    const { activeCategory, showColorPicker } = this.state;
    const { activeCategoryFluentID } = this.state;
    let filteredWallpapers = wallpaperList.filter(
      wallpaper => wallpaper.category === activeCategory
    );
    const wallpaperUploadMaxFileSize =
      this.props.Prefs.values[PREF_WALLPAPER_UPLOAD_MAX_FILE_SIZE];

    function reduceColorsToFitCustomColorInput(arr) {
      // Reduce the amount of custom colors to make space for the custom color picker
      while (arr.length % 3 !== 2) {
        arr.pop();
      }
      return arr;
    }

    let wallpaperCustomSolidColorHex = null;

    const selectedWallpaper = prefs["newtabWallpapers.wallpaper"];

    // User has previous selected a custom color
    if (selectedWallpaper.includes("solid-color-picker")) {
      this.setState({ showColorPicker: true });
      const regex = /#([a-fA-F0-9]{6})/;
      [wallpaperCustomSolidColorHex] = selectedWallpaper.match(regex);
    }

    // Enable custom color select if pref'ed on
    this.setState({
      showColorPicker: prefs["newtabWallpapers.customColor.enabled"],
    });

    // Remove last item of solid colors to make space for custom color picker
    if (
      prefs["newtabWallpapers.customColor.enabled"] &&
      activeCategory === "solid-colors"
    ) {
      filteredWallpapers =
        reduceColorsToFitCustomColorInput(filteredWallpapers);
    }

    // Bug 1953012 - If nothing selected, default to color of customize panel
    // --color-blue-70 : #054096
    // --color-blue-05 : #deeafc
    const starterColorHex = this.prefersDarkQuery?.matches
      ? "#054096"
      : "#deeafc";

    // Set initial state of the color picker (depending if the user has already set a custom color)
    let initStateClassname = wallpaperCustomSolidColorHex
      ? "custom-color-set"
      : "default-color-set";

    // If a custom color picker is set, make sure the icon has the correct contrast
    if (wallpaperCustomSolidColorHex) {
      const rgbColors = this.getRGBColors(wallpaperCustomSolidColorHex);
      const isColorDark = this.isWallpaperColorDark(rgbColors);
      if (isColorDark) {
        initStateClassname += " custom-color-dark";
      }
    }

    let colorPickerInput =
      showColorPicker && activeCategory === "solid-colors" ? (
        <div
          className={`theme-custom-color-picker ${initStateClassname}`}
          ref={this.customColorPickerRef}
        >
          <input
            onInput={this.handleColorInput}
            onChange={this.debouncedHandleChange}
            onClick={() => this.setActiveId("solid-color-picker")} //
            type="color"
            name={`wallpaper-solid-color-picker`}
            id="solid-color-picker"
            // aria-checked is not applicable for input[type="color"] elements
            aria-current={this.state.activeId === "solid-color-picker"}
            value={wallpaperCustomSolidColorHex || starterColorHex}
            className={`wallpaper-input
              ${this.state.activeId === "solid-color-picker" ? "active" : ""}`}
            ref={this.customColorInput}
          />
          <label
            htmlFor="solid-color-picker"
            data-l10n-id="newtab-wallpaper-custom-color"
          ></label>
        </div>
      ) : (
        ""
      );

    return (
      <div>
        <div className="category-header">
          <h2 data-l10n-id="newtab-wallpaper-title"></h2>
          <button
            className="wallpapers-reset"
            onClick={this.handleReset}
            data-l10n-id="newtab-wallpaper-reset"
          />
        </div>
        <div
          role="grid"
          aria-label="Wallpaper category selection. Use arrow keys to navigate."
        >
          <fieldset className="category-list">
            {categories.map((category, index) => {
              const filteredList = wallpaperList.filter(
                wallpaper => wallpaper.category === category
              );
              const sortedList = this.sortWallpapersByOrder(filteredList);
              const activeWallpaperObj =
                activeWallpaper &&
                sortedList.find(wp => wp.title === activeWallpaper);
              // Detect custom solid color
              const isCustomSolidColor =
                category === "solid-colors" &&
                activeWallpaper.startsWith("solid-color-picker");
              const thumbnail = activeWallpaperObj || sortedList[0];
              let fluent_id;
              switch (category) {
                case "abstracts":
                  fluent_id = "newtab-wallpaper-category-title-abstract";
                  break;
                case "celestial":
                  fluent_id = "newtab-wallpaper-category-title-celestial";
                  break;
                case "custom-wallpaper":
                  fluent_id = "newtab-wallpaper-upload-image";
                  break;
                case "photographs":
                  fluent_id = "newtab-wallpaper-category-title-photographs";
                  break;
                case "solid-colors":
                  fluent_id = "newtab-wallpaper-category-title-colors";
                  break;
                case "firefox":
                  fluent_id = "newtab-wallpaper-category-title-firefox";
                  break;
              }
              let style = {};
              if (thumbnail?.wallpaperUrl) {
                style.backgroundImage = `url(${thumbnail.wallpaperUrl})`;
                style.backgroundPosition =
                  thumbnail.background_position || "center";
              } else {
                style.backgroundColor = thumbnail?.solid_color || "";
              }
              // If custom solid color is active, override the thumbnail to the chosen hex
              if (isCustomSolidColor) {
                const hex =
                  activeWallpaper.split("solid-color-picker-")[1] || "";
                style.backgroundColor = hex;
              }
              const isCategorySelected =
                activeWallpaperObj || isCustomSolidColor;
              return (
                <div key={category}>
                  <button
                    ref={el => {
                      if (el) {
                        this.categoryRef[index] = el;
                      }
                    }}
                    id={category}
                    style={style}
                    onKeyDown={e => this.handleCategoryKeyDown(e, category)}
                    // Add overrides for custom wallpaper upload UI
                    onClick={event => {
                      this.setState({ focusedCategoryIndex: index });
                      if (category !== "custom-wallpaper") {
                        this.handleCategory(event);
                      } else {
                        this.handleUpload();
                      }
                    }}
                    className={`wallpaper-input
                      ${category === "custom-wallpaper" ? "theme-custom-wallpaper" : ""}
                      ${isCategorySelected ? "selected" : ""}`}
                    tabIndex={
                      this.state.focusedCategoryIndex === index ? 0 : -1
                    }
                    {...(category === "custom-wallpaper"
                      ? { "aria-errormessage": "customWallpaperError" }
                      : {})}
                  />
                  <label htmlFor={category} data-l10n-id={fluent_id}>
                    {fluent_id}
                  </label>
                </div>
              );
            })}
          </fieldset>
          {this.state.customWallpaperErrorType && (
            <div className="custom-wallpaper-error" id="customWallpaperError">
              <span className="icon icon-info"></span>
              {(() => {
                switch (this.state.customWallpaperErrorType) {
                  case "fileSize":
                    return (
                      <span
                        data-l10n-id="newtab-wallpaper-error-max-file-size"
                        data-l10n-args={`{"file_size": ${wallpaperUploadMaxFileSize}}`}
                      ></span>
                    );
                  case "fileType":
                    return (
                      <span data-l10n-id="newtab-wallpaper-error-upload-file-type"></span>
                    );
                  default:
                    return null;
                }
              })()}
            </div>
          )}
        </div>

        <CSSTransition
          in={!!activeCategory}
          timeout={300}
          classNames="wallpaper-list"
          unmountOnExit={true}
        >
          <section className="category wallpaper-list ignore-color-mode">
            <button
              className="arrow-button"
              data-l10n-id={activeCategoryFluentID}
              onClick={this.handleBack}
              ref={el => {
                this.backToMenuButton = el;
              }}
            />
            <div
              role="grid"
              aria-label="Wallpaper selection. Use arrow keys to navigate."
            >
              <fieldset>
                {this.sortWallpapersByOrder(filteredWallpapers).map(
                  (
                    {
                      background_position,
                      fluent_id,
                      solid_color,
                      theme,
                      title,
                      wallpaperUrl,
                    },
                    index
                  ) => {
                    let style = {};
                    if (wallpaperUrl) {
                      style.backgroundImage = `url(${wallpaperUrl})`;
                      style.backgroundPosition =
                        background_position || "center";
                    } else {
                      style.backgroundColor = solid_color || "";
                    }
                    return (
                      <>
                        <input
                          ref={el => {
                            if (el) {
                              this.wallpaperRef[index] = el;
                            }
                          }}
                          onChange={this.handleChange}
                          onKeyDown={e => this.handleWallpaperKeyDown(e, title)}
                          style={style}
                          type="radio"
                          name={`wallpaper-${title}`}
                          id={title}
                          value={title}
                          checked={title === activeWallpaper}
                          aria-checked={title === activeWallpaper}
                          className={`wallpaper-input theme-${theme} ${this.state.activeId === title ? "active" : ""}`}
                          onClick={() => this.setActiveId(title)} //
                          tabIndex={index === 0 ? 0 : -1} //the first wallpaper in the array will have a tabindex of 0 so we can tab into it. The rest will have a tabindex of -1
                        />
                        <label
                          htmlFor={title}
                          className="sr-only"
                          data-l10n-id={fluent_id}
                        >
                          {fluent_id}
                        </label>
                      </>
                    );
                  }
                )}
                {colorPickerInput}
              </fieldset>
            </div>
          </section>
        </CSSTransition>
      </div>
    );
  }
}

export const WallpaperCategories = connect(state => {
  return {
    Wallpapers: state.Wallpapers,
    Prefs: state.Prefs,
  };
})(_WallpaperCategories);
