import {
    DoubleSide,
    GLSL3,
    LinearFilter,
    Mesh as ThreeMesh,
    OrthographicCamera,
    RedIntegerFormat,
    RGBAIntegerFormat,
    RGIntegerFormat,
    Scene,
    ShaderMaterial,
    WebGLRenderTarget
} from "three";
import { assert } from "../../../../../core/assert.js";
import { UINT32_MAX } from "../../../../../core/binary/UINT32_MAX.js";
import { CanvasView } from "../../../../../view/elements/CanvasView.js";
import { FULL_SCREEN_TRIANGLE_GEOMETRY } from "../../../geometry/FULL_SCREEN_TRIANGLE_GEOMETRY.js";
import { glsl_gen_swizzled_read } from "../../../shaders/glsl_gen_swizzled_read.js";
import CheckersTexture from "../../../texture/CheckersTexture.js";
import { formatToChannelCount } from "../../../texture/formatToChannelCount.js";
import { GL_RGBIntegerFormat } from "../../../texture/GL_RGBIntegerFormat.js";
import { Sampler2D } from "../../../texture/sampler/Sampler2D.js";


/**
 *
 * @param {THREE.Texture} texture
 * @returns {string}
 */
function defineSamplerType(texture) {
    switch (texture.format) {
        case RedIntegerFormat:
        case RGIntegerFormat:
        case GL_RGBIntegerFormat:
        case RGBAIntegerFormat:
            return 'usampler2D';
        default:
            return 'sampler2D'
    }
}

/**
 *
 * @param {THREE.Texture} texture
 * @param {string} name
 * @param {string} uv
 * @returns {string}
 */
function sampleTexel(texture, name, uv) {

    const texture_sample = `texture2D(${name}, ${uv})`;

    switch (texture.format) {
        case RedIntegerFormat:
            return `vec4(${texture_sample})`;
        default:
            return texture_sample;
    }
}

/**
 *
 * @param {THREE.Texture} texture
 * @param {string} sample
 * @retunrs {string}
 */
function scaleSample(texture, sample) {
    switch (texture.internalFormat) {
        case "R32UI":
            return `${sample} * ${1 / UINT32_MAX}`
        default:
            return sample;
    }
}

/**
 *
 * @param {THREE.Texture} texture
 * @param {number} [alpha_override] will force specified alpha value in output, useful for cases where alpha would be 0 otherwise
 * @param {(string|number)[]} swizzle allows us to re-arrange pixels as they are being written out
 */
function makeShader({ texture, alpha_override, swizzle }) {
    let _swizzle = swizzle.slice();

    const texture_channel_count = formatToChannelCount(texture.format);

    if (texture_channel_count < 4) {
        // no override specified, and alpha would be 0, force it to one to make image visible
        _swizzle[3] = 1;
    }

    return new ShaderMaterial({

        vertexShader: /* glsl */`

		varying vec2 vUv;

		void main() {

			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

		}`,

        fragmentShader: /* glsl */`

        precision mediump usampler2D;
		uniform ${defineSamplerType(texture)} tDiffuse;

		varying vec2 vUv;
		out vec4 out_frag;

		void main() {

			vec4 texel = ${scaleSample(texture, sampleTexel(texture, 'tDiffuse', 'vUv'))};
			out_frag = ${glsl_gen_swizzled_read('texel', _swizzle)};
		}`,
        uniforms: {

            'tDiffuse': { value: null }

        },
        glslVersion: GLSL3
    });
}


/**
 *
 * @param {number} height
 * @param {number} width
 * @param {THREE.Texture} texture
 * @param {THREE.WebGLRenderer} renderer
 * @param {boolean} [flipY]
 * @param {(string|number)[]} [swizzle]
 * @param {Sampler2D} [destination]
 * @return {{destination: Sampler2D, render: function}}
 */
export function renderSamplerFromTexture({

                                             texture = CheckersTexture.create(),
                                             width = texture.image.width,
                                             height = texture.image.height,
                                             renderer,
                                             flipY = true,
                                             swizzle = ['r', 'g', 'b', 'a'],
                                             destination = Sampler2D.uint8clamped(4, width, height)
                                         }) {
    assert.isArray(swizzle, 'swizzle');

    let copyShader = null;

    const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
    const scene = new Scene();
    scene.add(camera);

    const quad = new ThreeMesh(FULL_SCREEN_TRIANGLE_GEOMETRY, null);

    if (flipY) {
        quad.scale.y = -1;
    }

    scene.add(quad);

    const gl = renderer.getContext();

    const renderTargetOptions = {
        generateMipmaps: false,
        minFilter: LinearFilter,
        stencilBuffer: false,
        depthBuffer: false
    };
    const renderTarget = new WebGLRenderTarget(width, height, renderTargetOptions);

    let __texture_cached = texture;

    /**
     *
     * @return {ShaderMaterial}
     */
    function getShader() {
        if (__texture_cached !== texture || copyShader === null) {
            copyShader = makeShader({ texture: texture, swizzle });

            copyShader.side = DoubleSide;
            copyShader.uniforms.tDiffuse.value = texture;

            quad.material = copyShader;

            __texture_cached = texture;
        }

        return copyShader;
    }

    const render = () => {
        const shader = getShader();

        shader.uniforms.tDiffuse.value = texture;
        shader.uniformsNeedUpdate = true;

        const _rt = renderer.getRenderTarget();

        renderer.setRenderTarget(renderTarget);

        renderer.render(scene, camera);

        destination.resize(width, height);

        gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, destination.data);

        renderer.setRenderTarget(_rt);
    };

    return {
        destination,
        render,
        set texture(v) {
            if (v !== texture) {

                texture = v;

                getShader().uniforms.tDiffuse.value = v;
            }
        }
    }
}

/**
 *
 * @param {number} height
 * @param {number} width
 * @param {THREE.Texture} texture
 * @param {THREE.WebGLRenderer} renderer
 * @param {boolean} [flipY]
 * @param {(string|number)[]} [swizzle]
 * @return {{view: View, render: function}}
 */
export function buildCanvasViewFromTexture({
                                               texture,
                                               width = texture.image.width,
                                               height = texture.image.height,
                                               renderer,
                                               flipY = true,
                                               swizzle
                                           }) {

    const canvasView = new CanvasView();
    canvasView.css({
        position: 'absolute',
        left: 0,
        top: 0
    });
    canvasView.size.set(width, height);

    const ctx = canvasView.context2d;

    const imageData = ctx.createImageData(width, height);

    // wrap imageData in Sampler2D
    const sampler = new Sampler2D(imageData.data, 4, width, height);


    const ctrl = renderSamplerFromTexture({
        texture,
        width,
        height,
        flipY,
        renderer,
        swizzle,
        destination: sampler
    });


    function render() {
        ctrl.render();

        ctx.putImageData(imageData, 0, 0);
    }


    return {
        view: canvasView,
        render,
        set texture(v) {
            ctrl.texture = v;
        }
    }
}

