import { Effect, PostProcess, Camera } from '../loader/babylonjs-import';
import { lineShader } from '../loader/CustomBabylonMaterials/shaders/LineShader';
import { EXPENSIVE_NORMALS } from '../loader/ShaderConstants';
import { ShortFrustumDepthRenderer } from '../loader/ShortFrustumDepthRenderer';
import { TwinfinityPostProcess } from './TwinfinityPostProcess';

/**
 * LineShading is a post-process effect that adds an outline to objects in the viewer.
 * Unlike the other line shader, this one uses normals recreated from the depth buffer to be able to find lines where there are geoemtry with opposing normals but too similar depth values
 */

const SHADER_NAME_PREFIX = 'outlineFromDepthNormals';
const DEPTH_SAMPLER = 'depthSampler';
const INVERTED_PROJECTION_MATRIX = 'invertedProjectionMatrix';
const DEPTH_DIMENSIONS = 'depthDimensions';
const NEAR_FAR = 'nearFar';
const MIN_MAX_SEPERATION = 'minMaxSeperation';
const THICKNESS = 'thickness';
const STRENGTH = 'strength';
const TEXTURE_SAMPLER = 'textureSampler';

export class LineShading extends TwinfinityPostProcess {
    private _shortFrustumDepthRenderer: ShortFrustumDepthRenderer;
    private _strength: number;
    private _lineThickness = 1;
    protected _screenWrite: boolean;

    /**
     * Returns the current line thickness value.
     */
    public get lineThickness(): number {
        return this._lineThickness;
    }

    /**
     * Sets the line thickness and updates the shader with the new value.
     * @param thickness - Thickness of the lines, value between 0 and 1.
     */
    public set lineThickness(thickness: number) {
        this._lineThickness = thickness;
        this._lineThickness = Math.min(Math.max(thickness, 0.0), 1.0);
        const effect = this._postProcess?.getEffect();
        if (effect !== undefined) {
            this.updateLineThicknessUniform(effect, this._lineThickness);
        }
    }

    /*
     * Updates the thickness uniform in the fragment shader.
     */
    protected updateLineThicknessUniform(effect: Effect, thickness: number): void {
        effect.setFloat('thickness', thickness);
    }

    /**
     * @param lineThickness Thickness of the lines, value between 0 and 1.
     */
    constructor(lineThickness = 0.3, strength = 0.35) {
        super();
        this._lineThickness = lineThickness;
        this._strength = strength;
    }

    protected initialize(camera: Camera): PostProcess {
        const engine = camera.getEngine();

        if (this._postProcess === undefined) {
            Effect.ShadersStore[SHADER_NAME_PREFIX + 'FragmentShader'] = lineShader.fragment();

            const uniforms = [
                MIN_MAX_SEPERATION,
                THICKNESS,
                STRENGTH,
                INVERTED_PROJECTION_MATRIX,
                DEPTH_DIMENSIONS,
                NEAR_FAR
            ];
            const samplers = [DEPTH_SAMPLER, TEXTURE_SAMPLER];
            const defines: string[] = [];
            defines.push(`#define ${EXPENSIVE_NORMALS}`);

            const newPostProcess = new PostProcess(
                'Normals and depth based outline post-process',
                SHADER_NAME_PREFIX,
                uniforms,
                samplers,
                1 /* ratio */,
                null,
                undefined,
                engine,
                false,
                defines.join('\n')
            );

            const shortFrustumProjectionMatrixState = camera.twinfinity.shortFrustum;

            newPostProcess.onApplyObservable.add((effect) => {
                const viewer = camera.getScene().twinfinity.viewer;
                if (viewer) {
                    this._shortFrustumDepthRenderer = viewer.enableDepthRenderer(true);
                    this.updateLineThicknessUniform(effect, this.lineThickness);
                    effect.setTexture(DEPTH_SAMPLER, this._shortFrustumDepthRenderer.depthRenderer.getDepthMap());

                    effect.setMatrix(
                        INVERTED_PROJECTION_MATRIX,
                        shortFrustumProjectionMatrixState.getInvertedProjectionMatrix()
                    );
                    const depthDimensionsX = 1.0 / engine.getRenderWidth();
                    const depthDimensionsY = 1.0 / engine.getRenderHeight();
                    effect.setFloat2(DEPTH_DIMENSIONS, depthDimensionsX, depthDimensionsY);
                    effect.setFloat2(
                        NEAR_FAR,
                        shortFrustumProjectionMatrixState.depthRenderCameraMinZ,
                        shortFrustumProjectionMatrixState.depthRenderCameraMaxZ
                    );

                    effect.setFloat2(MIN_MAX_SEPERATION, 1.0, 1.0); // Hardcoded to avoid artifacts

                    effect.setFloat(THICKNESS, this.lineThickness);
                    effect.setFloat(STRENGTH, Math.min(this._strength, 1.0));
                } else {
                    throw new Error('No viewer created?');
                }
            });

            return newPostProcess;
        } else return this._postProcess;
    }

    detach(camera: Camera): boolean {
        const detached = super.detach(camera);

        if (detached) {
            this._shortFrustumDepthRenderer.disable();
        }

        return detached;
    }
}
