import { AssetEvent, AbstractionBase, WeakAssetSet } from '@awayjs/core';

import {
	Stage,
	_Stage_ImageBase,
	ImageSampler,
	ImageUtils,
	ImageBase,
} from '@awayjs/stage';

import { IPass } from './IPass';
import { Style } from './Style';
import { IMaterial } from './IMaterial';
import { IAnimator } from './IAnimator';
import { ITexture } from '../base/ITexture';
import { PassEvent } from '../events/PassEvent';
import { MaterialEvent } from '../events/MaterialEvent';
import { IAnimationSet } from './IAnimationSet';
import { _Render_ElementsBase } from './_Render_ElementsBase';
import { ShaderBase } from './ShaderBase';
import { _Render_RenderableBase } from './_Render_RenderableBase';
import { IRenderContainer } from './IRenderContainer';
import { CacheRenderer } from '../CacheRenderer';
import { RenderEntity } from './RenderEntity';

/**
 *
 * @class away.pool.Passes
 */
export class _Render_MaterialBase extends AbstractionBase {
	private _onInvalidateTexturesDelegate: (event: MaterialEvent) => void;
	private _onInvalidatePassesDelegate: (event: MaterialEvent) => void;
	private _onPassInvalidateDelegate: (event: PassEvent) => void;

	/**
	 * A list of material owners, renderables or custom Entities.
	 */
	public _owners: WeakAssetSet;

	protected _renderOrderId: number;
	protected _passes: IPass[] = [];
	private _animationSet: IAnimationSet;
	protected _stage: Stage;

	private _invalidAnimation: boolean = true;
	protected _invalidRender: boolean = true;
	private _invalidImages: boolean = true;

	private _imageIndices: Record<number, number[]> = {};
	private _numImages: number;
	private _usesAnimation: boolean = false;

	public _activePass: IPass;

	public images: _Stage_ImageBase[] = [];

	public samplers: ImageSampler[] = [];

	/**
     * Indicates whether or not the renderable requires alpha blending during rendering.
     */
	public requiresBlending: boolean = false;

	public materialID: number;

	public get animationSet(): IAnimationSet {
		return this._animationSet;
	}

	public get material(): IMaterial {
		return <IMaterial> this._asset;
	}

	public get numImages(): number {
		if (this._invalidImages)
			this._updateImages();

		return this._numImages;
	}

	public get renderOrderId(): number {
		if (this._invalidRender)
			this._pUpdateRender();
		if (this._invalidAnimation)
			this._updateAnimation();

		return this._renderOrderId;
	}

	public get numPasses(): number {
		if (this._invalidAnimation)
			this._updateAnimation();

		return this._passes.length;
	}

	public get style(): Style {
		return (<IMaterial> this._asset).style;
	}

	public get renderElements(): _Render_ElementsBase {
		return this._useWeak ? (<WeakRef<_Render_ElementsBase>> this._pool).deref() : <_Render_ElementsBase> this._pool;
	}

	constructor() {
		super();

		this._onInvalidateTexturesDelegate = (event: MaterialEvent) => this.onInvalidateTextures(event);
		this._onInvalidatePassesDelegate = (event: MaterialEvent) => this.onInvalidatePasses(event);
		this._onPassInvalidateDelegate = (event: PassEvent) => this.onPassInvalidate(event);
	}

	public init(material: IMaterial, renderElements: _Render_ElementsBase): void {
		super.init(material, renderElements, true);

		this.materialID = material.id;
		this._stage = renderElements.stage;
		renderElements.addMaterial(this);

		this._owners = new WeakAssetSet('_Render_RenderableBase');

		(<IMaterial> this._asset).addEventListener(MaterialEvent.INVALIDATE_TEXTURES, this._onInvalidateTexturesDelegate);
		(<IMaterial> this._asset).addEventListener(MaterialEvent.INVALIDATE_PASSES, this._onInvalidatePassesDelegate);
	}

	public activatePass(index: number): void {
		this._activePass = this._passes[index];

		//clear unused vertex streams
		let i: number;
		for (i =  this._activePass.shader.numUsedStreams; i < this._stage.numUsedStreams; i++)
			this._stage.context.setVertexBufferAt(i, null);

		//clear unused texture streams
		for (i =  this._activePass.shader.numUsedTextures; i < this._stage.numUsedTextures; i++)
			this._stage.context.setTextureAt(i, null);

		//activate shader object through pass
		this._activePass._activate(this.renderElements.renderer.view);
	}

