/**
 * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */

import { VolumeData } from 'mol-model/volume'
import { RuntimeContext } from 'mol-task'
import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
import { createDirectVolumeRenderObject } from 'mol-gl/render-object';
import { EmptyLoci } from 'mol-model/loci';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { Vec3, Mat4 } from 'mol-math/linear-algebra';
import { Box3D } from 'mol-math/geometry';
import { WebGLContext } from 'mol-gl/webgl/context';
import { createTexture } from 'mol-gl/webgl/texture';
import { LocationIterator } from 'mol-geo/util/location-iterator';
import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
import { Geometry, createRenderableState } from 'mol-geo/geometry/geometry';
import { VisualUpdateState } from 'mol-repr/util';
import { VisualContext, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
import { Theme, ThemeRegistryContext } from 'mol-theme/theme';

function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
    const bbox = Box3D.empty()
    Box3D.add(bbox, gridDimension)
    Box3D.transform(bbox, bbox, transform)
    return bbox
}

// 2d volume texture

function getVolumeTexture2dLayout(dim: Vec3, maxTextureSize: number) {
    let width = 0
    let height = dim[1]
    let rows = 1
    let columns = dim[0]
    if (maxTextureSize < dim[0] * dim[2]) {
        columns =  Math.floor(maxTextureSize / dim[0])
        rows = Math.ceil(dim[2] / columns)
        width = columns * dim[0]
        height *= rows
    } else {
        width = dim[0] * dim[2]
    }
    width += columns // horizontal padding
    height += rows // vertical padding
    return { width, height, columns, rows }
}

function createVolumeTexture2d(volume: VolumeData, maxTextureSize: number) {
    const { data: tensor, dataStats: stats } = volume
    const { space, data } = tensor
    const dim = space.dimensions as Vec3
    const { get } = space
    const { width, height, columns, rows } = getVolumeTexture2dLayout(dim, maxTextureSize)

    const array = new Uint8Array(width * height * 4)
    const textureImage = { array, width, height }

    const [ xl, yl, zl ] = dim
    const xlp = xl + 1 // horizontal padding
    const ylp = yl + 1 // vertical padding

    function setTex(value: number, x: number, y: number, z: number) {
        const column = Math.floor(((z * xlp) % width) / xlp)
        const row = Math.floor((z * xlp) / width)
        const px = column * xlp + x
        const index = 4 * ((row * ylp * width) + (y * width) + px)
        array[index + 3] = ((value - stats.min) / (stats.max - stats.min)) * 255
    }

    console.log('dim', dim)
    console.log('layout', { width, height, columns, rows })

    for (let z = 0; z < zl; ++z) {
        for (let y = 0; y < yl; ++y) {
            for (let x = 0; x < xl; ++x) {
                setTex(get(data, x, y, z), x, y, z)
            }
        }
    }

    return textureImage
}

export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: VolumeData, directVolume?: DirectVolume) {
    const gridDimension = volume.data.space.dimensions as Vec3
    const textureImage = createVolumeTexture2d(volume, webgl.maxTextureSize)
    // debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3)
    const transform = VolumeData.getGridToCartesianTransform(volume)
    const bbox = getBoundingBox(gridDimension, transform)
    const dim = Vec3.create(gridDimension[0], gridDimension[1], gridDimension[2])
    dim[0] += 1 // horizontal padding
    dim[0] += 1 // vertical padding

    const texture = directVolume ? directVolume.gridTexture.ref.value : createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'linear')
    texture.load(textureImage)

    return DirectVolume.create(bbox, dim, transform, texture, directVolume)
}

// 3d volume texture

function createVolumeTexture3d(volume: VolumeData) {
    const { data: tensor, dataStats: stats } = volume
    const { space, data } = tensor
    const [ width, height, depth ] = space.dimensions as Vec3
    const { get } = space

    const array = new Uint8Array(width * height * depth * 4)
    const textureVolume = { array, width, height, depth }

    let i = 0
    for (let z = 0; z < depth; ++z) {
        for (let y = 0; y < height; ++y) {
            for (let x = 0; x < width; ++x) {
                if (i < 100) {
                    console.log(get(data, x, y, z), ((get(data, x, y, z) - stats.min) / (stats.max - stats.min)) * 255)
                }
                array[i + 3] = ((get(data, x, y, z) - stats.min) / (stats.max - stats.min)) * 255
                i += 4
            }
        }
    }

    return textureVolume
}

export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: VolumeData, directVolume?: DirectVolume) {
    const gridDimension = volume.data.space.dimensions as Vec3
    const textureVolume = createVolumeTexture3d(volume)
    const transform = VolumeData.getGridToCartesianTransform(volume)
    // Mat4.invert(transform, transform)
    const bbox = getBoundingBox(gridDimension, transform)

    const texture = directVolume ? directVolume.gridTexture.ref.value : createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'linear')
    texture.load(textureVolume)

    return DirectVolume.create(bbox, gridDimension, transform, texture, directVolume)
}

//

export async function createDirectVolume(ctx: VisualContext, volume: VolumeData, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
    const { runtime, webgl } = ctx
    if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props')

    return webgl.isWebGL2 ?
        await createDirectVolume3d(runtime, webgl, volume, directVolume) :
        await createDirectVolume2d(runtime, webgl, volume, directVolume)
}


//

export const DirectVolumeParams = {
    ...Geometry.Params,
    ...DirectVolume.Params
}
export type DirectVolumeParams = typeof DirectVolumeParams
export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: VolumeData) {
    return PD.clone(DirectVolumeParams)
}

export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
    return VolumeVisual<DirectVolumeParams>({
        defaultProps: PD.getDefaultValues(DirectVolumeParams),
        createGeometry: createDirectVolume,
        getLoci: () => EmptyLoci,
        mark: () => false,
        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
        },
        createRenderObject: async (ctx: VisualContext, geometry: DirectVolume, locationIt: LocationIterator, theme: Theme, props: PD.Values<DirectVolumeParams>) => {
            const transform = createIdentityTransform()
            const values = await DirectVolume.createValues(ctx.runtime, geometry, transform, locationIt, theme, props)
            const state = createRenderableState(props)
            return createDirectVolumeRenderObject(values, state)
        },
        updateValues: DirectVolume.updateValues,
        updateBoundingSphere: DirectVolume.updateBoundingSphere
    })
}

export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> {
    return VolumeRepresentation('Direct Volume', ctx, getParams, DirectVolumeVisual)
}

export const DirectVolumeRepresentationProvider: VolumeRepresentationProvider<DirectVolumeParams> = {
    label: 'Direct Volume',
    description: 'Direct volume rendering of volumetric data.',
    factory: DirectVolumeRepresentation,
    getParams: getDirectVolumeParams,
    defaultValues: PD.getDefaultValues(DirectVolumeParams)
}