import {
	AssetEvent,
	IAssetClass,
	Matrix3D,
	PerspectiveProjection,
} from '@awayjs/core';
import {
	AttributesBuffer,
	Image2D,
	ContextGLTriangleFace
} from '@awayjs/stage';
import {
	ContainerNode,
	ContainerNodeEvent,
	INode,
	PickGroup
} from '@awayjs/view';

import { IMaterial } from './base/IMaterial';
import { ITexture } from './base/ITexture';
import { Style } from './base/Style';
import { _IRender_MaterialClass } from './base/_IRender_MaterialClass';
import { _Render_RenderableBase } from './base/_Render_RenderableBase';
import { _Render_RendererMaterial } from './base/_Render_RendererMaterial';
import { _Stage_ElementsBase } from './base/_Stage_ElementsBase';
import { TriangleElements, _Stage_TriangleElements } from './elements/TriangleElements';
import { MaterialEvent } from './events/MaterialEvent';
import { RenderableEvent } from './events/RenderableEvent';
import { RendererBase } from './RendererBase';
import { RenderGroup } from './RenderGroup';
import { ImageTexture2D } from './textures/ImageTexture2D';
import { Settings as StageSettings } from '@awayjs/stage';
import { RenderEntity } from './base/RenderEntity';
import { DefaultRenderer } from './DefaultRenderer';

export class CacheRenderer extends RendererBase implements IMaterial {
	public static assetType: string = '[renderer CacheRenderer]';

	private _texture: ImageTexture2D;
	private _textures: ITexture[] = [];
	private _onTextureInvalidate: (event: AssetEvent) => void;
	private _onInvalidateParentNode: (event: ContainerNodeEvent) => void;
	private _onInvalidateColorTransform: (event: ContainerNodeEvent) => void;

	public animateUVs: boolean = false;

	public bothSides: boolean = false;

	public curves: boolean = false;

	public imageRect: boolean = false;

	public useColorTransform: boolean = true;

	public alphaBlending: boolean = true;

	public alphaThreshold: number = 0;

	public get assetType(): string {
		return CacheRenderer.assetType;
	}

	/**
	* The 2d texture to use as a bitmap cache.
	*/
	public get texture(): ImageTexture2D {

		//TODO check invalidation of _texture

		return this._texture;
	}

	public set texture(value: ImageTexture2D) {
		if (this._texture == value)
			return;

		if (this._texture)
			this.removeTexture(this._texture);

		this._texture = value;

		if (this._texture)
			this.addTexture(this._texture);

		this.invalidatePasses();
	}

	constructor() {
		super();

		this._onTextureInvalidate = (event: AssetEvent) => this.invalidate();
		this._onInvalidateParentNode = (event: ContainerNodeEvent) => this.onInvalidate(null);
		this._onInvalidateColorTransform = (event: ContainerNodeEvent) => this.onInvalidate(null);

		this._traverserGroup = RenderGroup.getInstance(CacheRenderer);
		this._maskGroup = RenderGroup.getInstance(DefaultRenderer);
	}

	public init(node: INode, pool: RenderGroup): void {
		super.init(node, pool);

		if (this._parentNode) {
			this._parentNode.addEventListener(ContainerNodeEvent.INVALIDATE_MATRIX3D, this._onInvalidateParentNode);
			this._parentNode.addEventListener(ContainerNodeEvent.INVALIDATE_COLOR_TRANSFORM, this._onInvalidateColorTransform);
		}

		// for check filters/blends changes
		this.node.container.addEventListener(RenderableEvent.INVALIDATE_STYLE, this._onInvalidateParentNode);
		this.node.container.addEventListener(ContainerNodeEvent.INVALIDATE_COLOR_TRANSFORM, this._onInvalidateColorTransform);

		this.texture = new ImageTexture2D();
	}