	public deactivatePass(): void {
		//deactivate shader object through pass
		this._activePass._deactivate();

		this._stage.numUsedStreams = this._activePass.shader.numUsedStreams;
		this._stage.numUsedTextures = this._activePass.shader.numUsedTextures;
	}

	//
	// MATERIAL MANAGEMENT
	//
	/**
	 * Mark an IEntity as owner of this material.
	 * Assures we're not using the same material across renderables with different animations, since the
	 * Programs depend on animation. This method needs to be called when a material is assigned.
	 *
	 * @param owner The IEntity that had this material assigned
	 *
	 * @internal
	 */
	public addOwner(owner: _Render_RenderableBase): void {
		this._owners.add(owner);

		let animationSet: IAnimationSet;
		const animator: IAnimator = (<IRenderContainer> owner.entity.node.container).animator;

		if (animator)
			animationSet = <IAnimationSet> animator.animationSet;

		if ((<IRenderContainer> (<RenderEntity> owner.entity).node.container).animator) {
			if (this._animationSet && animationSet != this._animationSet) {
				throw new Error(
					'A Material instance cannot be shared across ' +
					'material owners with different animation sets');
			} else {
				if (this._animationSet != animationSet) {

					this._animationSet = animationSet;

					this._invalidAnimation = true;
				}
			}
		}
	}

	/**
	 * Removes an IEntity as owner.
	 * @param owner
	 *
	 * @internal
	 */
	public removeOwner(owner: _Render_RenderableBase): void {
		if (!this._owners) //check if material is already disposed
			return;

		this._owners.remove(owner);

		if (!this._owners.numAssets)
			this.onClear(null);
	}

	public getImageIndex(texture: ITexture, index: number = 0): number {
		if (this._invalidImages)
			this._updateImages();

		return this._imageIndices[texture.id][index];
	}

	/**
	 *
	 */
	public onClear(event: AssetEvent): void {
		this.renderElements?.removeMaterial(this);

		const len: number = this._passes.length;
		for (let i: number = 0; i < len; i++) {
			this._passes[i].removeEventListener(PassEvent.INVALIDATE, this._onPassInvalidateDelegate);
			this._passes[i].dispose();
		}

		this._passes.length = 0;

		(<IMaterial> this._asset).removeEventListener(MaterialEvent.INVALIDATE_TEXTURES, this._onInvalidateTexturesDelegate);
		(<IMaterial> this._asset).removeEventListener(MaterialEvent.INVALIDATE_PASSES, this._onInvalidatePassesDelegate);

		this._animationSet = null;
		this._stage = null;
		this._owners = null;

		this._invalidAnimation = true;
		this._invalidRender = true;
		this._invalidImages = true;

		this._imageIndices = {};
		this._usesAnimation = false;

		this._activePass = null;
		this.images = [];
		this.samplers = [];

		super.onClear(event);
	}

	/**
     *
     */
	public onInvalidatePasses(event: MaterialEvent): void {
		const len: number = this._passes.length;
		for (let i: number = 0; i < len; i++)
			this._passes[i].invalidate();

		this._invalidAnimation = true;
		this._invalidImages = true;
	}

	/**
     * Listener for when a pass's shader code changes. It recalculates the render order id.
     */
	private onPassInvalidate(event: PassEvent): void {
		this._invalidAnimation = true;
	}

	/**
     *
     */
	public onInvalidateTextures(event: MaterialEvent): void {
		this._owners.forEach((asset: _Render_RenderableBase) => asset._onInvalidateStyle());
	}

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

		this._invalidRender = true;
		this._invalidAnimation = true;

