import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
import {FormattedMessage, injectIntl} from "react-intl";
import {cardContextTypes} from "./types";
import {connect} from "react-redux";
import {GetPlatformString} from "content-src/lib/link-menu-options";
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,
      showContextMenu: false,
      cardImage: null,
    };
    this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
    this.onMenuUpdate = this.onMenuUpdate.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
      await gImageLoading.get(imageUrl);

      // 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;
  }

  onMenuButtonClick(event) {
    event.preventDefault();
    this.setState({
      activeCard: this.props.index,
      showContextMenu: true,
    });
  }

  /**
   * 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();
    if (this.props.link.type === "download") {
      this.props.dispatch(ac.OnlyToMain({
        type: at.SHOW_DOWNLOAD_FILE,
        data: this.props.link,
      }));
    } else {
      const {altKey, button, ctrlKey, metaKey, shiftKey} = event;
      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}],
        }));
      }
    }
  }

  onMenuUpdate(showContextMenu) {
    this.setState({showContextMenu});
  }

  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 isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
    // Display "now" as "trending" until we have new strings #3402
    const {icon, intlID} = 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"><FormattedMessage id={GetPlatformString(this.props.platform)} /></div>}
            {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}')`}} />}
              {intlID && !link.context && <div className="card-context-label"><FormattedMessage id={intlID} defaultMessage="Visited" /></div>}
              {link.context && <div className="card-context-label">{link.context}</div>}
            </div>
          </div>
        </div>
      </a>
      {!props.placeholder && <button aria-haspopup="true" className="context-menu-button icon" title={this.props.intl.formatMessage({id: "context_menu_title"})}
        onClick={this.onMenuButtonClick}>
        <span aria-haspopup="true" className="sr-only">{`Open context menu for ${link.title}`}</span>
      </button>}
      {isContextMenuOpen &&
        <LinkMenu
          dispatch={dispatch}
          index={index}
          source={eventSource}
          onUpdate={this.onMenuUpdate}
          options={link.contextMenuOptions || contextMenuOptions}
          site={link}
          siteInfo={this._getTelemetryInfo()}
          shouldSendImpressionStats={shouldSendImpressionStats} />
      }
   </li>);
  }
}
_Card.defaultProps = {link: {}};
export const Card = connect(state => ({platform: state.Prefs.values.platform}))(injectIntl(_Card));
export const PlaceholderCard = props => <Card placeholder={true} className={props.className} />;
