import {
	EventDispatcher,
	Rectangle,
	IAssetClass,
	IAbstractionPool,
	IAbstractionClass,
	CSS,
	Point,
	ColorTransform,
	IAsset,
	UUID,
	IAbstraction
} from '@awayjs/core';

import { ContextMode } from './base/ContextMode';
import { ContextGLMipFilter } from './base/ContextGLMipFilter';
import { ContextGLTextureFilter } from './base/ContextGLTextureFilter';
import { ContextGLWrapMode } from './base/ContextGLWrapMode';
import { ContextGLProfile } from './base/ContextGLProfile';
import { IContextGL } from './base/IContextGL';
import { IVertexBuffer } from './base/IVertexBuffer';
import { StageEvent } from './events/StageEvent';
import { ImageBase } from './image/ImageBase';
import { ImageSampler } from './image/ImageSampler';
import { _Stage_ImageBase } from './image/ImageBase';
import { ProgramData } from './image/ProgramData';
import { ProgramDataPool } from './image/ProgramDataPool';
import { StageManager } from './managers/StageManager';
import { ContextWebGL } from './webgl/ContextWebGL';
import { ContextGLClearMask } from './base/ContextGLClearMask';
import { Image2D } from './image/Image2D';
import { UnloadService } from './managers/UnloadManager';
import { ImageUtils } from './utils/ImageUtils';
import { TouchPoint } from './base/TouchPoint';
import { FilterManager } from './managers/FilterManager';
import { BUFFER_FORMATS_MAP } from './utils/BufferFormat';

const TMP_POINT = { x: 0, y: 0 };
interface ITargetConf {
	target: ImageBase;
	mipSelector: number;
	surfaceSelector: number;
	depthStencil: boolean;
	msaa: boolean;
}

/**
 * Stage provides a proxy class to handle the creation and attachment of the Context
 * (and in turn the back buffer) it uses. Stage should never be created directly,
 * but requested through StageManager.
 *
 * @see away.managers.StageManager
 *
 */
export class Stage extends EventDispatcher implements IAbstractionPool {
	private static _abstractionClassPool: Record<string, IAbstractionClass> = {};
	private _programData: Array<ProgramData> = new Array<ProgramData>();
	private _programDataPool: ProgramDataPool;
	private _context: IContextGL;
	private _container: HTMLCanvasElement;
	private _width: number;
	private _height: number;
	private _x: number = 0;
	private _y: number = 0;

	public _screenX: number;
	public _screenY: number;
	public _touchPoints: Array<TouchPoint> = new Array<TouchPoint>();

	//private static _frameEventDriver:Shape = new Shape(); // TODO: add frame driver/request animation frame

	private _stageIndex: number = -1;

	private _usesSoftwareRendering: boolean;
	private _profile: ContextGLProfile;
	private _stageManager: StageManager;
	private _antiAlias: number = 4;

	private readonly savedTargetStack: Array<ITargetConf> = [];
	private readonly _activeTargetConf: ITargetConf = {
		mipSelector: 0,
		surfaceSelector: 0,
		target: null,
		depthStencil: true,
		msaa: true
	};

	private _color: number;
	private _backBufferDirty: boolean;
	private _sizeDirty: boolean;

	private _lastScissorBox: Rectangle;

	private _frameEndCallbackOnce: Array<() => void> = [];

	private _initialised: boolean = false;

	public globalDisableMipmap: boolean = false;

	public globalDisableSmooth: boolean = false;

	public numUsedStreams: number = 0;
	public numUsedTextures: number = 0;

	public readonly filterManager: FilterManager;

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

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

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

	public get screenX(): number {
		return this._screenX;
	}

	public get screenY(): number {
		return this._screenY;
	}

	public get touchPoints(): Array<TouchPoint> {
		return this._touchPoints;
	}

	public readonly id: number;