	public render(
		enableDepthAndStencil: boolean = true,
		surfaceSelector: number = 0,
		mipmapSelector: number = 0,
		maskConfig: number = 0
	): void {
		const container = this.node.container;

		const stage = this.stage;
		const useNonNativeBlend = this.useNonNativeBlend;
		const targetImage = <Image2D> (useNonNativeBlend ? this.parentRenderer.style.image : this._style.image);

		if (!targetImage) {
			return super.render(enableDepthAndStencil, surfaceSelector, mipmapSelector, maskConfig);
		}

		// WE MUST store older render target config,
		// because node can have cached child, that can be filtered
		this.stage.pushRenderTargetConfig();

		let sourceImage: Image2D;

		// we not require use TMP texture when not have MSAA
		if (stage.context.glVersion === 2 &&
			StageSettings.ENABLE_MULTISAMPLE_TEXTURE
		) {
			sourceImage = stage.filterManager.popTemp(
				targetImage.width,
				targetImage.height,
				true
			);

		}

		//for DefaultRenderer when child has a blendmode applied
		if (useNonNativeBlend) {
			const proj = this.parentRenderer.view.projection;
			this.parentRenderer.view.projection = new PerspectiveProjection();
			this.parentRenderer.view.target = targetImage;
			//this.parentRenderer.disableClear = true;
			this.parentRenderer._initRender(targetImage);
			this.parentRenderer.executeRender();
			this.parentRenderer.resetHead();
			//this.parentRenderer.disableClear = false;
			this.parentRenderer.view.target = null;
			this.parentRenderer.view.projection = proj;
			this._disableClear = true;
		}

		// we should render with colorTransform to self, enable it
		this.node.colorTransformDisabled = false;
		this._initRender(sourceImage || targetImage);
		super.render(enableDepthAndStencil, surfaceSelector, mipmapSelector, maskConfig);

		// restore colorTransform state as in transform state
		this.node.colorTransformDisabled = this.node.transformDisabled;

		if (targetImage.width * targetImage.height === 0) {
			throw new Error('Cannot have image with size 0 * 0');
		}

		//@ts-ignore
		const filters = container.filters;
		if (filters && filters.length > 0) {
			filters.forEach((e) => e && (e.imageScale = this._boundsScale));
			stage.filterManager.applyFilters(
				sourceImage || targetImage,
				targetImage, // because we use source as filter target - we not require copy
				targetImage.rect,
				targetImage.rect,
				filters
			);
		} else if (sourceImage) {
			// this is fast, it should only call blitFramebuffer,
			// same as in regular MSAA
			stage.filterManager.copyPixels(
				sourceImage,
				targetImage,
				targetImage.rect,
				targetImage.rect.topLeft,
				useNonNativeBlend,
				useNonNativeBlend ? <string> container.blendMode : ''
			);
		}

		if (sourceImage) {
			stage.filterManager.pushTemp(sourceImage);
		}

		// pop render target after any filters,
		// required for deep filters (when node with filter has filtered child)
		this.stage.popRenderTarget();
	}

	// apply blend modes and swap texture if needed
	public preActivateRenderPass() {

	}

	public getNumTextures(): number {
		return this._textures.length;
	}

	public getTextureAt(index: number): ITexture {
		return this._textures[index];
	}

	public addTexture(texture: ITexture): void {
		this._textures.push(texture);

		texture.addEventListener(AssetEvent.INVALIDATE, this._onTextureInvalidate);

		this.invalidate();
	}

	public removeTexture(texture: ITexture): void {
		this._textures.splice(this._textures.indexOf(texture), 1);

		texture.removeEventListener(AssetEvent.INVALIDATE, this._onTextureInvalidate);

		this.invalidate();
	}

	/**
	 * Marks the shader programs for all passes as invalid, so they will be recompiled before the next use.
	 *
	 * @private
	 */
	public invalidatePasses(): void {
		this.dispatchEvent(new MaterialEvent(MaterialEvent.INVALIDATE_PASSES, this));
	}

	/**
	 *
	 */
	public enterNode(node: ContainerNode): boolean {
		const enter: boolean = super.enterNode(node);

		if (enter && node.boundsVisible)
			this.applyEntity(node.getBoundsPrimitive(PickGroup.getInstance()));

		return enter;
	}