		//prevent infinite loop with cacheRenderer invalidation
		if (<CacheRenderer> this.renderElements.renderer != <IMaterial> this._asset)
			(<CacheRenderer> this.renderElements.renderer).invalidate();
	}

	/**
     * Removes all passes from the surface
     */
	public _pClearPasses(): void {
		const len: number = this._passes.length;
		for (let i: number = 0; i < len; ++i)
			this._passes[i].removeEventListener(PassEvent.INVALIDATE, this._onPassInvalidateDelegate);

		this._passes.length = 0;
	}

	/**
     * Adds a pass to the surface
     * @param pass
     */
	public _pAddPass(pass: IPass): void {
		this._passes.push(pass);
		pass.addEventListener(PassEvent.INVALIDATE, this._onPassInvalidateDelegate);
	}

	/**
     * Removes a pass from the surface.
     * @param pass The pass to be removed.
     */
	public _pRemovePass(pass: IPass): void {
		pass.removeEventListener(PassEvent.INVALIDATE, this._onPassInvalidateDelegate);
		this._passes.splice(this._passes.indexOf(pass), 1);
	}

	/**
	 * Performs any processing that needs to occur before any of its passes are used.
	 *
	 * @private
	 */
	public _pUpdateRender(): void {
		if (this._invalidImages)
			this._updateImages();

		this._invalidRender = false;
	}

	/**
     *
     * @param surface
     */
	protected _updateAnimation(): void {
		// if (this._invalidRender)
		// 	this._pUpdateRender();

		this._invalidAnimation = false;

		const usesAnimation: boolean = this._getEnabledGPUAnimation();

		let renderOrderId = 0;
		let mult: number = 1;
		let shader: ShaderBase;
		const len: number = this._passes.length;
		for (let i: number = 0; i < len; i++) {
			shader = <ShaderBase> this._passes[i].shader;
			shader.usesAnimation = usesAnimation;

			// this is getter, this enforce update programData
			// renderOrderId += shader.programData.id * mult;
			if ((<any>shader)._programData) {
				renderOrderId += (<any>shader)._programData.id * mult;
			} else {
				renderOrderId += shader.programData.id * mult;
			}

			mult *= 1000;
		}

		if (this._usesAnimation != usesAnimation) {
			this._usesAnimation = usesAnimation;

			this._owners.forEach((asset: _Render_RenderableBase) => asset._onInvalidateElements());
		}

		this._renderOrderId = renderOrderId;
	}

	private _updateImages(): void {
		this._invalidImages = false;

		const style: Style = (<IMaterial> this._asset).style;
		const numTextures: number = (<IMaterial> this._asset).getNumTextures();
		let texture: ITexture;
		let numImages: number;
		let images: Array<number>;
		let asset: ImageBase;
		let index: number = 0;

		for (let i: number = 0; i < numTextures; i++) {
			texture = (<IMaterial> this._asset).getTextureAt(i);
			numImages = texture.getNumImages();
			images = this._imageIndices[texture.id] = new Array<number>();
			for (let j: number = 0; j < numImages; j++) {
				asset = texture.getImageAt(j) || style?.getImageAt(texture, j) || ImageUtils.getDefaultImage2D();
				this.images[index] = asset.getAbstraction<_Stage_ImageBase>(this._stage);

				this.samplers[index] = texture.getSamplerAt(j)
					|| style?.getSamplerAt(texture, j)
					|| ImageUtils.getDefaultImageSampler();

				images[j] = index++;
			}
		}

		this._numImages = index;
	}

	/**
     * test if animation will be able to run on gpu BEFORE compiling materials
     * test if the shader objects supports animating the animation set in the vertex shader
     * if any object using this material fails to support accelerated animations for any of the shader objects,
     * we should do everything on cpu (otherwise we have the cost of both gpu + cpu animations)
     */
	private _getEnabledGPUAnimation(): boolean {
		if (this._animationSet) {
			this._animationSet.resetGPUCompatibility();

			const len: number = this._passes.length;
			let shader: ShaderBase;
			for (let i: number = 0; i < len; i++) {
				shader = <ShaderBase> this._passes[i].shader;
				shader.usesAnimation = false;
				this._owners.forEach((asset: _Render_RenderableBase) => {
					if ((<IRenderContainer> (<RenderEntity> asset.entity).node.container).animator)
						(<IRenderContainer> (<RenderEntity> asset.entity).node.container).animator.testGPUCompatibility(shader);
				});
			}

			return !this._animationSet.usesCPU;
		}

		return false;
	}
}