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

/*
 * This code has been modified from https://github.com/regl-project/regl-camera,
 * copyright (c) 2016 Mikola Lysenko. MIT License
 */

const isBrowser = typeof window !== 'undefined'

import REGL = require('regl');

import mouseChange, { MouseModifiers } from 'mol-util/mouse-change'
import mouseWheel from 'mol-util/mouse-wheel'
import { defaults } from 'mol-util'
import { Mat4, Vec3 } from 'mol-math/linear-algebra/3d'
import { clamp, damp } from 'mol-math/interpolate'

export interface CameraUniforms {
    projection: Mat4,
}

export interface CameraState {
    center: Vec3,
    theta: number,
    phi: number,
    distance: number,
    eye: Vec3,
    up: Vec3,
    fovy: number,
    near: number,
    far: number,
    noScroll: boolean,
    flipY: boolean,
    dtheta: number,
    dphi: number,
    rotationSpeed: number,
    zoomSpeed: number,
    renderOnDirty: boolean,
    damping: number,
    minDistance: number,
    maxDistance: number,
}

export interface Camera {
    update: (props: any, block: any) => void,
    setState: (newState: CameraState) => void,
    isDirty: () => boolean
}

export namespace Camera {
    export function create (regl: REGL.Regl, element: HTMLElement, props: Partial<CameraState> = {}): Camera {
        const state: CameraState = {
            center: defaults(props.center, Vec3.zero()),
            theta: defaults(props.theta, 0),
            phi: defaults(props.phi, 0),
            distance: Math.log(defaults(props.distance, 10.0)),
            eye: Vec3.zero(),
            up: defaults(props.up, Vec3.create(0, 1, 0)),
            fovy: defaults(props.fovy, Math.PI / 4.0),
            near: defaults(props.near, 0.01),
            far: defaults(props.far, 1000.0),
            noScroll: defaults(props.noScroll, false),
            flipY: defaults(props.flipY, false),
            dtheta: 0,
            dphi: 0,
            rotationSpeed: defaults(props.rotationSpeed, 1),
            zoomSpeed: defaults(props.zoomSpeed, 1),
            renderOnDirty: defaults(props.renderOnDirty, false),
            damping: defaults(props.damping, 0.9),
            minDistance: Math.log(defaults(props.minDistance, 0.1)),
            maxDistance: Math.log(defaults(props.maxDistance, 1000))
        }

        const view = Mat4.identity()
        const projection = Mat4.identity()

        const right = Vec3.create(1, 0, 0)
        const front = Vec3.create(0, 0, 1)

        let dirty = false
        let ddistance = 0

        let prevX = 0
        let prevY = 0

        if (isBrowser) {
            const source = element || regl._gl.canvas

            const getWidth = function () {
                return element ? element.offsetWidth : window.innerWidth
            }

            const getHeight = function () {
                return element ? element.offsetHeight : window.innerHeight
            }

            mouseChange(source, function (buttons: number, x: number, y: number, mods: MouseModifiers) {
                if (buttons & 1) {
                    const dx = (x - prevX) / getWidth()
                    const dy = (y - prevY) / getHeight()

                    state.dtheta += state.rotationSpeed * 4.0 * dx
                    state.dphi += state.rotationSpeed * 4.0 * dy
                    dirty = true;
                }
                prevX = x
                prevY = y
            })

            mouseWheel(source, function (dx: number, dy: number) {
                ddistance += dy / getHeight() * state.zoomSpeed
                dirty = true;
            }, state.noScroll)
        }

        function dampAndMarkDirty (x: number) {
            const xd = damp(x, state.damping)
            if (Math.abs(xd) < 0.1) return 0
            dirty = true;
            return xd
        }

        function setState (newState: Partial<CameraState> = {}) {
            Object.assign(state, newState)

            const { center, eye, up, dtheta, dphi } = state

            state.theta += dtheta
            state.phi = clamp(state.phi + dphi, -Math.PI / 2.0, Math.PI / 2.0)
            state.distance = clamp(state.distance + ddistance, state.minDistance, state.maxDistance)

            state.dtheta = dampAndMarkDirty(dtheta)
            state.dphi = dampAndMarkDirty(dphi)
            ddistance = dampAndMarkDirty(ddistance)

            const theta = state.theta
            const phi = state.phi
            const r = Math.exp(state.distance)

            const vf = r * Math.sin(theta) * Math.cos(phi)
            const vr = r * Math.cos(theta) * Math.cos(phi)
            const vu = r * Math.sin(phi)

            for (let i = 0; i < 3; ++i) {
                eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
            }

            Mat4.lookAt(view, eye, center, up)
        }

        const injectContext = regl({
            context: {
                view: () => view,
                dirty: () => dirty,
                projection: (context: REGL.DefaultContext) => {
                    Mat4.perspective(
                        projection,
                        state.fovy,
                        context.viewportWidth / context.viewportHeight,
                        state.near,
                        state.far
                    )
                    if (state.flipY) { projection[5] *= -1 }
                    return projection
                }
            },
            uniforms: {  // TODO
                view: regl.context('view' as any),
                projection: regl.context('projection' as any)
            }
        })

        function update (props: any, block: any) {
            setState()
            injectContext(props, block)
            if (dirty) {
                console.log(view)
            }
            dirty = false
        }

        return {
            update,
            setState,
            isDirty: () => dirty
        }
    }
}