import { queryAll } from '../utils/util.js'
import { colorToRgb, colorBrightness } from '../utils/color.js'

/**
 * Creates and updates slide backgrounds.
 */
export default class Backgrounds {

	constructor( Reveal ) {

		this.Reveal = Reveal;

	}

	render() {

		this.element = document.createElement( 'div' );
		this.element.className = 'backgrounds';
		this.Reveal.getRevealElement().appendChild( this.element );

	}

	/**
	 * Creates the slide background elements and appends them
	 * to the background container. One element is created per
	 * slide no matter if the given slide has visible background.
	 */
	create() {

		// Clear prior backgrounds
		this.element.innerHTML = '';
		this.element.classList.add( 'no-transition' );

		// Iterate over all horizontal slides
		this.Reveal.getHorizontalSlides().forEach( slideh => {

			let backgroundStack = this.createBackground( slideh, this.element );

			// Iterate over all vertical slides
			queryAll( slideh, 'section' ).forEach( slidev => {

				this.createBackground( slidev, backgroundStack );

				backgroundStack.classList.add( 'stack' );

			} );

		} );

		// Add parallax background if specified
		if( this.Reveal.getConfig().parallaxBackgroundImage ) {

			this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")';
			this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize;
			this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat;
			this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition;

			// Make sure the below properties are set on the element - these properties are
			// needed for proper transitions to be set on the element via CSS. To remove
			// annoying background slide-in effect when the presentation starts, apply
			// these properties after short time delay
			setTimeout( () => {
				this.Reveal.getRevealElement().classList.add( 'has-parallax-background' );
			}, 1 );

		}
		else {

			this.element.style.backgroundImage = '';
			this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' );

		}

	}

	/**
	 * Creates a background for the given slide.
	 *
	 * @param {HTMLElement} slide
	 * @param {HTMLElement} container The element that the background
	 * should be appended to
	 * @return {HTMLElement} New background div
	 */
	createBackground( slide, container ) {

		// Main slide background element
		let element = document.createElement( 'div' );
		element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );

		// Inner background element that wraps images/videos/iframes
		let contentElement = document.createElement( 'div' );
		contentElement.className = 'slide-background-content';

		element.appendChild( contentElement );
		container.appendChild( element );

		slide.slideBackgroundElement = element;
		slide.slideBackgroundContentElement = contentElement;

		// Syncs the background to reflect all current background settings
		this.sync( slide );