	constructor(
		container: HTMLCanvasElement, stageIndex: number,
		stageManager: StageManager, forceSoftware: boolean = false,
		profile: ContextGLProfile = ContextGLProfile.BASELINE) {

		super();

		this.id = UUID.Next();
		this._programDataPool = new ProgramDataPool(this);

		this._container = container;
		if (this._container) {
			this._container.addEventListener('webglcontextlost', (event) => this.onContextLost(event));
			this._container.addEventListener('webglcontextrestored', (event) => this.onContextRestored(event));
		}

		this._stageIndex = stageIndex;

		this._stageManager = stageManager;

		CSS.setElementX(this._container, 0);
		CSS.setElementY(this._container, 0);

		this._width = this._container.clientWidth;
		this._height = this._container.clientHeight;

		this.visible = true;

		this.filterManager = new FilterManager(this);
	}

	// for avoid using a Dispather, because it has big overhead
	public requiestFrameEnd(func: () => void) {
		if (this._frameEndCallbackOnce.indexOf(func) > -1 || typeof func !== 'function') {
			return;
		}

		this._frameEndCallbackOnce.push(func);
	}

	/**
	 * @description Should be executed AFTER rendering process
	 */
	public present() {
		this._context.present();
		UnloadService.executeAll();

		for (const call of this._frameEndCallbackOnce) {
			call();
		}
		this._frameEndCallbackOnce.length = 0;
	}

	public getProgramData(vertexString: string, fragmentString: string): ProgramData {
		return this._programDataPool.getItem(vertexString, fragmentString);
	}

	/**
	 * Safe current target config to stack
	 */
	public pushRenderTargetConfig() {
		this.savedTargetStack.push(Object.assign({}, this._activeTargetConf));
	}

	/**
	 * Pop render target config and apply it, used for deep caching
	 */
	public popRenderTarget() {
		if (this.savedTargetStack.length === 0)
			return;

		const {
			target,
			depthStencil,
			surfaceSelector,
			mipSelector,
			msaa
		} = this.savedTargetStack.pop();

		this.setRenderTarget(
			target,
			depthStencil,
			surfaceSelector,
			mipSelector,
			!msaa
		);
	}

	public setRenderTarget(
		target: ImageBase & {antialiasQuality?: number},
		enableDepthAndStencil: boolean = false,
		surfaceSelector: number = 0,
		mipmapSelector: number = 0,
		disableMSAA = false
	): void {
		const conf = this._activeTargetConf;

		if (conf.target === target
			&& conf.surfaceSelector === surfaceSelector
			&& conf.mipSelector === mipmapSelector
			&& conf.depthStencil === enableDepthAndStencil
			&& conf.msaa !== disableMSAA
		) {
			return;
		}

		conf.msaa = !disableMSAA;
		conf.target = target;
		conf.surfaceSelector = surfaceSelector;
		conf.mipSelector = mipmapSelector;
		conf.depthStencil = enableDepthAndStencil;

		if (target) {
			const targetStageElement = target.getAbstraction<_Stage_ImageBase>(this);
			const antiallias = typeof target.antialiasQuality === 'number' // for SceneImage2D MSAA
				? target.antialiasQuality
				: this._antiAlias;
			this._context.setRenderToTexture(
				targetStageElement.getTexture(),
				enableDepthAndStencil,
				disableMSAA ? 0 : antiallias,
				surfaceSelector,
				mipmapSelector);

			if (mipmapSelector != 0 && this._context.glVersion != 1) { //hack to stop auto generated mipmaps
				targetStageElement._invalidMipmaps = false;
				targetStageElement._mipmap = true;
			}
		} else {
			this._context.setRenderToBackBuffer();
			this.configureBackBuffer(this._width, this._height, this._antiAlias, conf.depthStencil);
		}
	}

	public copyPixels(
		source: Image2D, target: Image2D,
		rect: Rectangle, destPoint: Point,
		alphaBitmapData: Image2D = null, alphaPoint: Point = null,
		mergeAlpha: boolean = false): void {

		// migrated to filter manager
		this.filterManager.copyPixels(source, target, rect, destPoint, mergeAlpha);
	}

	public threshold(
		source: Image2D, target: Image2D,
		rect: Rectangle, destPoint: Point,
		operation: string, threshold: number,
		color: number, mask: number, copySource: boolean): void {

		// migrated to filter manager
		this.filterManager.threshold(source, target, rect, destPoint, operation, threshold, color, mask, copySource);
	}

