/*
 * 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, globalThis */

(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.webglDebugHelper = factory.call( root );

	}

}( this || globalThis, function () {

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

	//------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ]

	/*
  ** Copyright (c) 2012 The Khronos Group Inc.
  **
  ** Permission is hereby granted, free of charge, to any person obtaining a
  ** copy of this software and/or associated documentation files (the
  ** "Materials"), to deal in the Materials without restriction, including
  ** without limitation the rights to use, copy, modify, merge, publish,
  ** distribute, sublicense, and/or sell copies of the Materials, and to
  ** permit persons to whom the Materials are furnished to do so, subject to
  ** the following conditions:
  **
  ** The above copyright notice and this permission notice shall be included
  ** in all copies or substantial portions of the Materials.
  **
  ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
  */

	/**
   * Types of contexts we have added to map
   */
	const mappedContextTypes = {};

	/**
   * Map of numbers to names.
   * @type {Object}
   */
	const glEnums = {};

	/**
   * Map of names to numbers.
   * @type {Object}
   */
	const enumStringToValue = {};

	/**
   * Initializes this module. Safe to call more than once.
   * @param {!WebGLRenderingContext} ctx A WebGL context. If
   *    you have more than one context it doesn't matter which one
   *    you pass in, it is only used to pull out constants.
   */
	function addEnumsForContext( ctx, type ) {

		if ( ! mappedContextTypes[ type ] ) {

			mappedContextTypes[ type ] = true;
			for ( const propertyName in ctx ) {

				if ( typeof ctx[ propertyName ] === 'number' ) {

					glEnums[ ctx[ propertyName ] ] = propertyName;
					enumStringToValue[ propertyName ] = ctx[ propertyName ];

				}

			}

		}

	}

	function enumArrayToString( enums ) {

		const enumStrings = [];
		if ( enums.length ) {

			for ( let i = 0; i < enums.length; ++ i ) {

        enums.push(glEnumToString(enums[i]));  // eslint-disable-line
			}

			return '[' + enumStrings.join( ', ' ) + ']';

		}

		return enumStrings.toString();

	}

	function makeBitFieldToStringFunc( enums ) {

		return function ( value ) {

			let orResult = 0;
			const orEnums = [];
			for ( let i = 0; i < enums.length; ++ i ) {

				const enumValue = enumStringToValue[ enums[ i ] ];
				if ( ( value & enumValue ) !== 0 ) {

					orResult |= enumValue;
          orEnums.push(glEnumToString(enumValue));  // eslint-disable-line
				}

			}

			if ( orResult === value ) {

				return orEnums.join( ' | ' );

			} else {

        return glEnumToString(value);  // eslint-disable-line
			}

		};

	}

	const destBufferBitFieldToString = makeBitFieldToStringFunc( [
		'COLOR_BUFFER_BIT',
		'DEPTH_BUFFER_BIT',
		'STENCIL_BUFFER_BIT',
	] );

	/**
   * Which arguments are enums based on the number of arguments to the function.
   * So
   *    'texImage2D': {
   *       9: { 0:true, 2:true, 6:true, 7:true },
   *       6: { 0:true, 2:true, 3:true, 4:true },
   *    },
   *
   * means if there are 9 arguments then 6 and 7 are enums, if there are 6
   * arguments 3 and 4 are enums. Maybe a function as well in which case
   * value is passed to function and returns a string
   *
   * @type {!Object.<number, (!Object.<number, string>|function)}
   */
	const glValidEnumContexts = {
		// Generic setters and getters

		'enable': { 1: { 0: true } },
		'disable': { 1: { 0: true } },
		'getParameter': { 1: { 0: true } },

		// Rendering

		'drawArrays': { 3: { 0: true } },
		'drawElements': { 4: { 0: true, 2: true } },
		'drawArraysInstanced': { 4: { 0: true } },
		'drawElementsInstanced': { 5: { 0: true, 2: true } },
		'drawRangeElements': { 6: { 0: true, 4: true } },

		// Shaders

		'createShader': { 1: { 0: true } },
		'getShaderParameter': { 2: { 1: true } },
		'getProgramParameter': { 2: { 1: true } },
		'getShaderPrecisionFormat': { 2: { 0: true, 1: true } },

		// Vertex attributes

		'getVertexAttrib': { 2: { 1: true } },
		'vertexAttribPointer': { 6: { 2: true } },
		'vertexAttribIPointer': { 5: { 2: true } }, // WebGL2

		// Textures

		'bindTexture': { 2: { 0: true } },
		'activeTexture': { 1: { 0: true } },
		'getTexParameter': { 2: { 0: true, 1: true } },
		'texParameterf': { 3: { 0: true, 1: true } },
		'texParameteri': { 3: { 0: true, 1: true, 2: true } },
		'texImage2D': {
			9: { 0: true, 2: true, 6: true, 7: true },
			6: { 0: true, 2: true, 3: true, 4: true },
			10: { 0: true, 2: true, 6: true, 7: true }, // WebGL2
		},
		'texImage3D': {
			10: { 0: true, 2: true, 7: true, 8: true }, // WebGL2
			11: { 0: true, 2: true, 7: true, 8: true }, // WebGL2
		},
		'texSubImage2D': {
			9: { 0: true, 6: true, 7: true },
			7: { 0: true, 4: true, 5: true },
			10: { 0: true, 6: true, 7: true }, // WebGL2
		},
		'texSubImage3D': {
			11: { 0: true, 8: true, 9: true }, // WebGL2
			12: { 0: true, 8: true, 9: true }, // WebGL2
		},
		'texStorage2D': { 5: { 0: true, 2: true } }, // WebGL2
		'texStorage3D': { 6: { 0: true, 2: true } }, // WebGL2
		'copyTexImage2D': { 8: { 0: true, 2: true } },
		'copyTexSubImage2D': { 8: { 0: true } },
		'copyTexSubImage3D': { 9: { 0: true } }, // WebGL2
		'generateMipmap': { 1: { 0: true } },
		'compressedTexImage2D': {
			7: { 0: true, 2: true },
			8: { 0: true, 2: true }, // WebGL2
		},
		'compressedTexSubImage2D': {
			8: { 0: true, 6: true },
			9: { 0: true, 6: true }, // WebGL2
		},
		'compressedTexImage3D': {
			8: { 0: true, 2: true, }, // WebGL2
			9: { 0: true, 2: true, }, // WebGL2
		},
		'compressedTexSubImage3D': {
			9: { 0: true, 8: true, }, // WebGL2
			10: { 0: true, 8: true, }, // WebGL2
		},

		// Buffer objects

		'bindBuffer': { 2: { 0: true } },
		'bufferData': {
			3: { 0: true, 2: true },
			4: { 0: true, 2: true }, // WebGL2
			5: { 0: true, 2: true }, // WebGL2
		},
		'bufferSubData': {
			3: { 0: true },
			4: { 0: true }, // WebGL2
			5: { 0: true }, // WebGL2
		},
		'copyBufferSubData': {
			5: { 0: true }, // WeBGL2
		},
		'getBufferParameter': { 2: { 0: true, 1: true } },
		'getBufferSubData': {
			3: { 0: true, }, // WebGL2
			4: { 0: true, }, // WebGL2
			5: { 0: true, }, // WebGL2
		},

		// Renderbuffers and framebuffers

		'pixelStorei': { 2: { 0: true, 1: true } },
		'readPixels': {
			7: { 4: true, 5: true },
			8: { 4: true, 5: true }, // WebGL2
		},
		'bindRenderbuffer': { 2: { 0: true } },
		'bindFramebuffer': { 2: { 0: true } },
		'blitFramebuffer': { 10: { 8: destBufferBitFieldToString, 9: true } }, // WebGL2
		'checkFramebufferStatus': { 1: { 0: true } },
		'framebufferRenderbuffer': { 4: { 0: true, 1: true, 2: true } },
		'framebufferTexture2D': { 5: { 0: true, 1: true, 2: true } },
		'framebufferTextureLayer': { 5: { 0: true, 1: true } }, // WebGL2
		'getFramebufferAttachmentParameter': { 3: { 0: true, 1: true, 2: true } },
		'getInternalformatParameter': { 3: { 0: true, 1: true, 2: true } }, // WebGL2
		'getRenderbufferParameter': { 2: { 0: true, 1: true } },
		'invalidateFramebuffer': { 2: { 0: true, 1: enumArrayToString, } }, // WebGL2
		'invalidateSubFramebuffer': { 6: { 0: true, 1: enumArrayToString, } }, // WebGL2
		'readBuffer': { 1: { 0: true } }, // WebGL2
		'renderbufferStorage': { 4: { 0: true, 1: true } },
		'renderbufferStorageMultisample': { 5: { 0: true, 2: true } }, // WebGL2

		// Frame buffer operations (clear, blend, depth test, stencil)

		'clear': { 1: { 0: destBufferBitFieldToString } },
		'depthFunc': { 1: { 0: true } },
		'blendFunc': { 2: { 0: true, 1: true } },
		'blendFuncSeparate': { 4: { 0: true, 1: true, 2: true, 3: true } },
		'blendEquation': { 1: { 0: true } },
		'blendEquationSeparate': { 2: { 0: true, 1: true } },
		'stencilFunc': { 3: { 0: true } },
		'stencilFuncSeparate': { 4: { 0: true, 1: true } },
		'stencilMaskSeparate': { 2: { 0: true } },
		'stencilOp': { 3: { 0: true, 1: true, 2: true } },
		'stencilOpSeparate': { 4: { 0: true, 1: true, 2: true, 3: true } },

		// Culling

		'cullFace': { 1: { 0: true } },
		'frontFace': { 1: { 0: true } },

		// ANGLE_instanced_arrays extension

		'drawArraysInstancedANGLE': { 4: { 0: true } },
		'drawElementsInstancedANGLE': { 5: { 0: true, 2: true } },

		// EXT_blend_minmax extension

		'blendEquationEXT': { 1: { 0: true } },

		// Multiple Render Targets

		'drawBuffersWebGL': { 1: { 0: enumArrayToString, } }, // WEBGL_draw_bufers
		'drawBuffers': { 1: { 0: enumArrayToString, } }, // WebGL2
		'clearBufferfv': {
			4: { 0: true }, // WebGL2
			5: { 0: true }, // WebGL2
		},
		'clearBufferiv': {
			4: { 0: true }, // WebGL2
			5: { 0: true }, // WebGL2
		},
		'clearBufferuiv': {
			4: { 0: true }, // WebGL2
			5: { 0: true }, // WebGL2
		},
		'clearBufferfi': { 4: { 0: true } }, // WebGL2

		// QueryObjects

		'beginQuery': { 2: { 0: true } }, // WebGL2
		'endQuery': { 1: { 0: true } }, // WebGL2
		'getQuery': { 2: { 0: true, 1: true } }, // WebGL2
		'getQueryParameter': { 2: { 1: true } }, // WebGL2

		//  Sampler Objects

		'samplerParameteri': { 3: { 1: true } }, // WebGL2
		'samplerParameterf': { 3: { 1: true } }, // WebGL2
		'getSamplerParameter': { 2: { 1: true } }, // WebGL2

		//  Sync objects

		'clientWaitSync': { 3: { 1: makeBitFieldToStringFunc( [ 'SYNC_FLUSH_COMMANDS_BIT' ] ) } }, // WebGL2
		'fenceSync': { 2: { 0: true } }, // WebGL2
		'getSyncParameter': { 2: { 1: true } }, // WebGL2

		//  Transform Feedback

		'bindTransformFeedback': { 2: { 0: true } }, // WebGL2
		'beginTransformFeedback': { 1: { 0: true } }, // WebGL2

		// Uniform Buffer Objects and Transform Feedback Buffers
		'bindBufferBase': { 3: { 0: true } }, // WebGL2
		'bindBufferRange': { 5: { 0: true } }, // WebGL2
		'getIndexedParameter': { 2: { 0: true } }, // WebGL2
		'getActiveUniforms': { 3: { 2: true } }, // WebGL2
		'getActiveUniformBlockParameter': { 3: { 2: true } }, // WebGL2
	};

	/**
   * Gets an string version of an WebGL enum.
   *
   * Example:
   *   var str = WebGLDebugUtil.glEnumToString(ctx.getError());
   *
   * @param {number} value Value to return an enum for
   * @return {string} The string version of the enum.
   */
	function glEnumToString( value ) {

		const name = glEnums[ value ];
		return ( name !== undefined )
			? `gl.${name}`
			: `/*UNKNOWN WebGL ENUM*/ 0x${value.toString( 16 )}`;

	}

	/**
   * Returns the string version of a WebGL argument.
   * Attempts to convert enum arguments to strings.
   * @param {string} functionName the name of the WebGL function.
   * @param {number} numArgs the number of arguments passed to the function.
   * @param {number} argumentIndx the index of the argument.
   * @param {*} value The value of the argument.
   * @return {string} The value as a string.
   */
	function glFunctionArgToString( functionName, numArgs, argumentIndex, value ) {

		const funcInfos = glValidEnumContexts[ functionName ];
		if ( funcInfos !== undefined ) {

			const funcInfo = funcInfos[ numArgs ];
			if ( funcInfo !== undefined ) {

				const argType = funcInfo[ argumentIndex ];
				if ( argType ) {

					if ( typeof argType === 'function' ) {

						return argType( value );

					} else {

						return glEnumToString( value );

					}

				}

			}

		}

		if ( value === null ) {

			return 'null';

		} else if ( value === undefined ) {

			return 'undefined';

		} else {

			return value.toString();

		}

	}

	/**
   * Converts the arguments of a WebGL function to a string.
   * Attempts to convert enum arguments to strings.
   *
   * @param {string} functionName the name of the WebGL function.
   * @param {number} args The arguments.
   * @return {string} The arguments as a string.
   */
	function glFunctionArgsToString( functionName, args ) {

		// apparently we can't do args.join(",");
		const argStrs = [];
		const numArgs = args.length;
		for ( let ii = 0; ii < numArgs; ++ ii ) {

			argStrs.push( glFunctionArgToString( functionName, numArgs, ii, args[ ii ] ) );

		}

		return argStrs.join( ', ' );

	}

	function makePropertyWrapper( wrapper, original, propertyName ) {

    wrapper.__defineGetter__(propertyName, function() {  // eslint-disable-line
			return original[ propertyName ];

		} );
		// TODO(gmane): this needs to handle properties that take more than
		// one value?
    wrapper.__defineSetter__(propertyName, function(value) {  // eslint-disable-line
			original[ propertyName ] = value;

		} );

	}

	/**
   * Given a WebGL context returns a wrapped context that calls
   * gl.getError after every command and calls a function if the
   * result is not gl.NO_ERROR.
   *
   * @param {!WebGLRenderingContext} ctx The webgl context to
   *        wrap.
   * @param {!function(err, funcName, args): void} opt_onErrorFunc
   *        The function to call when gl.getError returns an
   *        error. If not specified the default function calls
   *        console.log with a message.
   * @param {!function(funcName, args): void} opt_onFunc The
   *        function to call when each webgl function is called.
   *        You can use this to log all calls for example.
   * @param {!WebGLRenderingContext} opt_err_ctx The webgl context
   *        to call getError on if different than ctx.
   */
	function makeDebugContext( ctx, options ) {

		options = options || {};
		const errCtx = options.errCtx || ctx;
		const onFunc = options.funcFunc;
		const sharedState = options.sharedState || {
			numDrawCallsRemaining: options.maxDrawCalls || - 1,
			wrappers: {},
		};
		options.sharedState = sharedState;

		const errorFunc = options.errorFunc || function ( err, functionName, args ) {

			console.error( `WebGL error ${glEnumToString( err )} in ${functionName}(${glFunctionArgsToString( functionName, args )})` );

		};

		// Holds booleans for each GL error so after we get the error ourselves
		// we can still return it to the client app.
		const glErrorShadow = { };
		const wrapper = {};

		function removeChecks() {

			Object.keys( sharedState.wrappers ).forEach( function ( name ) {

				const pair = sharedState.wrappers[ name ];
				const wrapper = pair.wrapper;
				const orig = pair.orig;
				for ( const propertyName in wrapper ) {

					if ( typeof wrapper[ propertyName ] === 'function' ) {

						wrapper[ propertyName ] = orig[ propertyName ].bind( orig );

					}

				}

			} );

		}

		function checkMaxDrawCalls() {

			if ( sharedState.numDrawCallsRemaining === 0 ) {

				removeChecks();

			}

			-- sharedState.numDrawCallsRemaining;

		}

		function noop() {
		}

		// Makes a function that calls a WebGL function and then calls getError.
		function makeErrorWrapper( ctx, functionName ) {

			const check = functionName.substring( 0, 4 ) === 'draw' ? checkMaxDrawCalls : noop;
			return function () {

				if ( onFunc ) {

					onFunc( functionName, arguments );

				}

				const result = ctx[ functionName ].apply( ctx, arguments );
				const err = errCtx.getError();
				if ( err !== 0 ) {

					glErrorShadow[ err ] = true;
					errorFunc( err, functionName, arguments );

				}

				check();
				return result;

			};

		}

		function makeGetExtensionWrapper( ctx, wrapped ) {

			return function () {

				const extensionName = arguments[ 0 ];
				let ext = sharedState.wrappers[ extensionName ];
				if ( ! ext ) {

					ext = wrapped.apply( ctx, arguments );
					if ( ext ) {

						const origExt = ext;
						ext = makeDebugContext( ext, { ...options, errCtx: ctx } );
						sharedState.wrappers[ extensionName ] = { wrapper: ext, orig: origExt };
						addEnumsForContext( origExt, extensionName );

					}

				}

				return ext;

			};

		}

		// Make an object that has a copy of every property of the WebGL context
		// but wraps all functions.
		for ( const propertyName in ctx ) {

			if ( typeof ctx[ propertyName ] === 'function' ) {

				if ( propertyName !== 'getExtension' ) {

					wrapper[ propertyName ] = makeErrorWrapper( ctx, propertyName );

				} else {

					const wrapped = makeErrorWrapper( ctx, propertyName );
					wrapper[ propertyName ] = makeGetExtensionWrapper( ctx, wrapped );

				}

			} else {

				makePropertyWrapper( wrapper, ctx, propertyName );

			}

		}

		// Override the getError function with one that returns our saved results.
		if ( wrapper.getError ) {

			wrapper.getError = function () {

				for ( const err of Object.keys( glErrorShadow ) ) {

					if ( glErrorShadow[ err ] ) {

						glErrorShadow[ err ] = false;
						return err;

					}

				}

				return ctx.NO_ERROR;

			};

		}

		if ( wrapper.bindBuffer ) {

			sharedState.wrappers[ 'webgl' ] = { wrapper: wrapper, orig: ctx };
			addEnumsForContext( ctx, ctx.bindBufferBase ? 'WebGL2' : 'WebGL' );

		}

		return wrapper;

	}

	return {
		makeDebugContext,
		glFunctionArgsToString,
		glFunctionArgToString,
		glEnumToString,
	};

} ) );

