/* 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";

const PLACEHOLDER_IMAGE_DATA_ARRAY = [
  {
    rotation: "0deg",
    offsetx: "20px",
    offsety: "8px",
    scale: "45%",
  },
  {
    rotation: "54deg",
    offsetx: "-26px",
    offsety: "62px",
    scale: "55%",
  },
  {
    rotation: "-30deg",
    offsetx: "78px",
    offsety: "30px",
    scale: "68%",
  },
  {
    rotation: "-22deg",
    offsetx: "0",
    offsety: "92px",
    scale: "60%",
  },
  {
    rotation: "-65deg",
    offsetx: "66px",
    offsety: "28px",
    scale: "60%",
  },
  {
    rotation: "22deg",
    offsetx: "-35px",
    offsety: "62px",
    scale: "52%",
  },
  {
    rotation: "-25deg",
    offsetx: "86px",
    offsety: "-15px",
    scale: "68%",
  },
];

const PLACEHOLDER_IMAGE_COLORS_ARRAY =
  "#0090ED #FF4F5F #2AC3A2 #FF7139 #A172FF #FFA437 #FF2A8A".split(" ");

function generateIndex({ keyCode, max }) {
  if (!keyCode) {
    // Just grab a random index if we cannot generate an index from a key.
    return Math.floor(Math.random() * max);
  }

  const hashStr = str => {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      let charCode = str.charCodeAt(i);
      hash += charCode;
    }
    return hash;
  };

  const hash = hashStr(keyCode);
  return hash % max;
}

export function PlaceholderImage({ urlKey, titleKey }) {
  const dataIndex = generateIndex({
    keyCode: urlKey,
    max: PLACEHOLDER_IMAGE_DATA_ARRAY.length,
  });
  const colorIndex = generateIndex({
    keyCode: titleKey,
    max: PLACEHOLDER_IMAGE_COLORS_ARRAY.length,
  });
  const { rotation, offsetx, offsety, scale } =
    PLACEHOLDER_IMAGE_DATA_ARRAY[dataIndex];
  const color = PLACEHOLDER_IMAGE_COLORS_ARRAY[colorIndex];
  const style = {
    "--placeholderBackgroundColor": color,
    "--placeholderBackgroundRotation": rotation,
    "--placeholderBackgroundOffsetx": offsetx,
    "--placeholderBackgroundOffsety": offsety,
    "--placeholderBackgroundScale": scale,
  };

  return <div style={style} className="placeholder-image" />;
}

export class DSImage extends React.PureComponent {
  constructor(props) {
    super(props);

    this.onOptimizedImageError = this.onOptimizedImageError.bind(this);
    this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);
    this.onLoad = this.onLoad.bind(this);

    this.state = {
      isLoaded: false,
      optimizedImageFailed: false,
      useTransition: false,
    };
  }

  onIdleCallback() {
    if (!this.state.isLoaded) {
      this.setState({
        useTransition: true,
      });
    }
  }

  // Wraps the image url with the Pocket proxy to both resize and crop the image.
  reformatImageURL(url, width, height) {
    const smart = this.props.smartCrop ? "smart/" : "";
    // Change the image URL to request a size tailored for the parent container width
    // Also: force WebP, quality 75, no upscaling, no EXIF data
    // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
    const formattedUrl = `https://img-getpocket.cdn.mozilla.net/${width}x${height}/${smart}filters:format(webp):quality(75):no_upscale():strip_exif()/${encodeURIComponent(
      url
    )}`;
    return this.secureImageURL(formattedUrl);
  }

  // Wraps the image URL with the moz-cached-ohttp:// protocol.
  // This enables Firefox to load resources over Oblivious HTTP (OHTTP),
  // providing privacy-preserving resource loading.
  // Applied only when inferred personalization is enabled.
  // See: https://firefox-source-docs.mozilla.org/browser/components/mozcachedohttp/docs/index.html
  secureImageURL(url) {
    if (!this.props.secureImage) {
      return url;
    }
    return `moz-cached-ohttp://newtab-image/?url=${encodeURIComponent(url)}`;
  }

  componentDidMount() {
    this.idleCallbackId = this.props.windowObj.requestIdleCallback(
      this.onIdleCallback.bind(this)
    );
  }

  componentWillUnmount() {
    if (this.idleCallbackId) {
      this.props.windowObj.cancelIdleCallback(this.idleCallbackId);
    }
  }

  render() {
    let classNames = `ds-image
      ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}
      ${this.state && this.state.useTransition ? ` use-transition` : ``}
      ${this.state && this.state.isLoaded ? ` loaded` : ``}
    `;

    let img;

    if (this.state) {
      if (
        this.props.optimize &&
        this.props.rawSource &&
        !this.state.optimizedImageFailed
      ) {
        const baseSource = this.props.rawSource;

        // We don't care about securing this.props.source, as this exclusivly
        // comes from an older service that is not personalized.
        // This can also return a non secure url if this functionality is not enabled.
        const securedSource = this.secureImageURL(baseSource);

        let sizeRules = [];
        let srcSetRules = [];

        for (let rule of this.props.sizes) {
          let { mediaMatcher, width, height } = rule;
          let sizeRule = `${mediaMatcher} ${width}px`;
          sizeRules.push(sizeRule);
          let srcSetRule = `${this.reformatImageURL(
            baseSource,
            width,
            height
          )} ${width}w`;
          let srcSetRule2x = `${this.reformatImageURL(
            baseSource,
            width * 2,
            height * 2
          )} ${width * 2}w`;
          srcSetRules.push(srcSetRule);
          srcSetRules.push(srcSetRule2x);
        }

        const explicitSize = this.props.sizes[this.props.sizes.length - 1];

        if (this.props.sizes.length) {
          // We have to supply a fallback in the very unlikely event that none of
          // the media queries match. The smallest dimension was chosen arbitrarily.
          sizeRules.push(`${explicitSize.width}px`);
        }

        img = (
          <img
            loading="lazy"
            alt={this.props.alt_text}
            crossOrigin="anonymous"
            onLoad={this.onLoad}
            onError={this.onOptimizedImageError}
            sizes={sizeRules.join(",")}
            src={securedSource}
            srcSet={srcSetRules.join(",")}
            style={
              explicitSize
                ? {
                    width: `${explicitSize.width}px`,
                    height: `${explicitSize.height}px`,
                  }
                : undefined
            }
          />
        );
      } else if (this.props.source && !this.state.nonOptimizedImageFailed) {
        img = (
          <img
            loading="lazy"
            alt={this.props.alt_text}
            crossOrigin="anonymous"
            onLoad={this.onLoad}
            onError={this.onNonOptimizedImageError}
            src={this.props.source}
          />
        );
      } else {
        // We consider a failed to load img or source without an image as loaded.
        classNames = `${classNames} loaded`;
        // Remove the img element if we have no source. Render a placeholder instead.
        // This only happens for recent saves without a source.
        if (
          this.props.isRecentSave &&
          !this.props.rawSource &&
          !this.props.source
        ) {
          img = (
            <PlaceholderImage
              urlKey={this.props.url}
              titleKey={this.props.title}
            />
          );
        } else {
          img = <div className="broken-image" />;
        }
      }
    }

    return <picture className={classNames}>{img}</picture>;
  }

  onOptimizedImageError() {
    // This will trigger a re-render and the unoptimized 450px image will be used as a fallback
    this.setState({
      optimizedImageFailed: true,
    });
  }

  onNonOptimizedImageError() {
    this.setState({
      nonOptimizedImageFailed: true,
    });
  }

  onLoad() {
    this.setState({
      isLoaded: true,
    });
  }
}

DSImage.defaultProps = {
  source: null, // The current source style from Pocket API (always 450px)
  rawSource: null, // Unadulterated image URL to filter through Thumbor
  extraClassNames: null, // Additional classnames to append to component
  optimize: true, // Measure parent container to request exact sizes
  alt_text: "",
  windowObj: window, // Added to support unit tests
  sizes: [],
};