	public colorTransform(source: Image2D, target: Image2D, rect: Rectangle, colorTransform: ColorTransform): void {

		// migrated to filter manager
		this.filterManager.colorTransform(source, target, rect, colorTransform);
	}

	public requestAbstraction(asset: IAsset): IAbstraction {
		return new Stage._abstractionClassPool[asset.assetType];
	}

	public storeAbstraction(abstraction: IAbstraction): void {

	}

	/**
	 *
	 * @param imageObjectClass
	 */
	public static registerAbstraction(abstractionClass: IAbstractionClass, assetClass: IAssetClass): void {
		Stage._abstractionClassPool[assetClass.assetType] = abstractionClass;
	}

	/**
	 * Requests a Context object to attach to the managed gl canvas.
	 */
	public requestContext(
		forceSoftware: boolean = false,
		profile: ContextGLProfile = ContextGLProfile.BASELINE,
		_mode: ContextMode = ContextMode.AUTO,
		alpha: boolean = false
	): void {

		// If forcing software, we can be certain that the
		// returned Context will be running software mode.
		// If not, we can't be sure and should stick to the
		// old value (will likely be same if re-requesting.)

		if (this._usesSoftwareRendering != null)
			this._usesSoftwareRendering = forceSoftware;

		this._profile = profile;

		try {
			this._context = new ContextWebGL(this._container, alpha);
		} catch (e) {
			this.dispatchEvent(new StageEvent(StageEvent.STAGE_ERROR, this));
		}

		if (this._context)
			this._callback(this._context);
	}

	/**
	 * The width of the gl canvas
	 */
	public get width(): number {
		this._sizeDirty = false;

		return this._width;
	}

	public set width(val: number) {
		if (this._width == val)
			return;

		this._container.style.width = val + 'px';
		this._container.width = val * this.pixelRatio;

		this._width = val;

		this._backBufferDirty = true;

		if (this._width && this._height)
			this._invalidateSize();
	}

	/**
	 * The height of the gl canvas
	 */
	public get height(): number {
		this._sizeDirty = false;

		return this._height;
	}

	public set height(val: number) {
		if (this._height == val)
			return;

		this._container.style.height = val + 'px';
		this._container.height = val * this.pixelRatio;

		this._height = val;

		this._backBufferDirty = true;

		if (this._width && this._height)
			this._invalidateSize();
	}

	/**
	 * The x position of the gl canvas
	 */
	public get x(): number {
		return this._x;
	}

	public set x(val: number) {
		if (this._x == val)
			return;

		CSS.setElementX(this._container, val);

		this._x = val;
	}

	/**
	 * The y position of the gl canvas
	 */
	public get y(): number {
		return this._y;
	}

	public set y(val: number) {
		if (this._y == val)
			return;

		CSS.setElementY(this._container, val);

		this._y = val;
	}

	public set visible(val: boolean) {
		CSS.setElementVisibility(this._container, val);
	}

	public get visible(): boolean {
		return CSS.getElementVisibility(this._container);
	}

	public set container(value: HTMLElement) {
		this._container = <HTMLCanvasElement>value;
		if (this._context) {
			this._context.container = this._container;
		}

	}

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

	/**
	 * The Context object associated with the given stage object.
	 */
	public get context(): IContextGL {
		return this._context;
	}

	private _invalidateSize(): void {
		if (this._sizeDirty)
			return;

		this._sizeDirty = true;

		this.dispatchEvent(new StageEvent(StageEvent.INVALIDATE_SIZE, this));
	}

	public get profile(): ContextGLProfile {
		return this._profile;
	}

	/**
	 * Disposes the Stage object, freeing the Context attached to the Stage.
	 */
	public dispose(): void {
		/*
		for (var id in this._abstractionPool){
			if(this._abstractionPool[id].clear)
				this._abstractionPool[id].clear();
		}*/

		this._stageManager.iRemoveStage(this);
		this.freeContext();
		this._stageManager = null;
		this._stageIndex = -1;

		UnloadService.clearAll();
	}

