/**
 * Allows to create a shader with GLSL nodes to create the texture values.
 *
 *
 */
import {
	Camera,
	Mesh,
	PlaneGeometry,
	WebGLRenderer,
	WebGLRenderTarget,
	ShaderMaterial,
	Scene,
	ClampToEdgeWrapping,
	RGBAFormat,
	LinearFilter,
	NearestFilter,
	NoToneMapping,
	WebGLArrayRenderTarget,
	NoColorSpace,
	ColorSpace,
	ToneMapping,
	OrthographicCamera,
} from 'three';
import {Number2} from '../../../types/GlobalTypes';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {TypedCopNode} from './_Base';
import {GlobalsGeometryHandler} from '../gl/code/globals/Geometry';
import {GlNodeChildrenMap} from '../../poly/registers/nodes/Gl';
import {BaseGlNodeType} from '../gl/_Base';
import {GlNodeFinder} from '../gl/code/utils/NodeFinder';
import {NodeContext} from '../../poly/NodeContext';
import {IUniforms} from '../../../core/geometry/Material';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {DataTextureController} from './utils/DataTextureController';
import {CopRendererController} from './utils/RendererController';
import {AssemblerName} from '../../poly/registers/assemblers/_BaseRegister';
import {Poly} from '../../Poly';
import {TexturePersistedConfig} from '../gl/code/assemblers/textures/TexturePersistedConfig';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';
import {BaseNodeType} from '../_Base';
import {CopType} from '../../poly/registers/nodes/types/Cop';
import {TextureParamsController, TextureParamConfig} from './utils/TextureParamsController';
import {isBooleanTrue} from '../../../core/Type';
import FRAGMENT_SHADER from '../gl/code/templates/textures/Default.frag.glsl';
import VERTEX_SHADER from '../gl/code/templates/textures/Default.vert.glsl';
import {handleCopBuilderDependencies} from './utils/BuilderUtils';
import {GlAssemblerController} from '../gl/code/Controller';
import {ShaderAssemblerTexture2DArray} from '../gl/code/assemblers/textures/Texture2DArray';

const RESOLUTION_DEFAULT: Number2 = [128, 128];

function Builder2DArrayCopParamConfig<TBase extends Constructor>(Base: TBase) {
	return class Mixin extends Base {
		/** @param textures resolution */
		resolution = ParamConfig.VECTOR2(RESOLUTION_DEFAULT);
		/** @param layers */
		layers = ParamConfig.INTEGER(4, {
			range: [2, 32],
			rangeLocked: [true, false],
		});
		/** @param use the main camera renderer. This can save memory, but can also lead to colors being affected by the renderer's output color space */
		useCameraRenderer = ParamConfig.BOOLEAN(1, {
			callback: (node: BaseNodeType) => {
				Builder2DArrayCopNode.PARAM_CALLBACK_render(node as Builder2DArrayCopNode);
			},
		});
		/** @param use a data texture instead of a render target, which can be useful when using that texture as and envMap */
		// useDataTexture = ParamConfig.BOOLEAN(0);
		/** @param force Render */
		render = ParamConfig.BUTTON(null, {
			callback: (node: BaseNodeType) => {
				Builder2DArrayCopNode.PARAM_CALLBACK_render(node as Builder2DArrayCopNode);
			},
		});
	};
}
class Builder2DArrayCopParamsConfig extends TextureParamConfig(Builder2DArrayCopParamConfig(NodeParamsConfig)) {}

const ParamsConfig = new Builder2DArrayCopParamsConfig();

