import { CoordinateSystem, Point, Rectangle } from '@awayjs/core';
import { BitmapImage2D } from '../image/BitmapImage2D';
import { ContextGLBlendEquation, ContextGLBlendFactor } from '../base/ContextGLBlendFactor';
import { ContextGLDrawMode } from '../base/ContextGLDrawMode';
import { ContextGLClearMask } from '../base/ContextGLClearMask';
import { ContextGLCompareMode } from '../base/ContextGLCompareMode';
import { ContextGLMipFilter } from '../base/ContextGLMipFilter';
import { ContextGLProgramType } from '../base/ContextGLProgramType';
import { ContextGLStencilAction } from '../base/ContextGLStencilAction';
import { ContextGLTextureFilter } from '../base/ContextGLTextureFilter';
import { ContextGLTriangleFace } from '../base/ContextGLTriangleFace';
import { ContextGLTextureFormat } from '../base/ContextGLTextureFormat';
import { ContextGLWrapMode } from '../base/ContextGLWrapMode';
import { IContextGL } from '../base/IContextGL';
import { CubeTextureWebGL } from './CubeTextureWebGL';
import { IndexBufferWebGL } from './IndexBufferWebGL';
import { ProgramWebGL } from './ProgramWebGL';
import { TextureBaseWebGL } from './TextureBaseWebGL';
import { TextureWebGL } from './TextureWebGL';
import { VertexBufferWebGL } from './VertexBufferWebGL';
import { TextureContextWebGL } from './TextureContextWebGL';
import { VaoContextWebGL, VaoWebGL } from './VaoWebGL';
import { State } from './State';
import { Settings } from '../Settings';
import { FenceContextWebGL } from './FenceContextWebGL';
import * as GL_MAP from './ConstantsWebGL';
import { StatsWebGL } from './StatsWebGL';

let _DEBUG_renderMode: '' | 'line' = '';

//@ts-ignore
window._AWAY_DEBUG_ = Object.assign(window._AWAY_DEBUG_ || {}, {
	forceLineMode(lineMode = false) {
		_DEBUG_renderMode = lineMode ? 'line' : '';
	}
});

export class ContextWebGL implements IContextGL {
	public readonly stats = new StatsWebGL()

	private _container: HTMLCanvasElement;
	private _width: number;
	private _height: number;
	private _drawing: boolean = true;

	private _standardDerivatives: boolean;

	// [x, y, w, h]
	private _viewportState: State<number> = new State(0,0,0,0);
	// [enable, func, funcA, src, dst, srcA, dstA];
	private _blendState: State<number> = new State(
		0,GL_MAP.BLEND_EQ[ContextGLBlendEquation.ADD], GL_MAP.BLEND_EQ[ContextGLBlendEquation.ADD], 0,0, -1, -1);

	// [r, g, b, a]
	private _colorMapState: State<boolean> = new State(true, true, true, true);
	// [r, g, b, a]
	private _clearColorState: State<number> = new State(0,0,0,0);
	// [stencil, depth], i don't know default a depth value, set -1, to force change it
	private _clearSD: State<number> = new State(0,-1);

	// [enable, mask, func]
	private _depthState: State<number> = new State(0, 0, 0);
	// [enable, mask, func]
	private _stencilState: State<number> = new State(0, 1, 0);
	// [enable, face]
	private _cullState: State<number> = new State(0,0,0);

	//@protected
	public _gl: WebGLRenderingContext | WebGL2RenderingContext;

	//@protected
	public _currentProgram: ProgramWebGL;

	/* internal */ _texContext: TextureContextWebGL;
	/* internal */ _vaoContext: VaoContextWebGL;
	/* internal */ _fenceContext: FenceContextWebGL;

	private _currentArrayBuffer: VertexBufferWebGL;
	private _stencilCompareMode: number;
	private _stencilCompareModeBack: number;
	private _stencilCompareModeFront: number;
	private _stencilReferenceValue: number = 0;
	private _stencilReadMask: number = 0xff;
	private _separateStencil: boolean = false;
	private _lastBoundedIndexBuffer: IndexBufferWebGL = null;

