/* 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 {
  actionCreators as ac,
  actionTypes as at,
} from "common/Actions.sys.mjs";
import { cardContextTypes } from "./types";
import { connect } from "react-redux";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import React from "react";
import { ScreenshotUtils } from "content-src/lib/screenshot-utils";

// Keep track of pending image loads to only request once
const gImageLoading = new Map();

/**
 * Card component.
 * Cards are found within a Section component and contain information about a link such
 * as preview image, page title, page description, and some context about if the page
 * was visited, bookmarked, trending etc...
 * Each Section can make an unordered list of Cards which will create one instane of
 * this class. Each card will then get a context menu which reflects the actions that
 * can be done on this Card.
 */
export class _Card extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      activeCard: null,
      imageLoaded: false,
      cardImage: null,
    };
    this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);
    this.onLinkClick = this.onLinkClick.bind(this);
  }

  /**
   * Helper to conditionally load an image and update state when it loads.
   */
  async maybeLoadImage() {
    // No need to load if it's already loaded or no image
    const { cardImage } = this.state;
    if (!cardImage) {
      return;
    }

    const imageUrl = cardImage.url;
    if (!this.state.imageLoaded) {
      // Initialize a promise to share a load across multiple card updates
      if (!gImageLoading.has(imageUrl)) {
        const loaderPromise = new Promise((resolve, reject) => {
          const loader = new Image();
          loader.addEventListener("load", resolve);
          loader.addEventListener("error", reject);
          loader.src = imageUrl;
        });

        // Save and remove the promise only while it's pending
        gImageLoading.set(imageUrl, loaderPromise);
        loaderPromise
          .catch(ex => ex)
          .then(() => gImageLoading.delete(imageUrl))
          .catch();
      }

      // Wait for the image whether just started loading or reused promise
      try {
        await gImageLoading.get(imageUrl);
      } catch (ex) {
        // Ignore the failed image without changing state
        return;
      }

      // Only update state if we're still waiting to load the original image
      if (
        ScreenshotUtils.isRemoteImageLocal(
          this.state.cardImage,
          this.props.link.image
        ) &&
        !this.state.imageLoaded
      ) {
        this.setState({ imageLoaded: true });
      }
    }
  }

  /**
   * Helper to obtain the next state based on nextProps and prevState.
   *
   * NOTE: Rename this method to getDerivedStateFromProps when we update React
   *       to >= 16.3. We will need to update tests as well. We cannot rename this
   *       method to getDerivedStateFromProps now because there is a mismatch in
   *       the React version that we are using for both testing and production.
   *       (i.e. react-test-render => "16.3.2", react => "16.2.0").
   *
   * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.
   */
  static getNextStateFromProps(nextProps, prevState) {
    const { image } = nextProps.link;
    const imageInState = ScreenshotUtils.isRemoteImageLocal(
      prevState.cardImage,
      image
    );
    let nextState = null;

    // Image is updating.
    if (!imageInState && nextProps.link) {
      nextState = { imageLoaded: false };
    }

    if (imageInState) {
      return nextState;
    }

    // Since image was updated, attempt to revoke old image blob URL, if it exists.
    ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.cardImage);

    nextState = nextState || {};
    nextState.cardImage = ScreenshotUtils.createLocalImageObject(image);

    return nextState;
  }

  onMenuButtonUpdate(isOpen) {
    if (isOpen) {
      this.setState({ activeCard: this.props.index });
    } else {
      this.setState({ activeCard: null });
    }
  }

  /**
   * Report to telemetry additional information about the item.
   */
  _getTelemetryInfo() {
    // Filter out "history" type for being the default
    if (this.props.link.type !== "history") {
      return { value: { card_type: this.props.link.type } };
    }

    return null;
  }

  onLinkClick(event) {
    event.preventDefault();
    const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
    if (this.props.link.type === "download") {
      this.props.dispatch(
        ac.OnlyToMain({
          type: at.OPEN_DOWNLOAD_FILE,
          data: Object.assign(this.props.link, {
            event: { button, ctrlKey, metaKey, shiftKey },
          }),
        })
      );
    } else {
      this.props.dispatch(
        ac.OnlyToMain({
          type: at.OPEN_LINK,
          data: Object.assign(this.props.link, {
            event: { altKey, button, ctrlKey, metaKey, shiftKey },
          }),
        })
      );
    }
    if (this.props.isWebExtension) {
      this.props.dispatch(
        ac.WebExtEvent(at.WEBEXT_CLICK, {
          source: this.props.eventSource,
          url: this.props.link.url,
          action_position: this.props.index,
        })
      );
    } else {
      this.props.dispatch(
        ac.UserEvent(
          Object.assign(
            {
              event: "CLICK",
              source: this.props.eventSource,
              action_position: this.props.index,
            },
            this._getTelemetryInfo()
          )
        )
      );

      if (this.props.shouldSendImpressionStats) {
        this.props.dispatch(
          ac.ImpressionStats({
            source: this.props.eventSource,
            click: 0,
            tiles: [{ id: this.props.link.guid, pos: this.props.index }],
          })
        );
      }
    }
  }

  componentDidMount() {
    this.maybeLoadImage();
  }

  componentDidUpdate() {
    this.maybeLoadImage();
  }

  // NOTE: Remove this function when we update React to >= 16.3 since React will
  //       call getDerivedStateFromProps automatically. We will also need to
  //       rename getNextStateFromProps to getDerivedStateFromProps.
  componentWillMount() {
    const nextState = _Card.getNextStateFromProps(this.props, this.state);
    if (nextState) {
      this.setState(nextState);
    }
  }

  // NOTE: Remove this function when we update React to >= 16.3 since React will
  //       call getDerivedStateFromProps automatically. We will also need to
  //       rename getNextStateFromProps to getDerivedStateFromProps.
  componentWillReceiveProps(nextProps) {
    const nextState = _Card.getNextStateFromProps(nextProps, this.state);
    if (nextState) {
      this.setState(nextState);
    }
  }

  componentWillUnmount() {
    ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.cardImage);
  }

  render() {
    const {
      index,
      className,
      link,
      dispatch,
      contextMenuOptions,
      eventSource,
      shouldSendImpressionStats,
    } = this.props;
    const { props } = this;
    const title = link.title || link.hostname;
    const isContextMenuOpen = this.state.activeCard === index;
    // Display "now" as "trending" until we have new strings #3402
    const { icon, fluentID } =
      cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
    const hasImage = this.state.cardImage || link.hasImage;
    const imageStyle = {
      backgroundImage: this.state.cardImage
        ? `url(${this.state.cardImage.url})`
        : "none",
    };
    const outerClassName = [
      "card-outer",
      className,
      isContextMenuOpen && "active",
      props.placeholder && "placeholder",
    ]
      .filter(v => v)
      .join(" ");

    return (
      <li className={outerClassName}>
        <a
          href={link.type === "pocket" ? link.open_url : link.url}
          onClick={!props.placeholder ? this.onLinkClick : undefined}
        >
          <div className="card">
            <div className="card-preview-image-outer">
              {hasImage && (
                <div
                  className={`card-preview-image${
                    this.state.imageLoaded ? " loaded" : ""
                  }`}
                  style={imageStyle}
                />
              )}
            </div>
            <div className="card-details">
              {link.type === "download" && (
                <div
                  className="card-host-name alternate"
                  data-l10n-id="newtab-menu-open-file"
                />
              )}
              {link.hostname && (
                <div className="card-host-name">
                  {link.hostname.slice(0, 100)}
                  {link.type === "download" && `  \u2014 ${link.description}`}
                </div>
              )}
              <div
                className={[
                  "card-text",
                  icon ? "" : "no-context",
                  link.description ? "" : "no-description",
                  link.hostname ? "" : "no-host-name",
                ].join(" ")}
              >
                <h4 className="card-title" dir="auto">
                  {link.title}
                </h4>
                <p className="card-description" dir="auto">
                  {link.description}
                </p>
              </div>
              <div className="card-context">
                {icon && !link.context && (
                  <span
                    aria-haspopup="true"
                    className={`card-context-icon icon icon-${icon}`}
                  />
                )}
                {link.icon && link.context && (
                  <span
                    aria-haspopup="true"
                    className="card-context-icon icon"
                    style={{ backgroundImage: `url('${link.icon}')` }}
                  />
                )}
                {fluentID && !link.context && (
                  <div className="card-context-label" data-l10n-id={fluentID} />
                )}
                {link.context && (
                  <div className="card-context-label">{link.context}</div>
                )}
              </div>
            </div>
          </div>
        </a>
        {!props.placeholder && (
          <ContextMenuButton
            tooltip="newtab-menu-content-tooltip"
            tooltipArgs={{ title }}
            onUpdate={this.onMenuButtonUpdate}
          >
            <LinkMenu
              dispatch={dispatch}
              index={index}
              source={eventSource}
              options={link.contextMenuOptions || contextMenuOptions}
              site={link}
              siteInfo={this._getTelemetryInfo()}
              shouldSendImpressionStats={shouldSendImpressionStats}
            />
          </ContextMenuButton>
        )}
      </li>
    );
  }
}
_Card.defaultProps = { link: {} };
export const Card = connect(state => ({
  platform: state.Prefs.values.platform,
}))(_Card);
export const PlaceholderCard = props => (
  <Card placeholder={true} className={props.className} />
);
