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

import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code'
import { Context } from './context';
import { getUniformUpdaters, getTextureUniformUpdaters, UniformValues } from './uniform';
import { AttributeBuffers } from './buffer';
import { TextureId, Textures } from './texture';
import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache';
import { idFactory } from 'mol-util/id-factory';
import { RenderableSchema } from '../renderable/schema';
import { hashFnv32a, hashString } from 'mol-data/util';

const getNextProgramId = idFactory()

export interface Program {
    readonly id: number

    use: () => void
    setUniforms: (uniformValues: UniformValues) => void
    bindAttributes: (attribueBuffers: AttributeBuffers) => void
    bindTextures: (textures: Textures) => void

    destroy: () => void
}

type AttributeLocations = { [k: string]: number }

function getAttributeLocations(ctx: Context, program: WebGLProgram, schema: RenderableSchema) {
    const { gl } = ctx
    const locations: AttributeLocations = {}
    gl.useProgram(program)
    Object.keys(schema).forEach(k => {
        const spec = schema[k]
        if (spec.type === 'attribute') {
            const loc = gl.getAttribLocation(program, k)
            // if (loc === -1) {
            //     console.info(`Could not get attribute location for '${k}'`)
            // }
            locations[k] = loc
        }
    })
    return locations
}

export interface ProgramProps {
    defineValues: DefineValues,
    shaderCode: ShaderCode,
    schema: RenderableSchema
}

export function createProgram(ctx: Context, props: ProgramProps): Program {
    const { gl, shaderCache } = ctx
    const { defineValues, shaderCode: _shaderCode, schema } = props

    const program = gl.createProgram()
    if (program === null) {
        throw new Error('Could not create WebGL program')
    }

    const shaderCode = addShaderDefines(ctx, defineValues, _shaderCode)
    const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert })
    const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag })

    vertShaderRef.value.attach(program)
    fragShaderRef.value.attach(program)
    gl.linkProgram(program)

    const uniformUpdaters = getUniformUpdaters(ctx, program, schema)
    const attributeLocations = getAttributeLocations(ctx, program, schema)
    const textureUniformUpdaters = getTextureUniformUpdaters(ctx, program, schema)

    let destroyed = false

    return {
        id: getNextProgramId(),

        use: () => {
            Object.keys(uniformUpdaters).forEach(k => uniformUpdaters[k].clear())
            Object.keys(textureUniformUpdaters).forEach(k => textureUniformUpdaters[k].clear())
            gl.useProgram(program)
        },
        setUniforms: (uniformValues: UniformValues) => {
            Object.keys(uniformValues).forEach(k => {
                const uv = uniformValues[k]
                if (uv !== undefined) uniformUpdaters[k].set(uv.ref.value)
            })
        },
        bindAttributes: (attribueBuffers: AttributeBuffers) => {
            Object.keys(attribueBuffers).forEach(k => {
                const loc = attributeLocations[k]
                if (loc !== -1) attribueBuffers[k].bind(loc)
            })
        },
        bindTextures: (textures: Textures) => {
            Object.keys(textures).forEach((k, i) => {
                textures[k].bind(i as TextureId)
                textureUniformUpdaters[k].set(i)
            })
        },

        destroy: () => {
            if (destroyed) return
            vertShaderRef.free()
            fragShaderRef.free()
            gl.deleteProgram(program)
            destroyed = true
        }
    }
}

export type ProgramCache = ReferenceCache<Program, ProgramProps, Context>

function defineValueHash(v: boolean | number | string): number {
    return typeof v === 'boolean' ? (v ? 1 : 0) :
        typeof v === 'number' ? v : hashString(v)
}

export function createProgramCache(): ProgramCache {
    return createReferenceCache(
        (props: ProgramProps) => {
            const array = [ props.shaderCode.id ]
            Object.keys(props.defineValues).forEach(k => {
                const v = props.defineValues[k].ref.value
                array.push(hashString(k), defineValueHash(v))
            })
            return hashFnv32a(array).toString()
        },
        (ctx: Context, props: ProgramProps) => createProgram(ctx, props),
        (program: Program) => { program.destroy() }
    )
}