	private _angleInstanced: ANGLE_instanced_arrays;

	public stateChangeCallback: (name: string) => void;

	public hasInstancing() {
		return (this._gl instanceof self.WebGL2RenderingContext) || !!this._angleInstanced;
	}

	public get hasFence() {
		return !!this._fenceContext;
	}

	private _hasVao = false;
	public get hasVao() {
		return this._hasVao;
	}

	private _glVersion: number;
	public get glVersion(): number {
		return this._glVersion;
	}

	private _pixelRatio: number;
	public get pixelRatio(): number {
		return this._pixelRatio;
	}

	public set pixelRatio(v: number) {
		this._pixelRatio = v;
	}

	public get container(): HTMLCanvasElement {
		return this._container;
	}

	public set container(value: HTMLCanvasElement) {
		this._container = value;
		this.initWebGL();
	}

	public get standardDerivatives(): boolean {
		return this._standardDerivatives;
	}

	constructor(canvas: HTMLCanvasElement, alpha: boolean = false) {
		this._container = canvas;
		this.initWebGL(alpha);
	}

	public assertLost(when: string) {
		if (!Settings.PROFILE_CONTEXT_LOST) {
			return;
		}

		if (this._gl.isContextLost()) {
			throw `[ContextWebGL] Context lost after ${when}, state: ${this.stats.toString()}`;
		}
	}

	private initWebGL(alpha: boolean = false) {

		const props: WebGLContextAttributes = {
			alpha: alpha,
			antialias: Settings.ENABLE_ANTIALIAS,
			stencil: true
		};

		try {
			if (Settings.PREF_WEBGL_VERSION === 2) {
				this._gl = <WebGLRenderingContext> this._container.getContext('webgl2', props);
				this._glVersion = 2;
			}

			if (!this._gl) {
				this._gl = <WebGLRenderingContext> (
						this._container.getContext('webgl', props)
						|| this._container.getContext('experimental-webgl', props));

				this._glVersion = 1;

				if (Settings.PREF_WEBGL_VERSION === 2) {
					console.warn('[CONTEXT] Preferred WebGL2 not supported on you device. WebGL1 will used!');
				}
			}

			console.debug(`[CONTEXT] Preferred WebGL: ${Settings.PREF_WEBGL_VERSION}, used: ${this._glVersion}`);

		} catch (e) {
			//
		}

		if (this._gl) {
			const gl = this._gl;

			this._standardDerivatives = this._glVersion === 2 || !!gl.getExtension('OES_standard_derivatives');
			this._stencilCompareMode = gl.ALWAYS;
			this._stencilCompareModeBack = gl.ALWAYS;
			this._stencilCompareModeFront = gl.ALWAYS;

			this._pixelRatio = self.devicePixelRatio || 1;
		} else {
			alert('WebGL is not available.');
		}

		this._texContext = new TextureContextWebGL(this);

		if (Settings.ENABLE_VAO) {
			if (VaoContextWebGL.isSupported(this._gl)) {
				this._vaoContext = new VaoContextWebGL(this);
				this._hasVao = true;
			} else {
				console.warn('[ContextWebGL] VAO isn\'t supported');
			}
		} else {
			console.debug('[ContextWebGL] Vao disabled by settings \'ENABLE_VAO\'');
		}

		if (Settings.ENABLE_ASYNC_READ) {
			if (FenceContextWebGL.isSupported(this._gl)) {
				this._fenceContext = new FenceContextWebGL(this);
			} else {
				console.warn('[ContextWebGL] FenceSync isn\'t supported');
			}
		} else {
			console.debug('[ContextWebGL] FenceSync disabled by settings \'ENABLE_ASYNC_READ\'');
		}

		if (this._glVersion === 1) {
			this._angleInstanced = this._gl.getExtension('ANGLE_instanced_arrays');
		}

		// first locked state 0,0,0,0
		this._blendState.lock(true);

		//@ts-ignore
		window._AWAY_CONTEXT_STATS_ = this.stats;
	}

