/*
 * This file is part of the MediaWiki extension MultimediaViewer.
 *
 * MultimediaViewer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * MultimediaViewer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MultimediaViewer.  If not, see <http://www.gnu.org/licenses/>.
 */

const ThumbnailWidth = require( './model/mmv.model.ThumbnailWidth.js' );
const { thumbnailBucketSizes } = require( './config.json' );

/**
 * A helper class for bucketing image sizes.
 * Bucketing helps to avoid cache fragmentation and thus speed up image loading:
 * instead of generating potentially hundreds of different thumbnail sizes, we restrict
 * ourselves to a short list of acceptable thumbnail widths, and only ever load thumbnails
 * of that size. Final size adjustment is done in a thumbnail.
 *
 * See also the [Standardized thumbnail sizes RFC][1].
 *
 * [1]: https://www.mediawiki.org/wiki/Talk:Requests_for_comment/Standardized_thumbnails_sizes
 */
class ThumbnailWidthCalculator {
	/**
	 * @param {Object} [options]
	 * @param {number[]} [options.widthBuckets] see {@link ThumbnailWidthCalculator#widthBuckets}
	 * @param {number} [options.devicePixelRatio] see {@link ThumbnailWidthCalculator#devicePixelRatio};
	 *     will be autodetected if omitted
	 */
	constructor( options ) {
		options = Object.assign( {
			// default image widths
			widthBuckets: thumbnailBucketSizes,

			// screen pixel per CSS pixel
			devicePixelRatio: window.devicePixelRatio || 1
		}, options );

		if ( !options.widthBuckets.length ) {
			throw new Error( 'No buckets!' );
		}

		/**
		 * List of thumbnail width bucket sizes, in pixels.
		 *
		 * @property {number[]}
		 */
		this.widthBuckets = options.widthBuckets;
		this.widthBuckets.sort( ( a, b ) => a - b );

		/**
		 * Screen pixel count per CSS pixel.
		 *
		 * @property {number}
		 */
		this.devicePixelRatio = options.devicePixelRatio;
	}

	/**
	 * Finds the smallest bucket which is large enough to hold the target size
	 * (i. e. the smallest bucket whose size is equal to or greater than the target).
	 * If none of the buckets are large enough, returns the largest bucket.
	 *
	 * @param {number} target
	 * @return {number}
	 */
	findNextBucket( target ) {
		let bucket;
		const buckets = this.widthBuckets;

		for ( let i = 0; i < buckets.length; i++ ) {
			bucket = buckets[ i ];

			if ( bucket >= target ) {
				return bucket;
			}
		}

		// If we failed to find a high enough size...good luck
		return bucket;
	}

	/**
	 * Finds the largest width for an image so that it will still fit into a given bounding box,
	 * based on the size of a sample (some smaller version of the same image, like the thumbnail
	 * shown in the article) which is used to calculate the ratio.
	 *
	 * This is for internal use, you should probably use calculateWidths() instead.
	 *
	 * @protected
	 * @param {number} boundingWidth width of the bounding box
	 * @param {number} boundingHeight height of the bounding box
	 * @param {number} sampleWidth width of the sample image
	 * @param {number} sampleHeight height of the sample image
	 * @return {number} the largest width so that the scaled version of the sample image fits
	 *     into the bounding box (either horizontal or vertical edges touch on both sides).
	 */
	calculateFittingWidth( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
		if ( ( boundingWidth / boundingHeight ) > ( sampleWidth / sampleHeight ) ) {
			// we are limited by height; we need to calculate the max width that fits
			return Math.round( ( sampleWidth / sampleHeight ) * boundingHeight );
		} else {
			// simple case, ratio tells us we're limited by width
			return boundingWidth;
		}
	}

	/**
	 * Finds the largest width for an image so that it will still fit into a given bounding box,
	 * based on the size of a sample (some smaller version of the same image, like the thumbnail
	 * shown in the article) which is used to calculate the ratio.
	 *
	 * Returns two values, a CSS width which is the size in pixels that should be used so the image
	 * fits exactly into the bounding box, and a real width which should be the size of the
	 * downloaded image in pixels. The two will be different for two reasons:
	 * - Images are bucketed for more efficient caching, so the real width will always be one of
	 *     the numbers in this.widthBuckets. The resulting thumbnail will be slightly larger than
	 *     the bounding box so that it takes roughly the same amount of bandwidth and
	 *     looks decent when resized by the browser.
	 * - For devices with high pixel density (multiple actual pixels per CSS pixel) we want to use
	 *     a larger image so that there will be roughly one image pixel per physical display pixel.
	 *
	 * @param {number} boundingWidth width of the bounding box, in CSS pixels
	 * @param {number} boundingHeight height of the bounding box, in CSS pixels
	 * @param {number} sampleWidth width of the sample image (in whatever - only used for aspect ratio)
	 * @param {number} sampleHeight height of the sample image (in whatever - only used for aspect ratio)
	 * @return {ThumbnailWidth}
	 */
	calculateWidths( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
		const ratio = sampleHeight / sampleWidth;

		const cssWidth = this.calculateFittingWidth( boundingWidth, boundingHeight, sampleWidth, sampleHeight );
		const cssHeight = Math.max( 1, Math.round( cssWidth * ratio ) );

		const screenPixelWidth = Math.max( 1, cssWidth * this.devicePixelRatio );

		const bucketedWidth = this.findNextBucket( screenPixelWidth );

		return new ThumbnailWidth( cssWidth, cssHeight, screenPixelWidth, bucketedWidth );
	}
}

module.exports = ThumbnailWidthCalculator;