	public onInvalidate(event: AssetEvent): void {
		super.onInvalidate(event);

		this.dispatchEvent(new RenderableEvent(RenderableEvent.INVALIDATE_ELEMENTS, this));

		this.dispatchEvent(new RenderableEvent(RenderableEvent.INVALIDATE_STYLE, this));

		this.invalidate();
		this.invalidatePasses();
	}

	public onClear(event: AssetEvent): void {

		this.removeTexture(this._texture);
		this._texture.clear();
		this._texture = null;
		this._style.image.clear();
		this._style.image = null;

		if (this._parentNode) {
			this._parentNode.removeEventListener(ContainerNodeEvent.INVALIDATE_MATRIX3D, this._onInvalidateParentNode);
			this._parentNode.removeEventListener(ContainerNodeEvent.INVALIDATE_COLOR_TRANSFORM, this._onInvalidateColorTransform);
		}

		this.node.container.removeEventListener(RenderableEvent.INVALIDATE_STYLE, this._onInvalidateParentNode);
		this.node.container.removeEventListener(ContainerNodeEvent.INVALIDATE_COLOR_TRANSFORM, this._onInvalidateColorTransform);

		super.onClear(event);
	}

	public static registerMaterial(renderMaterialClass: _IRender_MaterialClass, materialClass: IAssetClass): void {
		RenderGroup.getInstance(CacheRenderer).registerMaterial(renderMaterialClass, materialClass);
	}
}

export class _Render_Renderer extends _Render_RenderableBase {

	public static assetType: string = '[render Renderer]';

	private _elements: TriangleElements;

	protected _getStageElements(): _Stage_ElementsBase {

		const asset = <CacheRenderer> this._asset;
		const paddedBounds = asset.getPaddedBounds();
		//const bounds = asset.getBounds();
		const offsetX = 0;//paddedBounds.x - bounds.x;
		const offsetY = 0;//paddedBounds.y - bounds.y;
		const matrix3D: Matrix3D = Matrix3D.CALCULATION_MATRIX;

		matrix3D.copyFrom(this.renderSceneTransform);
		matrix3D.appendScale(asset._boundsScale, asset._boundsScale, asset._boundsScale);
		matrix3D.appendTranslation(offsetX * 0.5, offsetY * 0.5, 0);
		matrix3D.invert();

		const vectors: number[] = [
			paddedBounds.left, paddedBounds.top, 0,
			paddedBounds.right, paddedBounds.bottom, 0,
			paddedBounds.right, paddedBounds.top, 0,

			paddedBounds.left, paddedBounds.top, 0,
			paddedBounds.left, paddedBounds.bottom, 0,
			paddedBounds.right, paddedBounds.bottom, 0,
		];

		matrix3D.transformVectors(vectors, vectors);

		let elements: TriangleElements = this._elements;

		if (!elements) {
			elements = this._elements = new TriangleElements(new AttributesBuffer(5, 6));
			elements.setPositions(vectors);
			elements.setUVs([0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]);
		} else {
			elements.setPositions(vectors);
		}

		return elements.getAbstraction<_Stage_TriangleElements>(this._stage);
	}

	public executeRender(
		enableDepthAndStencil: boolean = true,
		surfaceSelector: number = 0, mipmapSelector: number = 0, maskConfig: number = 0): void {

		// disable cull, because for render to texture it is bugged
		this._stage.context.setCulling(ContextGLTriangleFace.NONE);
		super.executeRender(enableDepthAndStencil, surfaceSelector, mipmapSelector, maskConfig);
	}

	protected _getRenderMaterial(): _Render_RendererMaterial {
		return this._asset.getAbstraction<_Render_RendererMaterial>(
			this.entity.renderer.getRenderElements(this.stageElements.elements));
	}

	protected _getStyle(): Style {

		return (<CacheRenderer> this._asset).style;
	}
}
DefaultRenderer.registerMaterial(_Render_RendererMaterial, CacheRenderer);
CacheRenderer.registerMaterial(_Render_RendererMaterial, CacheRenderer);
RenderEntity.registerRenderable(_Render_Renderer, CacheRenderer);