	public gl(): WebGLRenderingContext {
		return this._gl;
	}

	public clear(
		red: number = 0, green: number = 0, blue: number = 0, alpha: number = 1,
		depth: number = 1, stencil: number = 0, mask: ContextGLClearMask = ContextGLClearMask.ALL): void {

		this.stateChangeCallback && this.stateChangeCallback('clear');

		if (!this._drawing) {
			this.updateBlendStatus();
			this._drawing = true;
		}

		let glmask: number = 0;
		if (mask & ContextGLClearMask.COLOR) {
			if (this._clearColorState.set(red, green, blue, alpha)) {
				this._gl.clearColor(red, green, blue, alpha);
			}

			glmask |= this._gl.COLOR_BUFFER_BIT;
		}

		if (mask & ContextGLClearMask.STENCIL) {
			glmask |= this._gl.STENCIL_BUFFER_BIT;
			this._clearSD.setAt(0, stencil) && this._gl.clearStencil(stencil);
		}

		if (mask & ContextGLClearMask.DEPTH) {
			glmask |= this._gl.DEPTH_BUFFER_BIT;
			this._clearSD.setAt(0, depth) && this._gl.clearDepth(depth);
		}

		this._gl.clear(glmask);
	}

	public configureBackBuffer(
		width: number, height: number, antiAlias: number, enableDepthAndStencil: boolean = true): void {

		this.stateChangeCallback && this.stateChangeCallback('setBackBuffer');

		this._width = width * this._pixelRatio;
		this._height = height * this._pixelRatio;

		if (enableDepthAndStencil) {
			this.enableStencil();
			this.enableDepth();
		} else {
			this.disableStencil();
			this.disableDepth();
		}

		if (this._viewportState.set(0,0,this._width, this._height)) {
			this._gl.viewport(0, 0, this._width, this._height);
		}
	}

	public createCubeTexture(
		size: number, _format: ContextGLTextureFormat,
		_optimizeForRenderToTexture: boolean, _streamingLevels: number = 0): CubeTextureWebGL {

		return new CubeTextureWebGL(this, size);
	}

	public createIndexBuffer(numIndices: number): IndexBufferWebGL {
		return IndexBufferWebGL.create(this, numIndices);
	}

	public createProgram(): ProgramWebGL {
		return new ProgramWebGL(this);
	}

	public createTexture(
		width: number, height: number, _format: ContextGLTextureFormat,
		_optimizeForRenderToTexture: boolean, _streamingLevels: number = 0): TextureWebGL {

		//TODO streaming -- not supported on WebGL
		return TextureWebGL.create(this, width, height);
	}

	public createVertexBuffer(numVertices: number, dataPerVertex: number): VertexBufferWebGL {
		return VertexBufferWebGL.create(this, numVertices, dataPerVertex);
	}

	public createVao(): VaoWebGL {
		if (!this._hasVao) throw 'VAO isn\'n supported';

		return new VaoWebGL(this);
	}

	public dispose(): void {
		//
	}

	public drawToBitmapImage2D(
		destination: BitmapImage2D, invalidate = true, async: boolean = false): undefined | Promise<boolean> {

		this.stateChangeCallback && this.stateChangeCallback('drawToBitmap');

		const pixels = new Uint8Array(destination.getDataInternal(true, true).buffer);
		const rt = this._texContext._currentRT;
		const fence = this._fenceContext;
		const { width, height } = destination;

		let promise: Promise<boolean>;

		if (async && !fence) {
			promise = Promise.resolve(false);
		}

		const currentRT = this._texContext._currentRT;

		if (rt.isMsaaTarget) {
			// because RT is MSAA, we should blit it to no-MSAA and use noMSAA framebufer for reading
			rt.present();
			this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, rt.readBuffer);
		}