	/**
	 * Configures the back buffer associated with the Stage object.
	 * @param backBufferWidth The width of the backbuffer.
	 * @param backBufferHeight The height of the backbuffer.
	 * @param antiAlias The amount of anti-aliasing to use.
	 * @param enableDepthAndStencil Indicates whether the back buffer contains a depth and stencil buffer.
	 */
	public configureBackBuffer(
		backBufferWidth: number, backBufferHeight: number, antiAlias: number, enableDepthAndStencil: boolean): void {

		this.width = backBufferWidth;
		this.height = backBufferHeight;

		this._antiAlias = antiAlias;
		this._activeTargetConf.depthStencil = enableDepthAndStencil;

		if (this._context)
			this._context.configureBackBuffer(backBufferWidth, backBufferHeight, antiAlias, enableDepthAndStencil);
	}

	/*
	 * Indicates whether the depth and stencil buffer is used
	 */
	public get enableDepthAndStencil(): boolean {
		return this._activeTargetConf.depthStencil;
	}

	public set enableDepthAndStencil(enableDepthAndStencil: boolean) {
		if (this._activeTargetConf.depthStencil == enableDepthAndStencil)
			return;

		this._activeTargetConf.depthStencil = enableDepthAndStencil;
		this._backBufferDirty = true;
	}

	public get renderSurfaceSelector(): number {
		return this._activeTargetConf.surfaceSelector;
	}

	/*
	 * Clear and reset the back buffer when using a shared context
	 */
	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 {

		if (!this._context)
			return;

		if (this._backBufferDirty && this._activeTargetConf.target == null) {
			this._backBufferDirty = false;

			this.configureBackBuffer(
				this._width,
				this._height,
				this._antiAlias,
				this._activeTargetConf.depthStencil);
		}

		this._context.clear(red, green, blue, alpha, depth, stencil, mask);
	}

	/**
	 * The index of the Stage which is managed by this instance of StageProxy.
	 */
	public get stageIndex(): number {
		return this._stageIndex;
	}

	/**
	 * Indicates whether the Stage managed by this proxy is running in software mode.
	 * Remember to wait for the CONTEXT_CREATED event before checking this property,
	 * as only then will it be guaranteed to be accurate.
	 */
	public get usesSoftwareRendering(): boolean {
		return this._usesSoftwareRendering;
	}

	/**
	 * The antiAliasing of the Stage.
	 */
	public get antiAlias(): number {
		return this._antiAlias;
	}

	public set antiAlias(antiAlias: number) {
		if (this._antiAlias == antiAlias)
			return;

		this._antiAlias = antiAlias;

		this._backBufferDirty = true;
	}

	/**
	 * The background color of the Stage.
	 */
	public get color(): number {
		return this._color;
	}

	public set color(color: number) {
		this._color = color;
	}

	public registerProgram(programData: ProgramData): void {
		let i: number = 0;
		while (this._programData[i] != null)
			i++;

		this._programData[i] = programData;
		programData.id = i;
	}

	public unRegisterProgram(programData: ProgramData): void {
		this._programData[programData.id] = null;
		programData.id = -1;
	}

	public interactionHandler(event: MouseEvent | TouchEvent) {

		const screenX: number = ((<MouseEvent> event).clientX != null) ? (<MouseEvent> event).clientX : (<TouchEvent> event).changedTouches[0].clientX;
		const screenY: number = ((<MouseEvent> event).clientY != null) ? (<MouseEvent> event).clientY : (<TouchEvent> event).changedTouches[0].clientY;

		let point = this.mapWindowToStage(screenX, screenY, TMP_POINT);
		this._screenX = point.x;
		this._screenY = point.y;

		this._touchPoints.length = 0;

		if ((<TouchEvent> event).touches) {
			for (let i: number = 0; i < (<TouchEvent> event).touches.length; i++) {
				const touch = (<TouchEvent> event).touches[i];

				point = this.mapWindowToStage(touch.clientX, touch.clientY, TMP_POINT);

				this._touchPoints.push(new TouchPoint(point.x, point.y, touch.identifier));
			}
		}

	}

