/*
 * Copyright 2012, Gregg Tavares.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Gregg Tavares. nor the names of his
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* global define */

(function(root, factory) {  // eslint-disable-line
	if ( typeof define === 'function' && define.amd ) {

		// AMD. Register as an anonymous module.
		define( [], function () {

			return factory.call( root );

		} );

	} else {

		// Browser globals
		root.lessonsHelper = factory.call( root );

	}

}( this, function () {

  'use strict';  // eslint-disable-line

	const lessonSettings = window.lessonSettings || {};
	const topWindow = this;

	/**
	 * Check if the page is embedded.
	 *
	 * @param {Window?} w - window to check.
	 * @return {boolean} True of we are in an iframe
	 */
	function isInIFrame( w ) {

		w = w || topWindow;
		return w !== w.top;

	}

	function updateCSSIfInIFrame() {

		if ( isInIFrame() ) {

			try {

				document.getElementsByTagName( 'html' )[ 0 ].className = 'iframe';

			} catch ( e ) { }

			try {

				document.body.className = 'iframe';

			} catch ( e ) { }

		}

	}

	function isInEditor() {

		return window.location.href.substring( 0, 4 ) === 'blob';

	}

	/**
   * Creates a webgl context. If creation fails it will
   * change the contents of the container of the <canvas>
   * tag to an error message with the correct links for WebGL.
   * @param {HTMLCanvasElement} canvas. The canvas element to
   *     create a context from.
   * @param {WebGLContextCreationAttributes} opt_attribs Any
   *     creation attributes you want to pass in.
   * @return {WebGLRenderingContext} The created context.
   * @memberOf module:webgl-utils
   */
	function showNeedWebGL( canvas ) {

		const doc = canvas.ownerDocument;
		if ( doc ) {

			const temp = doc.createElement( 'div' );
			temp.innerHTML = `
        <div style="
          position: absolute;
          left: 0;
          top: 0;
          background-color: #DEF;
          width: 100%;
          height: 100%;
          display: flex;
          flex-flow: column;
          justify-content: center;
          align-content: center;
          align-items: center;
        ">
          <div style="text-align: center;">
            It doesn't appear your browser supports WebGL.<br/>
            <a href="http://get.webgl.org" target="_blank">Click here for more information.</a>
          </div>
        </div>
      `;
			const div = temp.querySelector( 'div' );
			doc.body.appendChild( div );

		}

	}

	const origConsole = {};

	function setupConsole() {

		const style = document.createElement( 'style' );
		style.innerText = `
    .console {
      font-family: monospace;
      font-size: medium;
      max-height: 50%;
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      overflow: auto;
      background: rgba(221, 221, 221, 0.9);
    }
    .console .console-line {
      white-space: pre-line;
    }
    .console .log .warn {
      color: black;
    }
    .console .error {
      color: red;
    }
    `;
		const parent = document.createElement( 'div' );
		parent.className = 'console';
		const toggle = document.createElement( 'div' );
		let show = false;
		Object.assign( toggle.style, {
			position: 'absolute',
			right: 0,
			bottom: 0,
			background: '#EEE',
			'font-size': 'smaller',
			cursor: 'pointer',
		} );
		toggle.addEventListener( 'click', showHideConsole );

		function showHideConsole() {

			show = ! show;
			toggle.textContent = show ? '☒' : '☐';
			parent.style.display = show ? '' : 'none';

		}

		showHideConsole();

		const maxLines = 100;
		const lines = [];
		let added = false;

		function addLine( type, str, prefix ) {

			const div = document.createElement( 'div' );
			div.textContent = ( prefix + str ) || ' ';
			div.className = `console-line ${type}`;
			parent.appendChild( div );
			lines.push( div );
			if ( ! added ) {

				added = true;
				document.body.appendChild( style );
				document.body.appendChild( parent );
				document.body.appendChild( toggle );

			}
			// scrollIntoView only works in Chrome
			// In Firefox and Safari scrollIntoView inside an iframe moves
			// that element into the view. It should arguably only move that
			// element inside the iframe itself, otherwise that's giving
			// any random iframe control to bring itself into view against
			// the parent's wishes.
			//
			// note that even if we used a solution (which is to manually set
			// scrollTop) there's a UI issue that if the user manually scrolls
			// we want to stop scrolling automatically and if they move back
			// to the bottom we want to pick up scrolling automatically.
			// Kind of a PITA so TBD
			//
			// div.scrollIntoView();

		}

		function addLines( type, str, prefix ) {

			while ( lines.length > maxLines ) {

				const div = lines.shift();
				div.parentNode.removeChild( div );

			}

			addLine( type, str, prefix );

		}

		const threePukeRE = /WebGLRenderer.*?extension not supported/;
		function wrapFunc( obj, funcName, prefix ) {

			const oldFn = obj[ funcName ];
			origConsole[ funcName ] = oldFn.bind( obj );
			return function ( ...args ) {

				// three.js pukes all over so filter here
				const src = [ ...args ].join( ' ' );
				if ( ! threePukeRE.test( src ) ) {

					addLines( funcName, src, prefix );

				}

				oldFn.apply( obj, arguments );

			};

		}

		window.console.log = wrapFunc( window.console, 'log', '' );
		window.console.warn = wrapFunc( window.console, 'warn', '⚠' );
		window.console.error = wrapFunc( window.console, 'error', '❌' );

	}

	function reportJSError( url, lineNo, colNo, msg ) {

		try {

			const { origUrl, actualLineNo } = window.parent.getActualLineNumberAndMoveTo( url, lineNo, colNo );
			url = origUrl;
			lineNo = actualLineNo;

		} catch ( ex ) {

			origConsole.error( ex );

		}
    console.error(url, "line:", lineNo, ":", msg);  // eslint-disable-line
	}

	/**
   * @typedef {Object} StackInfo
   * @property {string} url Url of line
   * @property {number} lineNo line number of error
   * @property {number} colNo column number of error
   * @property {string} [funcName] name of function
   */

	/**
   * @parameter {string} stack A stack string as in `(new Error()).stack`
   * @returns {StackInfo}
   */
	const parseStack = function () {

		const browser = getBrowser();
		let lineNdx;
		let matcher;
		if ( ( /chrome|opera/i ).test( browser.name ) ) {

			lineNdx = 3;
			matcher = function ( line ) {

				const m = /at ([^(]*?)\(*(.*?):(\d+):(\d+)/.exec( line );
				if ( m ) {

					let userFnName = m[ 1 ];
					let url = m[ 2 ];
					const lineNo = parseInt( m[ 3 ] );
					const colNo = parseInt( m[ 4 ] );
					if ( url === '' ) {

						url = userFnName;
						userFnName = '';

					}

					return {
						url: url,
						lineNo: lineNo,
						colNo: colNo,
						funcName: userFnName,
					};

				}

				return undefined;

			};

		} else if ( ( /firefox|safari/i ).test( browser.name ) ) {

			lineNdx = 2;
			matcher = function ( line ) {

				const m = /@(.*?):(\d+):(\d+)/.exec( line );
				if ( m ) {

					const url = m[ 1 ];
					const lineNo = parseInt( m[ 2 ] );
					const colNo = parseInt( m[ 3 ] );
					return {
						url: url,
						lineNo: lineNo,
						colNo: colNo,
					};

				}

				return undefined;

			};

		}

		return function stackParser( stack ) {

			if ( matcher ) {

				try {

					const lines = stack.split( '\n' );
					// window.fooLines = lines;
					// lines.forEach(function(line, ndx) {
					//   origConsole.log("#", ndx, line);
					// });
					return matcher( lines[ lineNdx ] );

				} catch ( e ) {}

			}

			return undefined;

		};

	}();

	function setupWorkerSupport() {

		function log( data ) {

			const { logType, msg } = data;
			console[ logType ]( '[Worker]', msg );

		}

		function lostContext( /* data */ ) {

			addContextLostHTML();

		}

		function jsError( data ) {

			const { url, lineNo, colNo, msg } = data;
			reportJSError( url, lineNo, colNo, msg );

		}

		function jsErrorWithStack( data ) {

			const { url, stack, msg } = data;
			const errorInfo = parseStack( stack );
			if ( errorInfo ) {

				reportJSError( errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg );

			} else {

        console.error(errorMsg)  // eslint-disable-line
			}

		}

		const handlers = {
			log,
			lostContext,
			jsError,
			jsErrorWithStack,
		};
		const OrigWorker = self.Worker;
		class WrappedWorker extends OrigWorker {

			constructor( url, ...args ) {

				super( url, ...args );
				let listener;
				this.onmessage = function ( e ) {

					if ( ! e || ! e.data || e.data.type !== '___editor___' ) {

						if ( listener ) {

							listener( e );

						}

						return;

					}

					e.stopImmediatePropagation();
					const data = e.data.data;
					const fn = handlers[ data.type ];
					if ( typeof fn !== 'function' ) {

						origConsole.error( 'unknown editor msg:', data.type );

					} else {

						fn( data );

					}

					return;

				};

				Object.defineProperty( this, 'onmessage', {
					get() {

						return listener;

					},
					set( fn ) {

						listener = fn;

					},
				} );

			}

		}
		self.Worker = WrappedWorker;

	}

	function addContextLostHTML() {

		const div = document.createElement( 'div' );
		div.className = 'contextlost';
		div.innerHTML = '<div>Context Lost: Click To Reload</div>';
		div.addEventListener( 'click', function () {

			window.location.reload();

		} );
		document.body.appendChild( div );

	}

	/**
   * Gets a WebGL context.
   * makes its backing store the size it is displayed.
   * @param {HTMLCanvasElement} canvas a canvas element.
   * @memberOf module:webgl-utils
   */
	let setupLesson = function ( canvas ) {

		// only once
		setupLesson = function () {};

		if ( canvas ) {

			canvas.addEventListener( 'webglcontextlost', function () {

				// the default is to do nothing. Preventing the default
				// means allowing context to be restored
				// e.preventDefault();  // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
				addContextLostHTML();

			} );
			/* can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
      canvas.addEventListener('webglcontextrestored', function() {
          // just reload the page. Easiest.
          window.location.reload();
      });
      */

		}

		if ( isInIFrame() ) {

			updateCSSIfInIFrame();

		}

	};

	// Replace requestAnimationFrame and cancelAnimationFrame with one
	// that only executes when the body is visible (we're in an iframe).
	// It's frustrating that th browsers don't do this automatically.
	// It's half of the point of rAF that it shouldn't execute when
	// content is not visible but browsers execute rAF in iframes even
	// if they are not visible.
	if ( topWindow.requestAnimationFrame ) {

		topWindow.requestAnimationFrame = ( function ( oldRAF, oldCancelRAF ) {

			let nextFakeRAFId = 1;
			const fakeRAFIdToCallbackMap = new Map();
			let rafRequestId;
			let isBodyOnScreen;

			function rAFHandler( time ) {

				rafRequestId = undefined;
				const ids = [ ...fakeRAFIdToCallbackMap.keys() ]; // WTF! Map.keys() iterates over live keys!
				for ( const id of ids ) {

					const callback = fakeRAFIdToCallbackMap.get( id );
					fakeRAFIdToCallbackMap.delete( id );
					if ( callback ) {

						callback( time );

					}

				}

			}

			function startRAFIfIntersectingAndNeeded() {

				if ( ! rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0 ) {

					rafRequestId = oldRAF( rAFHandler );

				}

			}

			function stopRAF() {

				if ( rafRequestId ) {

					oldCancelRAF( rafRequestId );
					rafRequestId = undefined;

				}

			}

			function initIntersectionObserver() {

				const intersectionObserver = new IntersectionObserver( ( entries ) => {

					entries.forEach( entry => {

						isBodyOnScreen = entry.isIntersecting;

					} );
					if ( isBodyOnScreen ) {

						startRAFIfIntersectingAndNeeded();

					} else {

						stopRAF();

					}

				} );
				intersectionObserver.observe( document.body );

			}

			function betterRAF( callback ) {

				const fakeRAFId = nextFakeRAFId ++;
				fakeRAFIdToCallbackMap.set( fakeRAFId, callback );
				startRAFIfIntersectingAndNeeded();
				return fakeRAFId;

			}

			function betterCancelRAF( id ) {

				fakeRAFIdToCallbackMap.delete( id );

			}

			topWindow.cancelAnimationFrame = betterCancelRAF;

			return function ( callback ) {

				// we need to lazy init this because this code gets parsed
				// before body exists. We could fix it by moving lesson-helper.js
				// after <body> but that would require changing 100s of examples
				initIntersectionObserver();
				topWindow.requestAnimationFrame = betterRAF;
				return betterRAF( callback );

			};

		}( topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame ) );

	}

	updateCSSIfInIFrame();

	function captureJSErrors() {

		// capture JavaScript Errors
		window.addEventListener( 'error', function ( e ) {

			const msg = e.message || e.error;
			const url = e.filename;
			const lineNo = e.lineno || 1;
			const colNo = e.colno || 1;
			reportJSError( url, lineNo, colNo, msg );
			origConsole.error( e.error );

		} );

	}

	// adapted from http://stackoverflow.com/a/2401861/128511
	function getBrowser() {

		const userAgent = navigator.userAgent;
		let m = userAgent.match( /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i ) || [];
		if ( /trident/i.test( m[ 1 ] ) ) {

			m = /\brv[ :]+(\d+)/g.exec( userAgent ) || [];
			return {
				name: 'IE',
				version: m[ 1 ],
			};

		}

		if ( m[ 1 ] === 'Chrome' ) {

			const temp = userAgent.match( /\b(OPR|Edge)\/(\d+)/ );
			if ( temp ) {

				return {
					name: temp[ 1 ].replace( 'OPR', 'Opera' ),
					version: temp[ 2 ],
				};

			}

		}

		m = m[ 2 ] ? [ m[ 1 ], m[ 2 ] ] : [ navigator.appName, navigator.appVersion, '-?' ];
		const version = userAgent.match( /version\/(\d+)/i );
		if ( version ) {

			m.splice( 1, 1, version[ 1 ] );

		}

		return {
			name: m[ 0 ],
			version: m[ 1 ],
		};

	}

	const canvasesToTimeoutMap = new Map();
	const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
	const isWebGL2RE = /^webgl2$/i;
	function installWebGLLessonSetup() {

		HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {

			return function () {

				const timeoutId = canvasesToTimeoutMap.get( this );
				if ( timeoutId ) {

					clearTimeout( timeoutId );

				}

				const type = arguments[ 0 ];
				const isWebGL1or2 = isWebGLRE.test( type );
				const isWebGL2 = isWebGL2RE.test( type );
				if ( isWebGL1or2 ) {

					setupLesson( this );

				}

				const args = [].slice.apply( arguments );
				args[ 1 ] = {
					powerPreference: 'low-power',
					...args[ 1 ],
				};
				const ctx = oldFn.apply( this, args );
				if ( ! ctx ) {

					if ( isWebGL2 ) {

						// three tries webgl2 then webgl1
						// so wait 1/2 a second before showing the failure
						// message. If we get success on the same canvas
						// we'll cancel this.
						canvasesToTimeoutMap.set( this, setTimeout( () => {

							canvasesToTimeoutMap.delete( this );
							showNeedWebGL( this );

						}, 500 ) );

					} else {

						showNeedWebGL( this );

					}

				}

				return ctx;

			};

		}( HTMLCanvasElement.prototype.getContext ) );

	}

	function installWebGLDebugContextCreator() {

		if ( ! self.webglDebugHelper ) {

			return;

		}

		const {
			makeDebugContext,
			glFunctionArgToString,
			glEnumToString,
		} = self.webglDebugHelper;

		// capture GL errors
		HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {

			return function () {

				let ctx = oldFn.apply( this, arguments );
				// Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
				// but that might fail if wrapped by debugging extension
				if ( ctx && ctx.bindTexture ) {

					ctx = makeDebugContext( ctx, {
						maxDrawCalls: 100,
						errorFunc: function ( err, funcName, args ) {

							const numArgs = args.length;
							const enumedArgs = [].map.call( args, function ( arg, ndx ) {

								let str = glFunctionArgToString( funcName, numArgs, ndx, arg );
								// shorten because of long arrays
								if ( str.length > 200 ) {

									str = str.substring( 0, 200 ) + '...';

								}

								return str;

							} );
							const errorMsg = `WebGL error ${glEnumToString( err )} in ${funcName}(${enumedArgs.join( ', ' )})`;
							const errorInfo = parseStack( ( new Error() ).stack );
							if ( errorInfo ) {

								reportJSError( errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg );

							} else {

                console.error(errorMsg)  // eslint-disable-line
							}

						},
					} );

				}

				return ctx;

			};

		}( HTMLCanvasElement.prototype.getContext ) );

	}

	installWebGLLessonSetup();

	if ( isInEditor() ) {

		setupWorkerSupport();
		setupConsole();
		captureJSErrors();
		if ( lessonSettings.glDebug !== false ) {

			installWebGLDebugContextCreator();

		}

	}

	return {
		setupLesson: setupLesson,
		showNeedWebGL: showNeedWebGL,
	};

} ) );