export class Builder2DArrayCopNode extends TypedCopNode<Builder2DArrayCopParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return CopType.BUILDER_2D_ARRAY;
	}
	override readonly persisted_config: TexturePersistedConfig = new TexturePersistedConfig(this);
	protected _assemblerController = this._createAssemblerController();

	public override usedAssembler(): Readonly<AssemblerName.GL_TEXTURE_2D_ARRAY> {
		return AssemblerName.GL_TEXTURE_2D_ARRAY;
	}
	protected _createAssemblerController(): GlAssemblerController<ShaderAssemblerTexture2DArray> | undefined {
		const assemblerController: GlAssemblerController<ShaderAssemblerTexture2DArray> | undefined =
			Poly.assemblersRegister.assembler(this, this.usedAssembler());
		if (assemblerController) {
			const globalsHandler = new GlobalsGeometryHandler();
			assemblerController.setAssemblerGlobalsHandler(globalsHandler);
			return assemblerController;
		}
	}

	assemblerController() {
		return this._assemblerController;
	}

	private _textureMesh: Mesh = new Mesh(new PlaneGeometry(2, 2));
	private _fragmentShader: string | undefined;
	private _uniforms: IUniforms | undefined;
	public readonly textureMaterial: ShaderMaterial = new ShaderMaterial({
		uniforms: {},
		vertexShader: VERTEX_SHADER,
		fragmentShader: FRAGMENT_SHADER,
	});
	private _textureScene: Scene = new Scene();
	private _textureCamera: Camera = new OrthographicCamera();
	private _renderTarget: WebGLArrayRenderTarget | undefined;
	private _dataTextureController: DataTextureController | undefined;
	private _rendererController: CopRendererController | undefined;

	public readonly textureParamsController: TextureParamsController = new TextureParamsController(this);
	protected override _childrenControllerContext = NodeContext.GL;
	override initializeNode() {
		this._textureMesh.material = this.textureMaterial;
		this._textureMesh.scale.multiplyScalar(0.25);
		this._textureScene.add(this._textureMesh);
		this._textureCamera.position.z = 1;

		// this ensures the builder recooks when its children are changed
		// and not just when a material that use it requests it
		this.addPostDirtyHook('_cook_main_without_inputs_when_dirty', () => {
			setTimeout(this._cook_main_without_inputs_when_dirty_bound, 0);
		});

		// this.dirtyController.addPostDirtyHook(
		// 	'_reset_if_resolution_changed',
		// 	this._reset_if_resolution_changed.bind(this)
		// );
		// this.params.onParamsCreated('reset', () => {
		// 	this._reset();
		// });
	}

	override createNode<S extends keyof GlNodeChildrenMap>(
		node_class: S,
		options?: NodeCreateOptions
	): GlNodeChildrenMap[S];
	override createNode<K extends valueof<GlNodeChildrenMap>>(
		node_class: Constructor<K>,
		options?: NodeCreateOptions
	): K;
	override createNode<K extends valueof<GlNodeChildrenMap>>(
		node_class: Constructor<K>,
		options?: NodeCreateOptions
	): K {
		return super.createNode(node_class, options) as K;
	}
	override children() {
		return super.children() as BaseGlNodeType[];
	}
	override nodesByType<K extends keyof GlNodeChildrenMap>(type: K): GlNodeChildrenMap[K][] {
		return super.nodesByType(type) as GlNodeChildrenMap[K][];
	}
	override childrenAllowed() {
		if (this.assemblerController()) {
			return super.childrenAllowed();
		}
		return false;
	}
	override sceneReadonly() {
		return this.assemblerController() == null;
	}

	private _cook_main_without_inputs_when_dirty_bound = this._cook_main_without_inputs_when_dirty.bind(this);
	private async _cook_main_without_inputs_when_dirty() {
		await this.cookController.cookMainWithoutInputs();
	}

	// private _reset_if_resolution_changed(trigger?: CoreGraphNode) {
	// 	if (trigger && trigger.graphNodeId() == this.p.resolution.graphNodeId()) {
	// 		this._reset();
	// 	}
	// }

	override async cook() {
		this.compileIfRequired();
		await this._renderOnTarget(true);
	}

	shaders_by_name() {
		return {
			fragment: this._fragmentShader,
		};
	}

	compileIfRequired() {
		if (this.assemblerController()?.compileRequired()) {
			try {
				this.compile();
			} catch (err) {
				const message = (err as any).message || 'failed to compile';
				this.states.error.set(message);
			}
		}
	}
	private compile() {
		const assemblerController = this.assemblerController();
		if (!assemblerController) {
			return;
		}
		const outputNodes: BaseGlNodeType[] = GlNodeFinder.findOutputNodes(this);
		if (outputNodes.length == 0) {
			this.states.error.set('one output node is required');
			return;
		}
		if (outputNodes.length > 1) {
			this.states.error.set('only one output node allowed');
			return;
		}
		const outputNode = outputNodes[0];
		if (outputNode) {
			//const param_nodes = GlNodeFinder.find_param_generating_nodes(this);
			const rootNodes = outputNodes; //.concat(param_nodes);
			assemblerController.assembler.set_root_nodes(rootNodes);

			// main compilation
			assemblerController.assembler.updateFragmentShader();

			// receives fragment and uniforms
			const fragmentShader = assemblerController.assembler.fragment_shader();
			const uniforms = assemblerController.assembler.uniforms();
			if (fragmentShader && uniforms) {
				this._fragmentShader = fragmentShader;
				this._uniforms = uniforms;
			}

			handleCopBuilderDependencies({
				node: this,
				timeDependent: assemblerController.assembler.uniformsTimeDependent(),
				uniforms: undefined,
			});
		}
		if (this._fragmentShader && this._uniforms) {
			this.textureMaterial.fragmentShader = this._fragmentShader;
			this.textureMaterial.uniforms = this._uniforms;
			this.textureMaterial.needsUpdate = true;
			this.textureMaterial.uniforms.resolution = {
				value: this.pv.resolution,
			};
		}
		assemblerController.post_compile();
	}

	callbackName() {
		return `cop/builder3D_${this.graphNodeId()}`;
	}
	// private _uniformCallbackName() {
	// 	return `cop/builder_uniforms_${this.graphNodeId()}`;
	// }
	override dispose() {
		super.dispose();
		this._renderTarget?.dispose();
		this._renderer?.dispose();
		this.removeCallbacks();
	}
	public removeCallbacks() {
		const scene = this.scene();
		// scene.uniformsController.removeTimeUniform(uniforms);
		scene.unRegisterOnBeforeTick(this.callbackName());
	}

	//
	//
	// RENDER + RENDER TARGET
	//
	//
	public readonly boundRenderOnTarget = this.renderOnTargetWithoutUpdatingTextureFromParams.bind(this);
	async renderOnTargetWithoutUpdatingTextureFromParams() {
		this._renderOnTarget(false);
	}
	private async _renderOnTarget(updateTextureFromParams: boolean) {
		await this.createRenderTargetIfRequired();
		await this._createRendererIfRequired();

		if (this.states.error.active()) {
			return;
		}
		if (!this._renderer) {
			console.warn('no renderer');
			return;
		}
		if (!this._uniforms) {
			return;
		}

		this._saveRendererState(this._renderer);
		this._prepareRenderer(this._renderer);
		const layersCount = this.pv.layers;
		for (let i = 0; i < layersCount; i++) {
			this._uniforms.uLayer.value = i;
			this._setRenderLayer(this._renderer, i);
			this._renderer.render(this._textureScene, this._textureCamera);
		}
		await this._postRender(updateTextureFromParams);
		this._restoreRendererState(this._renderer);
	}
	private async _postRender(updateTextureFromParams: boolean) {
		if (this._renderTarget?.texture) {
			// if (isBooleanTrue(this.pv.useDataTexture) && this._renderTarget && this._renderer) {
			// 	this._dataTextureController = this._dataTextureController || new DataTextureController();
			// 	const texture = this._dataTextureController.fromRenderTarget(this._renderer, this._renderTarget);
			// 	if (updateTextureFromParams) {
			// 		await this.textureParamsController.update(texture);
			// 	}
			// 	this.setTexture(texture);
			// } else {
			const texture = this._renderTarget.texture;
			if (updateTextureFromParams) {
				// await this.textureParamsController.update(texture);
			}
			this.setTexture(texture);
			// }
		} else {
			this.cookController.endCook();
		}
	}
	private _prevTarget: WebGLRenderTarget | null = null;
	private _prevOutputColorSpace: ColorSpace = NoColorSpace;
	private _prevToneMapping: ToneMapping = NoToneMapping;
	private _saveRendererState(renderer: WebGLRenderer) {
		this._prevTarget = renderer.getRenderTarget();
		this._prevOutputColorSpace = renderer.outputColorSpace;
		this._prevToneMapping = renderer.toneMapping;
	}
	private _prepareRenderer(renderer: WebGLRenderer) {
		if (!this._renderTarget) {
			console.warn('no render target');
			return;
		}
		renderer.outputColorSpace = NoColorSpace;
		renderer.toneMapping = NoToneMapping;
	}
	private _setRenderLayer(renderer: WebGLRenderer, layer: number) {
		if (!this._renderTarget) {
			console.warn('no render target');
			return;
		}
		renderer.setRenderTarget(this._renderTarget, layer);
		renderer.clear();
	}
	private _restoreRendererState(renderer: WebGLRenderer) {
		renderer.setRenderTarget(this._prevTarget);
		renderer.outputColorSpace = this._prevOutputColorSpace;
		renderer.toneMapping = this._prevToneMapping;
	}
	/*
	 *
	 * RENDERER
	 *
	 */
	private _renderer: WebGLRenderer | undefined;
	private async _createRendererIfRequired() {
		if (this._renderer) {
			return;
		}
		if (isBooleanTrue(this.pv.useCameraRenderer)) {
			this._rendererController = this._rendererController || new CopRendererController(this);
			const foundRenderer = await this._rendererController.waitForRenderer();
			if (foundRenderer instanceof WebGLRenderer) {
				this._renderer = foundRenderer;
			} else {
				console.warn('found renderer is not a WebGLRenderer');
			}
		} else {
			this._renderer = Poly.renderersController.linearRenderer();
		}
	}
	private _resetRenderer() {
		this._renderer = undefined;
	}
	renderer() {
		return this._renderer;
	}
	/*
	 *
	 * RENDER TARGET
	 *
	 */
	async renderTarget() {
		return (this._renderTarget =
			this._renderTarget ||
			(await this._createRenderTarget(this.pv.resolution.x, this.pv.resolution.y, this.pv.layers)));
	}
	private async createRenderTargetIfRequired() {
		if (!this._renderTarget || !this._renderTargetResolutionValid()) {
			this._renderTarget = await this._createRenderTarget(
				this.pv.resolution.x,
				this.pv.resolution.y,
				this.pv.layers
			);
			this._dataTextureController?.reset();
		}
	}
	private _renderTargetResolutionValid() {
		if (this._renderTarget) {
			const image = this._renderTarget.texture.image;
			if (image.width != this.pv.resolution.x || image.height != this.pv.resolution.y) {
				return false;
			} else {
				return true;
			}
		} else {
			return false;
		}
	}

	private async _createRenderTarget(width: number, height: number, depth: number) {
		if (this._renderTarget) {
			const image = this._renderTarget.texture.image;
			console.log(image);
			if (image.width == width && image.height == height && image.depth == depth) {
				return this._renderTarget;
			}
		}

		const wrapS = ClampToEdgeWrapping;
		const wrapT = ClampToEdgeWrapping;

		const minFilter = LinearFilter;
		const magFilter = NearestFilter;

		const renderTarget = new WebGLArrayRenderTarget(width, height, depth);
		renderTarget.texture.wrapS = wrapS;
		renderTarget.texture.wrapT = wrapT;
		renderTarget.texture.minFilter = minFilter;
		renderTarget.texture.magFilter = magFilter;
		renderTarget.texture.format = RGBAFormat;
		renderTarget.stencilBuffer = false;
		renderTarget.depthBuffer = false;

		// await this.textureParamsController.update(renderTarget.texture);
		Poly.warn(`${this.path()}: created WebGLArrayRenderTarget`, this.path(), width, height, depth);
		return renderTarget;
	}

	/*
	 *
	 * CALLBACK
	 *
	 */
	static PARAM_CALLBACK_render(node: Builder2DArrayCopNode) {
		node._renderOnTarget(true);
	}
	static PARAM_CALLBACK_resetRenderer(node: Builder2DArrayCopNode) {
		node._resetRenderer();
	}
}