	public mapWindowToStage(x: number, y: number, out: {x: number, y: number} = { x: 0, y: 0 }) {
		const container = <HTMLCanvasElement> this.container;

		// IE 11 fix
		const rect = (!container.parentElement)
			? { x: 0, y: 0, width: 0, height: 0 }
			: container.getBoundingClientRect();

		// workground when pixelRatio !== self.devicePixelRatio
		out.x = (x - rect.x) * container.width / (rect.width * this.pixelRatio);
		out.y = (y - rect.y) * container.height / (rect.height * this.pixelRatio);

		return out;
	}

	/**
	 * Frees the Context associated with this StageProxy.
	 */
	private freeContext(): void {
		if (this._context) {
			this._context.dispose();

			this.dispatchEvent(new StageEvent(StageEvent.CONTEXT_DISPOSED, this));
		}

		this._context = null;

		this._initialised = false;
	}

	private onContextLost(_event): void {

	}

	private onContextRestored(_event): void {

	}

	public recoverFromDisposal(): boolean {
		if (!this._context)
			return false;

		//away.Debug.throwPIR( 'StageProxy' , 'recoverFromDisposal' , '' );

		/*
		 if (this._iContext.driverInfo == "Disposed")
		 {
		 this._iContext = null;
		 this.dispatchEvent(new StageEvent(StageEvent.CONTEXT_DISPOSED));
		 return false;

		 }
		 */
		return true;

	}

	private _callback(context: IContextGL): void {
		const gl = (<ContextWebGL>context)._gl;
		ImageUtils.MAX_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE);

		this._context = context;

		this._container = this._context.container;

		// Only configure back buffer if width and height have been set,
		// which they may not have been if View.render() has yet to be
		// invoked for the first time.
		if (this._width && this._height) {
			this._context.configureBackBuffer(
				this._width,
				this._height,
				this._antiAlias,
				this._activeTargetConf.depthStencil);
		}

		// Dispatch the appropriate event depending on whether context was
		// created for the first time or recreated after a device loss.
		this.dispatchEvent(
			new StageEvent(this._initialised ? StageEvent.CONTEXT_RECREATED : StageEvent.CONTEXT_CREATED, this));

		this._initialised = true;
	}

	public setVertexBuffer(
		index: number, buffer: IVertexBuffer,
		size: number, dimensions: number,
		offset: number, unsigned: boolean = false): void {

		this._context.setVertexBufferAt(
			index, buffer, offset, BUFFER_FORMATS_MAP[unsigned ? size + 4 : size][dimensions - 1]);
	}

	public setScissor(rectangle: Rectangle): void {
		if (this._backBufferDirty && this._activeTargetConf.target == null) {
			this._backBufferDirty = false;

			this.configureBackBuffer(this._width, this._height, this._antiAlias, this._activeTargetConf.depthStencil);
		}

		if (!this._lastScissorBox && !rectangle) {
			return;
		}

		if (rectangle && this._lastScissorBox && this._lastScissorBox.equals(rectangle)) {
			return;
		}

		if (!rectangle) {
			this._lastScissorBox = null;
		} else {
			if (!this._lastScissorBox) {
				this._lastScissorBox = new Rectangle();
			}

			this._lastScissorBox.copyFrom(rectangle);
		}

		this._context.setScissorRectangle(rectangle);
	}

	public setSamplerAt(index: number, sampler: ImageSampler): void {
		const wrap = sampler.repeat ? ContextGLWrapMode.REPEAT : ContextGLWrapMode.CLAMP;
		const filter = (sampler.smooth && !this.globalDisableSmooth)
			? ContextGLTextureFilter.LINEAR
			: ContextGLTextureFilter.NEAREST;
		const mipfilter = (sampler.mipmap && !this.globalDisableMipmap)
			? ContextGLMipFilter.MIPLINEAR
			: ContextGLMipFilter.MIPNONE;

		this._context.setSamplerStateAt(index, wrap, filter, mipfilter);
	}
}