		return element;

	}

	/**
	 * Renders all of the visual properties of a slide background
	 * based on the various background attributes.
	 *
	 * @param {HTMLElement} slide
	 */
	sync( slide ) {

		const element = slide.slideBackgroundElement,
			contentElement = slide.slideBackgroundContentElement;

		const data = {
			background: slide.getAttribute( 'data-background' ),
			backgroundSize: slide.getAttribute( 'data-background-size' ),
			backgroundImage: slide.getAttribute( 'data-background-image' ),
			backgroundVideo: slide.getAttribute( 'data-background-video' ),
			backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
			backgroundColor: slide.getAttribute( 'data-background-color' ),
			backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
			backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
			backgroundPosition: slide.getAttribute( 'data-background-position' ),
			backgroundTransition: slide.getAttribute( 'data-background-transition' ),
			backgroundOpacity: slide.getAttribute( 'data-background-opacity' ),
		};

		const dataPreload = slide.hasAttribute( 'data-preload' );

		// Reset the prior background state in case this is not the
		// initial sync
		slide.classList.remove( 'has-dark-background' );
		slide.classList.remove( 'has-light-background' );

		element.removeAttribute( 'data-loaded' );
		element.removeAttribute( 'data-background-hash' );
		element.removeAttribute( 'data-background-size' );
		element.removeAttribute( 'data-background-transition' );
		element.style.backgroundColor = '';

		contentElement.style.backgroundSize = '';
		contentElement.style.backgroundRepeat = '';
		contentElement.style.backgroundPosition = '';
		contentElement.style.backgroundImage = '';
		contentElement.style.opacity = '';
		contentElement.innerHTML = '';

		if( data.background ) {
			// Auto-wrap image urls in url(...)
			if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
				slide.setAttribute( 'data-background-image', data.background );
			}
			else {
				element.style.background = data.background;
			}
		}

		// Create a hash for this combination of background settings.
		// This is used to determine when two slide backgrounds are
		// the same.
		if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
			element.setAttribute( 'data-background-hash', data.background +
															data.backgroundSize +
															data.backgroundImage +
															data.backgroundVideo +
															data.backgroundIframe +
															data.backgroundColor +
															data.backgroundGradient +
															data.backgroundRepeat +
															data.backgroundPosition +
															data.backgroundTransition +
															data.backgroundOpacity );
		}

		// Additional and optional background properties
		if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
		if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
		if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
		if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );

		if( dataPreload ) element.setAttribute( 'data-preload', '' );

		// Background image options are set on the content wrapper
		if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
		if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
		if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
		if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;

		const contrastClass = this.getContrastClass( slide );

		if( typeof contrastClass === 'string' ) {
			slide.classList.add( contrastClass );
		}

	}

	/**
	 * Returns a class name that can be applied to a slide to indicate
	 * if it has a light or dark background.
	 *
	 * @param {*} slide
	 *
	 * @returns {string|null}
	 */
	getContrastClass( slide ) {

		const element = slide.slideBackgroundElement;

		// If this slide has a background color, we add a class that
		// signals if it is light or dark. If the slide has no background
		// color, no class will be added
		let contrastColor = slide.getAttribute( 'data-background-color' );

		// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
		if( !contrastColor || !colorToRgb( contrastColor ) ) {
			let computedBackgroundStyle = window.getComputedStyle( element );
			if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
				contrastColor = computedBackgroundStyle.backgroundColor;
			}
		}

		if( contrastColor ) {
			const rgb = colorToRgb( contrastColor );

			// Ignore fully transparent backgrounds. Some browsers return
			// rgba(0,0,0,0) when reading the computed background color of
			// an element with no background
			if( rgb && rgb.a !== 0 ) {
				if( colorBrightness( contrastColor ) < 128 ) {
					return 'has-dark-background';
				}
				else {
					return 'has-light-background';
				}
			}
		}

		return null;

	}

	/**
	 * Bubble the 'has-light-background'/'has-dark-background' classes.
	 */
	bubbleSlideContrastClassToElement( slide, target ) {

		[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
			if( slide.classList.contains( classToBubble ) ) {
				target.classList.add( classToBubble );
			}
			else {
				target.classList.remove( classToBubble );
			}
		}, this );

	}

	/**
	 * Updates the background elements to reflect the current
	 * slide.
	 *
	 * @param {boolean} includeAll If true, the backgrounds of
	 * all vertical slides (not just the present) will be updated.
	 */
	update( includeAll = false ) {

		let config = this.Reveal.getConfig();
		let currentSlide = this.Reveal.getCurrentSlide();
		let indices = this.Reveal.getIndices();

		let currentBackground = null;

		// Reverse past/future classes when in RTL mode
		let horizontalPast = config.rtl ? 'future' : 'past',
			horizontalFuture = config.rtl ? 'past' : 'future';

		// Update the classes of all backgrounds to match the
		// states of their slides (past/present/future)
		Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {

			backgroundh.classList.remove( 'past', 'present', 'future' );

			if( h < indices.h ) {
				backgroundh.classList.add( horizontalPast );
			}
			else if ( h > indices.h ) {
				backgroundh.classList.add( horizontalFuture );
			}
			else {
				backgroundh.classList.add( 'present' );

				// Store a reference to the current background element
				currentBackground = backgroundh;
			}

			if( includeAll || h === indices.h ) {
				queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {

					backgroundv.classList.remove( 'past', 'present', 'future' );

					const indexv = typeof indices.v === 'number' ? indices.v : 0;

					if( v < indexv ) {
						backgroundv.classList.add( 'past' );
					}
					else if ( v > indexv ) {
						backgroundv.classList.add( 'future' );
					}
					else {
						backgroundv.classList.add( 'present' );

						// Only if this is the present horizontal and vertical slide
						if( h === indices.h ) currentBackground = backgroundv;
					}

				} );
			}

		} );

		// The previous background may refer to a DOM element that has
		// been removed after a presentation is synced & bgs are recreated
		if( this.previousBackground && !this.previousBackground.closest( 'body' ) ) {
			this.previousBackground = null;
		}

		if( currentBackground && this.previousBackground ) {

			// Don't transition between identical backgrounds. This
			// prevents unwanted flicker.
			let previousBackgroundHash = this.previousBackground.getAttribute( 'data-background-hash' );
			let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );

			if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
				this.element.classList.add( 'no-transition' );

				// If multiple slides have the same background video, carry
				// the <video> element forward so that it plays continuously
				// across multiple slides
				const currentVideo = currentBackground.querySelector( 'video' );
				const previousVideo = this.previousBackground.querySelector( 'video' );

				if( currentVideo && previousVideo ) {

					const currentVideoParent = currentVideo.parentNode;
					const previousVideoParent = previousVideo.parentNode;

					// Swap the two videos
					previousVideoParent.appendChild( currentVideo );
					currentVideoParent.appendChild( previousVideo );

				}
			}

		}

		// Stop content inside of previous backgrounds
		if( this.previousBackground ) {

			this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );

		}

		// Start content in the current background
		if( currentBackground ) {

			this.Reveal.slideContent.startEmbeddedContent( currentBackground );

			let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
			if( currentBackgroundContent ) {

				let backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';

				// Restart GIFs (doesn't work in Firefox)
				if( /\.gif/i.test( backgroundImageURL ) ) {
					currentBackgroundContent.style.backgroundImage = '';
					window.getComputedStyle( currentBackgroundContent ).opacity;
					currentBackgroundContent.style.backgroundImage = backgroundImageURL;
				}

			}

			this.previousBackground = currentBackground;

		}

		// If there's a background brightness flag for this slide,
		// bubble it to the .reveal container
		if( currentSlide ) {
			this.bubbleSlideContrastClassToElement( currentSlide, this.Reveal.getRevealElement() );
		}

		// Allow the first background to apply without transition
		setTimeout( () => {
			this.element.classList.remove( 'no-transition' );
		}, 10 );

	}

	/**
	 * Updates the position of the parallax background based
	 * on the current slide index.
	 */
	updateParallax() {

		let indices = this.Reveal.getIndices();

		if( this.Reveal.getConfig().parallaxBackgroundImage ) {

			let horizontalSlides = this.Reveal.getHorizontalSlides(),
				verticalSlides = this.Reveal.getVerticalSlides();

			let backgroundSize = this.element.style.backgroundSize.split( ' ' ),
				backgroundWidth, backgroundHeight;

			if( backgroundSize.length === 1 ) {
				backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
			}
			else {
				backgroundWidth = parseInt( backgroundSize[0], 10 );
				backgroundHeight = parseInt( backgroundSize[1], 10 );
			}

			let slideWidth = this.element.offsetWidth,
				horizontalSlideCount = horizontalSlides.length,
				horizontalOffsetMultiplier,
				horizontalOffset;

			if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {
				horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;
			}
			else {
				horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
			}

			horizontalOffset = horizontalOffsetMultiplier * indices.h * -1;

			let slideHeight = this.element.offsetHeight,
				verticalSlideCount = verticalSlides.length,
				verticalOffsetMultiplier,
				verticalOffset;

			if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {
				verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;
			}
			else {
				verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
			}

			verticalOffset = verticalSlideCount > 0 ?  verticalOffsetMultiplier * indices.v : 0;

			this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';

		}

	}

	destroy() {

		this.element.remove();

	}

}