		if (async && fence) {
			// tasking a PBO read async
			// http://www.songho.ca/opengl/gl_pbo.html
			promise = fence
				.readPixels(0,0, width, height)
				.then((pbo) => {
					pbo.read(pixels);

					if (invalidate) {
						destination.invalidateGPU();
					}

					// restore PBO to fence pool
					// but we can destroy it too
					fence.release(pbo);

					return true;
				});

		} else {

			// we MUST unbound all bounded PIXEL_PACK buffer to avoid warnings
			if (fence) {
				fence.unboundAll();
			}

			// no support or not required as async, use sync operation
			this._gl.readPixels(
				0, 0, width, height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixels);

			if (invalidate) {
				destination.invalidateGPU();
			}
		}

		// restore back buffer bounding to draw framebufer, but only for sync opps
		// if (rt.isMsaaTarget && !async) {
		this._texContext.bindRenderTarget(currentRT, false);
		// }
		// for any case return a promise, nut for sync it will be undef;
		return promise;
	}

	private _instancedEnabled: boolean = false;
	private _instancesCount: number = 1;

	/**
	 * Begin instanced rendering, can't be disabled until drawArrays or drawElements will called
	 * @param count Count of instances that will be rendered
	 */
	public beginInstancedRender(count: number = 1) {
		if (!this.hasInstancing()) {
			throw 'Instanced rendering not supported';
		}

		this._instancedEnabled = true;
		this._instancesCount = count;
	}

	public drawIndices(
		mode: ContextGLDrawMode, indexBuffer: IndexBufferWebGL, firstIndex: number = 0, numIndices: number = -1): void {

		this.stateChangeCallback && this.stateChangeCallback('drawIndices');
		const gl = this._gl;

		if (!this._drawing)
			throw 'Need to clear before drawing if the buffer has not been cleared since the last present() call.';

		// updata blend before draw, because blend state can mutated a more times
		// reduce a state changes
		this.updateBlendStatus();

		// so, if we delete the buffer, and then try to compare last bounded buffer - it will valid, because
		// because reverence is same
		// reset buffer when it not a buffer
		if (this._lastBoundedIndexBuffer && !this._lastBoundedIndexBuffer.glBuffer) {
			this._lastBoundedIndexBuffer = null;
		}

		// check, that VAO not bounded
		// VAO store index buffer inself
		if (!this._hasVao || !this._vaoContext._lastBoundedVao) {

			if (this._lastBoundedIndexBuffer !== indexBuffer) {
				gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.glBuffer);
			}

			this._lastBoundedIndexBuffer = indexBuffer;
		}

		mode = _DEBUG_renderMode === 'line' ? ContextGLDrawMode.LINES : mode;

		const glMode = GL_MAP.DRAW_MODE[mode];
		const count = (numIndices == -1) ? indexBuffer.numIndices : numIndices;
		const type = gl.UNSIGNED_SHORT;
		const offset = firstIndex * 2;
		const instances = this._instancesCount;

		if (!this._instancedEnabled) {
			gl.drawElements(glMode,count,type,offset);
		} else if (this._angleInstanced) {
			this._angleInstanced.drawElementsInstancedANGLE(
				glMode,count,type,offset, instances
			);
		} else {
			(<WebGL2RenderingContext> gl).drawElementsInstanced(
				glMode,count,type,offset, instances
			);
		}

		this._instancedEnabled = false;
		this._instancesCount = 1;
		this.assertLost('drawElements');
	}

	public bindIndexBuffer(indexBuffer: IndexBufferWebGL) {
		this.stateChangeCallback && this.stateChangeCallback('bindIndexBuffer');

		this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER, indexBuffer.glBuffer);
		this._lastBoundedIndexBuffer = indexBuffer;
	}

	public drawVertices(mode: ContextGLDrawMode, firstVertex: number = 0, numVertices: number = -1): void {
		if (!this._drawing)
			throw 'Need to clear before drawing if the buffer has not been cleared since the last present() call.';

		this.stateChangeCallback && this.stateChangeCallback('drawVertices');

		if (numVertices == 0)
			return;

		// updata blend before draw, because blend state can mutated a more times
		// reduce a state changes
		this.updateBlendStatus();

		mode = _DEBUG_renderMode === 'line' ? ContextGLDrawMode.LINES : mode;

		const glMode = GL_MAP.DRAW_MODE[mode];
		const instances = this._instancesCount;

		if (!this._instancedEnabled) {
			this._gl.drawArrays(glMode, firstVertex, numVertices);
		} else if (this._angleInstanced) {
			this._angleInstanced.drawArraysInstancedANGLE(
				glMode, firstVertex, numVertices, instances
			);
		} else {
			(<WebGL2RenderingContext> this._gl).drawArraysInstanced(
				glMode, firstVertex, numVertices, instances
			);
		}

		this._instancedEnabled = false;
		this._instancesCount = 1;

		this.assertLost('drawArrays');
	}

	public present(): void {
		this.stateChangeCallback && this.stateChangeCallback('present');
		//this._drawing = false;

		this._fenceContext && this._fenceContext.tick();
	}

	public setBlendState(enable: boolean) {
		this.stateChangeCallback && this.stateChangeCallback('setBlendState');
		this._blendState.setAt(0, +enable);
	}

	public setBlendEquation (
		equationRGB: ContextGLBlendEquation,
		equationA: ContextGLBlendEquation = equationRGB
	) {
		this._blendState.setAt(1, GL_MAP.BLEND_EQ[equationRGB]);
		this._blendState.setAt(2, GL_MAP.BLEND_EQ[equationA]);
	}

	public setBlendFactors(
		sourceFactor: ContextGLBlendFactor,
		destinationFactor: ContextGLBlendFactor,
		sourceAlphaFactor?: ContextGLBlendFactor,
		destinationAlphaFactor?: ContextGLBlendFactor
	): void {

		this.stateChangeCallback && this.stateChangeCallback('setBlendFactors');

		const src = GL_MAP.BLEND_OP[sourceFactor];
		const dst = GL_MAP.BLEND_OP[destinationFactor];
		const gl = this._gl;

		if (sourceAlphaFactor == void 0) {
			// disable separated mode
			this._blendState.set(+true, gl.FUNC_ADD, gl.FUNC_ADD, src, dst, -1, -1);
		} else {
			if (typeof sourceAlphaFactor !== typeof destinationAlphaFactor) {
				// eslint-disable-next-line max-len
				throw '[Context] sourceAlphaFactor and destinationAlphaFactor MUST be BOTH presented for separated blend mode';
			}

			const srcA = GL_MAP.BLEND_OP[sourceAlphaFactor];
			const dstA = GL_MAP.BLEND_OP[destinationAlphaFactor];
			// endable seprated mode
			this._blendState.set(+true, gl.FUNC_ADD, gl.FUNC_ADD, src, dst, srcA, dstA);
		}
	}

	public setColorMask(red: boolean, green: boolean, blue: boolean, alpha: boolean): void {
		this.stateChangeCallback && this.stateChangeCallback('setColorMask');

		if (this._colorMapState.set(red, green, blue, alpha)) {
			this._gl.colorMask(red, green, blue, alpha);
		}
	}

	public setCulling(
		triangleFaceToCull: ContextGLTriangleFace,
		coordinateSystem: CoordinateSystem = CoordinateSystem.LEFT_HANDED): void {

		this.stateChangeCallback && this.stateChangeCallback('setCulling');

		if (triangleFaceToCull === ContextGLTriangleFace.NONE) {
			this._cullState.setAt(0, +false) && this._gl.disable(this._gl.CULL_FACE);
		} else {
			this._cullState.setAt(0, +true) && this._gl.enable(this._gl.CULL_FACE);
			const face = this.translateTriangleFace(triangleFaceToCull, coordinateSystem);
			this._cullState.setAt(1, face) && this._gl.cullFace(face);
		}
	}

	// TODO ContextGLCompareMode
	public setDepthTest(depthMask: boolean, passCompareMode: ContextGLCompareMode): void {
		this.stateChangeCallback && this.stateChangeCallback('setDepthTest');

		const mode = GL_MAP.COMPARE_OP[passCompareMode];

		this._depthState.setAt(1, +depthMask) && this._gl.depthMask(depthMask);
		this._depthState.setAt(2, mode) && this._gl.depthFunc(mode);
	}

	public setViewport(x: number, y: number, width: number, height: number) {
		this.stateChangeCallback && this.stateChangeCallback('setViewport');

		if (this._viewportState.set(x,y,width, height)) {
			this._gl.viewport(x,y,width,height);
		}
	}

	public enableDepth() {
		this.stateChangeCallback && this.stateChangeCallback('enableDepth');
		this._depthState.setAt(0, +true) && this._gl.enable(this._gl.DEPTH_TEST);
	}

	public disableDepth() {
		this.stateChangeCallback && this.stateChangeCallback('disableDepth');

		this._depthState.setAt(0, +false) && this._gl.disable(this._gl.DEPTH_TEST);
	}

	public enableStencil() {
		this.stateChangeCallback && this.stateChangeCallback('enableStencil');

		this._stencilState.setAt(0, +true) && this._gl.enable(this._gl.STENCIL_TEST);
	}

	public disableStencil() {
		this.stateChangeCallback && this.stateChangeCallback('disableStencil');
		this._stencilState.setAt(0, +false) && this._gl.disable(this._gl.STENCIL_TEST);
	}

	public setStencilActions(
		triangleFace: ContextGLTriangleFace = ContextGLTriangleFace.FRONT_AND_BACK,
		compareMode: ContextGLCompareMode = ContextGLCompareMode.ALWAYS,
		actionOnBothPass: ContextGLStencilAction = ContextGLStencilAction.KEEP,
		actionOnDepthFail: ContextGLStencilAction = ContextGLStencilAction.KEEP,
		actionOnDepthPassStencilFail: ContextGLStencilAction = ContextGLStencilAction.KEEP,
		coordinateSystem: CoordinateSystem = CoordinateSystem.LEFT_HANDED): void
	{	/* eslint-disable-line */
		this.stateChangeCallback && this.stateChangeCallback('setStencilActions');
		this._separateStencil = triangleFace != ContextGLTriangleFace.FRONT_AND_BACK;

		const compareModeGL = GL_MAP.COMPARE_OP[compareMode];
		const fail = GL_MAP.STENCIL_ACTION[actionOnDepthPassStencilFail];
		const zFail = GL_MAP.STENCIL_ACTION[actionOnDepthFail];
		const pass = GL_MAP.STENCIL_ACTION[actionOnBothPass];

		if (!this._separateStencil) {

			this._stencilCompareMode = compareModeGL;
			this._gl.stencilFunc(compareModeGL, this._stencilReferenceValue, this._stencilReadMask);
			this._gl.stencilOp(fail, zFail, pass);

		} else if (triangleFace == ContextGLTriangleFace.BACK) {

			this._stencilCompareModeBack = compareModeGL;
			this._gl.stencilFuncSeparate(
				this._gl.BACK, compareModeGL, this._stencilReferenceValue, this._stencilReadMask);

			this._gl.stencilOpSeparate(this._gl.BACK, fail, zFail, pass);

		} else if (triangleFace == ContextGLTriangleFace.FRONT) {

			this._stencilCompareModeFront = compareModeGL;
			this._gl.stencilFuncSeparate(
				this._gl.FRONT, compareModeGL, this._stencilReferenceValue, this._stencilReadMask);
			this._gl.stencilOpSeparate(this._gl.FRONT, fail, zFail, pass);
		}

		this.assertLost('setStencilActions');
	}

	public setStencilReferenceValue(referenceValue: number, readMask: number = 0xFF, writeMask: number = 0xFF): void {
		this.stateChangeCallback && this.stateChangeCallback('setStencilReferenceValue');
		this._stencilReferenceValue = referenceValue;
		this._stencilReadMask = readMask;

		if (this._separateStencil) {
			this._gl.stencilFuncSeparate(this._gl.FRONT, this._stencilCompareModeFront, referenceValue, readMask);
			this._gl.stencilFuncSeparate(this._gl.BACK, this._stencilCompareModeBack, referenceValue, readMask);
		} else {
			this._gl.stencilFunc(this._stencilCompareMode, referenceValue, readMask);
		}

		this._gl.stencilMask(writeMask);

		this.assertLost('setStencilReferenceValue');
	}

	public setProgram(program: ProgramWebGL): void {
		// kill focus when program is same
		if (this._currentProgram === program) {
			return;
		}

		this.stateChangeCallback && this.stateChangeCallback('setProgram');

		//TODO decide on construction/reference resposibilities
		this._currentProgram = program;
		program.focusProgram();

		this.assertLost('setProgram');
	}

	public setProgramConstantsFromArray(programType: number, data: Float32Array): void {
		if (data.length) {
			this._currentProgram.uniform4fv(programType, data);
		}
	}

	public setScissorRectangle(rect: Rectangle): void {
		this.stateChangeCallback && this.stateChangeCallback('setScissorRectangle');

		if (!rect) {
			this._gl.disable(this._gl.SCISSOR_TEST);
			return;
		}

		const isRT = !!this._texContext._currentRT;

		// for framebuffer we use framebuffer size without internal scale
		const pr = isRT ? 1 : this._pixelRatio;

		/*
		// not require flip when renderer to RT, because already flipped
		const targetY = isRT
			? rect.y
			: this._height - (rect.y + rect.height) * pr;
		*/
		this._gl.enable(this._gl.SCISSOR_TEST);
		this._gl.scissor(
			rect.x * pr, rect.y * pr,
			rect.width * pr, rect.height * pr);

		this.assertLost('scissor');
	}

	public setTextureAt(sampler: number, texture: TextureBaseWebGL): void {
		this.stateChangeCallback && this.stateChangeCallback('setTextureAt');

		const id = this._texContext.setTextureAt(sampler, <TextureWebGL>texture);

		// id - is real sampler id, because texture context can change id of sasmpler
		// or return -1 when texture unbounded
		if (id >= 0) {
			this._currentProgram.uniform1i(ContextGLProgramType.SAMPLER, sampler, id);
		}
	}

	public setSamplerStateAt(
		sampler: number,
		wrap: ContextGLWrapMode,
		filter: ContextGLTextureFilter,
		mipfilter: ContextGLMipFilter
	): void {
		this.stateChangeCallback && this.stateChangeCallback('setSamplerStateAt');

		// proxy
		this._texContext.setSamplerStateAt(sampler, wrap, filter, mipfilter);

		this.assertLost('setSamplerStateAt');
	}

	public setVertexBufferAt(
		index: number, buffer: VertexBufferWebGL, bufferOffset: number = 0, format: number = 4
	): void {
		this.stateChangeCallback && this.stateChangeCallback('setVertexBufferAt');

		const location = this._currentProgram ? this._currentProgram.getAttribLocation(index) : -1;
		const gl = this._gl;

		// when we try bind any buffers without VAO we should unbound VAO
		// othwerwithe we bind a buffer to vao instead main contex
		if (this.hasVao && this._vaoContext._isRequireUnbound) {
			this._vaoContext.unbindVertexArrays();
		}

		// disable location, OS will fire error when loadings invalid buffer to it
		// location - is index of buffer location inside shader
		// index - attrib location for binding.
		// FOR OSx - IT CAN BE DIFFERENT

		if (!buffer) {
			if (location > -1) {
				gl.disableVertexAttribArray(index);
			}
			return;
		}

		//buffer may not have changed if concatenated buffers are being used
		if (this._currentArrayBuffer != buffer || (this.hasVao && this._vaoContext._lastBoundedVao)) {
			this._currentArrayBuffer = buffer;
			gl.bindBuffer(gl.ARRAY_BUFFER, buffer ? buffer.glBuffer : null);
		}

		const properties = GL_MAP.VERTEX_BUF_PROPS[format];

		gl.enableVertexAttribArray(location);
		gl.vertexAttribPointer(
			location, properties.size, properties.type, properties.normalized, buffer.dataPerVertex, bufferOffset);

		if (this._instancedEnabled && buffer.instanced) {
			if (this._angleInstanced) {
				this._angleInstanced.vertexAttribDivisorANGLE(location, 1);
			} else {
				(<WebGL2RenderingContext> gl).vertexAttribDivisor(location, 1);
			}
		}

		this.assertLost('setVertexBufferAt');
	}

	public setRenderToTexture(
		target: TextureBaseWebGL, enableDepthAndStencil: boolean = false, antiAlias: number = 0,
		_surfaceSelector: number = 0, _mipmapSelector: number = 0
	): void {
		this.stateChangeCallback && this.stateChangeCallback('setRenderToTexture');

		// proxy
		this._texContext.setRenderToTexture(
			target,
			enableDepthAndStencil,
			antiAlias > 1,
		);

		this.assertLost('setRenderToTexture');
	}

	public setRenderToBackBuffer(): void {
		this.stateChangeCallback && this.stateChangeCallback('setRenderToBackBuffer');
		this._texContext.setRenderToBackBuffer();
		this.assertLost('setRenderToBackBuffer');
	}

	public copyToTexture(target: TextureBaseWebGL, rect: Rectangle, destPoint: Point): void {
		this.stateChangeCallback && this.stateChangeCallback('setRenderToBackBuffer');

		if (!this._texContext._currentRT) {
			throw '[ContextWebGL] Try to copy from invalid frambuffer';
		}

		this._texContext.presentFrameBufferTo(this._texContext._currentRT, <TextureWebGL>target, rect, destPoint);
		this.assertLost('copyToTexture');

	}

	private updateBlendStatus(): void {
		const bs = this._blendState;

		if (!bs.dirty) {
			return;
		}

		const v = bs.values;
		const delta = bs.deltaDirty();

		// lock last applied state, used for avoid change state over unused, like A (draw) => B => A (draw)
		// B state is redundant and should be ignored
		bs.lock(true);

		if (!v[0] && delta[0]) {
			this._gl.disable(this._gl.BLEND);
			return;
		}

		delta[0] && this._gl.enable(this._gl.BLEND);

		// always ADD
		if (delta[0] || delta[1] || delta[2]) {
			if (v[1] !== v[2]) {
				this._gl.blendEquationSeparate(v[1], v[2]);
			} else {
				this._gl.blendEquation(v[1]);
			}
		}

		// we check that we use separatedBlendMode, for this v[4] (src_alpha) MUST be > -1
		// not required check a delta for it
		if (delta[0] || delta[3] || delta[4] || v[5] !== -1) {
			if (v[5] !== -1) {
				this._gl.blendFuncSeparate(v[3], v[4], v[5], v[6]);
			} else {
				this._gl.blendFunc(v[3], v[4]);
			}
		}
	}

	public finish() {
		this._gl.flush();
		this._gl.finish();

		this._fenceContext && this._fenceContext.tick();
		this.assertLost('finish');
	}

	private translateTriangleFace(triangleFace: ContextGLTriangleFace, coordinateSystem: CoordinateSystem) {
		switch (triangleFace) {
			case ContextGLTriangleFace.BACK:
				return (coordinateSystem == CoordinateSystem.LEFT_HANDED) ? this._gl.FRONT : this._gl.BACK;
			case ContextGLTriangleFace.FRONT:
				return (coordinateSystem == CoordinateSystem.LEFT_HANDED) ? this._gl.BACK : this._gl.FRONT;
			case ContextGLTriangleFace.FRONT_AND_BACK:
				return this._gl.FRONT_AND_BACK;
			default:
				throw 'Unknown ContextGLTriangleFace type.'; // TODO error
		}
	}
}
