'use strict'

let canvas, ctx, canvasBuffer

let showDebugInfo = false
let doPerf = false
let loopStartPoint = null
let loopEndPoint = null
let framerateCap = false

let script = scriptMaker()

const bpm = sync.globals.BeatsPerMin
const ticksPerLine = sync.globals.TicksPerLine
const linesPerBeat = sync.globals.LinesPerBeat
const syncs = Object.values(sync.patterns)

const msPerBeat = (1000*60)/bpm
const msPerLine = msPerBeat / linesPerBeat

const instrumentIds = {
    // name: [instrumentId, noteLength, decaySpeed, generatedNoteData]
    'basedrum':     [0, 30, 0.010, []],
    'snare1':       [1, 20, 0.1, []],
    'snare2':       [2, 20, 0.1, []],
    'bass':         [23, 10, 0.1, []],
    'longbd':       [14, 10, 0.1, []],
    'glitchbd':     [20, 10, 0.1, []],
    'glitch1':      [4, 10, 0.1, []],
    'glitch2':      [5, 10, 0.1, []],
    'glitch3':      [6, 10, 0.1, []],
    'dubbd':        [13, 10, 0.1, []],
    'dubsnare':     [15, 10, 0.1, []],
}
Object.values(instrumentIds).forEach(instrumentData => {
    const instrumentId = instrumentData[0]
    const noteLength = instrumentData[1]
    const noteDecay = instrumentData[2]

    let lastNoteOnSyncCursor = null
    let value = 0
    let noteOnLength
    for (let i = 0; i < 200*300; ++i) {
        const ms = (i * 10) * (60 / bpm)
        const syncCursor = Math.floor(ms / msPerLine)
        if (syncCursor !== lastNoteOnSyncCursor) {
            lastNoteOnSyncCursor = syncCursor
            const currentLine = syncs[syncCursor]
            if (currentLine && currentLine.notes && !!currentLine.notes.find(note => note && note.Instrument === instrumentId)) {
                noteOnLength = 0
                value = 1
            }
        } else {
            if (noteOnLength++ > noteLength) {
                value = value - noteDecay
            }
        }
        instrumentData[3].push(Math.max(0, Math.min(1, value)))
    }
})
function readInstrumentSync (instrumentName) {
    const instrumentData = instrumentIds[instrumentName]
    const time = Math.floor(currentFrameTime * 100)
    const value = instrumentData[3][time]
    return value
}
/*
{
    instrumentIds['rewind'] = [0, 0, 0, []]
    instrumentIds['!rewind'] = [0, 0, 0, []]
    for (let i = 0; i < 200*300; ++i) {
        const beat = i / 100
        const value = beat >= 340 && beat <  424 ? 1 : 0
        instrumentIds['rewind'][3].push(value)
        instrumentIds['!rewind'][3].push(1-value)
    }
}
*/

let audio;
const overlays = {}
const titleScreenImage = new Image()
const titleScreenImage2 = new Image()
titleScreenImage.src = `gfx/loading.png`
titleScreenImage2.src = `gfx/loading2.png`

let transport = null
let requestAnimationFrameId = null

function requestUpdate () {
    if (!framerateCap) {
        window.cancelAnimationFrame(requestAnimationFrameId)
        requestAnimationFrameId = window.requestAnimationFrame(update)
    } else {
        window.clearTimeout(requestAnimationFrameId)
        requestAnimationFrameId = window.setTimeout(update, 100)
    }
}

function startPressed () {
    transport.setIsPlaying(true)
    requestUpdate()
}

function degToRad (degrees) {
    return degrees * (Math.PI/180)
}

const maxAssets = window.overlays.length + 1 + 1// +1 for music, +1 for colourtextures
let totalLoaded = 0;
const loaded = () => totalLoaded += 1

function load () {
    let loadingFrame = 0, keep = 0, timeToBlink = 10;
    console.log(`                              __\\/__`)
    console.log(` ____ ____       ____  _____ _\\    /         ____ _____ __ ______         ____`)
    console.log(` \\  /    /_____._\\  /_/    //     /____ ____ \\  /       \\//  __  |_____ / \\  /`)
    console.log(`  \\/    </     |  \\/ /    </   _\\/    </    |/\\/_______/ /  //   !    </   \\/|`)
    console.log(` /   /     /   |    /          \\/\\    /    _/   \\_______/  </   /        /   |`)
    console.log(`/___/\\____/\\   |___/\\___/\\     /__\\________\\/_________  /______/    \\___/\\___!`)
    console.log(`            \\__!          \\______\\                    \\/     /_______\\`)
   
    const params = new URLSearchParams(window.location.search)
    if (params.has('capture')) {
        const background = document.getElementById('background')
        background.style.backgroundColor = "#888"
    }
    if (params.has('framerateCap')) {
        framerateCap = true
    }

    audio = new Audio('./audio/muzak.mp3')
    audio.addEventListener("canplaythrough", loaded)
    audio.load()

    window.overlays.forEach(x => {
        const image = new Image()
        image.onload = () => {
            loaded()
        }
        image.onerror = () => {
            throw new Error(`Failed to load overlay: ${x}`)
        }
        image.src = `gfx/${x}.png`
        overlays[x] = image
    })

    let loading = () => {
        fitScreen()

        ctx.fillStyle = '#000'
        ctx.fillRect(27, 0, 613, 480)
        ctx.fillStyle = '#fff'
        ctx.fillRect(27, 0, (totalLoaded/maxAssets) * 613, 480)

        ctx.drawImage(titleScreenImage, 0, 0)

        if (loadingFrame > timeToBlink || keep > 0) {
            ctx.drawImage(titleScreenImage2, 0, 0)

            if (keep <= 0) {
                keep = timeToBlink
            }
            loadingFrame = 0
        }

        loadingFrame += 1;
        keep -= 1;

        if (totalLoaded < maxAssets) {
            window.requestAnimationFrame(loading)
        } else {
            const params = new URLSearchParams(window.location.search)
            if (params.has('dev')) {
                main()
            } else {
                return setTimeout(() => main(true), 1000)
            }
        }
    }
    console.log('Loading..')
    loading()
}


window.onresize = () => { fitScreen() }
canvas = document.getElementById('canvas')
ctx = canvas.getContext('2d', { alpha: false })
canvasBuffer = ctx.createImageData(640, 480)
for (var i = 0; i < canvasBuffer.data.length; ++i) {
    canvasBuffer.data[i] = 255
}

function main () {
    console.log('Loaded!')

    console.log("                     _________    ____________")
    console.log("   _                 \\       /   /           /         ________")
    console.log("  (_)         ______/       /.___\\         _/ ________/     __/")
    console.log("              |    /  ___    |\\      ___  /___\\    ____     /")
    console.log("       o      |       \\/     |_\\_    \\/     |      \\/______/__   .")
    console.log("           .  |_______/      |      __      |___________     |       o")
    console.log("                /          __|_____//       | /       \\/     |           _")
    console.log("               /            \\!      \\_______!/        / _____|          (_)")
    console.log("              /______________\\           /______________\\")
    console.log(" ________\\___/         __                  _____ _____\\__       ___")
    console.log("_\\     _  \\/       _____\\__    _______ __. Z  _/_|     Z_____. /  /      \\__")
    console.log("|   ___/  /--------\\     \\/----\\_   _/   |/   |_\\      /_    |/   Z-------\\/_")
    console.log("|   X/  _/__ __   /   ___  |   !X   |    /    |)   __/  :)   /    |    __/  |")
    console.log("|   ___    | X/__/__  X/   |   /    |   /     |    X/   |   /     |    X/   |")
    console.log("|   X/     |       /       |  /     |   ___   |    _    |   ___   |    _    |")
    console.log("|   /     _|  \\     \\__    !        |   Z/    !    /    |   Z/    |    /    |")
    console.log("|_________\\   /\\______/   _/        l___/     :___/|____|   /|____|---/_____!")
    console.log("      /______/   \\________\\/--------'  /      !NE7      !__/         /_\\")
    console.log("                                    __/      _|         .\\/")
    console.log("MIKUCOM//EPISODE II//YAS BROUHAHA /\\\\        \\ ANNODEMONI/MMXXII//4 DEM YOUTS")
    console.log("                                 /__\\\\________\\")

    rulAllModuleInits()
    fitScreen()

    const params = new URLSearchParams(window.location.search)

    if (params.has('nomusic')) {
        transport = new Transport(60 * 20)
    } else {
        transport = new TransportMp3(audio, 76)
    }

    if (params.has('time')) {
        const time = parseInt(params.get('time')) * 60 / bpm
        transport.seekExact(time)
    }
    if (params.has('timeLoopLength')) {
        loopStartPoint = transport.getCurrentTime()
        loopEndPoint = loopStartPoint + parseInt(params.get('timeLoopLength')) * 60 / bpm
    }

    let reloadBlocksNoPlay = false
    if (params.has('devserver')) {
        reloadBlocksNoPlay = devserverInit()
    }

    if (params.has('controls')) {
        document.addEventListener('keydown', (event) => {
            const getSpeed = () => {
                if (event.shiftKey) return 1/60
                if (event.altKey) return 4
                return 1
            }
            if (event.key === ' ') {
                transport.setIsPlaying(!transport.getIsPlaying())
                requestUpdate()
            } else if (event.key === 'ArrowUp' ) {
                transport.seekExact(0)
                requestUpdate()
            } else if (event.key === 'ArrowLeft' ) {
                transport.seekDelta(-getSpeed())
                requestUpdate()
            } else if (event.key === 'ArrowRight' ) {
                transport.seekDelta(getSpeed())
                requestUpdate()
            } else if (event.key === 'f') {
                openFullscreen()
            }
        })
    } else {
        document.addEventListener('keydown', (event) => {
            if (event.key === ' ') {
                transport.setIsPlaying(!transport.getIsPlaying())
                requestUpdate()
            } else if (event.key === 'f') {
                openFullscreen()
            }
        })
        document.addEventListener('click', () => {
            transport.setIsPlaying(true)
            requestUpdate()
        })
        canvas.addEventListener('touchstart', () => {
            transport.setIsPlaying(true)
            openFullscreen()
            requestUpdate()
        })
    }

    const elDebuggo = document.getElementById('debugStuff')
    if (params.has('dev')) {
        const timelineCanvas = document.createElement('canvas')
        timelineCanvas.id = 'timeline'
        addDevSection(elDebuggo, 'Syncs', [timelineCanvas])

        const transportText = document.createElement('pre')
        transportText.id = 'transportText'
        transportText.style.color = '#fff'
        transportText.style.margin = '0px'
        addDevSection(elDebuggo, 'Transport', [transportText])

        const moduleText = document.createElement('pre')
        moduleText.id = 'moduleText'
        moduleText.style.color = '#fff'
        moduleText.style.margin = '0px'
        addDevSection(elDebuggo, 'Modules', [moduleText])

        const optionsChildren = []
        const options = [
            {
                text: 'dev',
                type: 'checkbox'
            },
            {
                text: 'devserver',
                type: 'checkbox'
            },
            {
                text: 'nomusic',
                type: 'checkbox'
            },
            {
                text: 'perf',
                type: 'checkbox'
            },
            {
                text: 'capture',
                type: 'checkbox'
            },
            {
                text: 'controls',
                type: 'checkbox'
            },
            {
                text: 'framerateCap',
                type: 'checkbox'
            },
            {
                text: 'time',
                type: 'number'
            },
            {
                text: 'timeLoopLength',
                type: 'number'
            },
        ]
        const writeParams = (params) => {
            let paramsString = ''
            params.forEach((value, key) => {
                if (value) paramsString += `${key}=${value}`
                else paramsString += `${key}`
                paramsString += '&'
            })
            paramsString = paramsString.replace(/&$/, '')
            window.location.search = paramsString
        }
        options.forEach(control => {
            if (control.type === 'checkbox') {
                const checkboxElement = document.createElement('input')
                checkboxElement.type = 'checkbox'
                checkboxElement.checked = params.has(control.text)
                checkboxElement.onchange = () => {
                    const params = new URLSearchParams(window.location.search)
                    if (checkboxElement.checked) {
                        params.set(control.text, '')
                    } else {
                        params.delete(control.text)
                    }
                    writeParams(params)
                }
                optionsChildren.push(checkboxElement)
            } else if (control.type === 'number') {
                const numberElement = document.createElement('input')
                numberElement.type = 'number'
                numberElement.value = params.get(control.text)
                numberElement.onchange = () => {
                    const params = new URLSearchParams(window.location.search)
                    if (numberElement.value !== '') {
                        params.set(control.text, numberElement.value)
                    } else {
                        params.delete(control.text)
                    }
                    writeParams(params)
                }
                optionsChildren.push(numberElement)
            }
            if (control.text) {
                const checkboxText = document.createElement('span')
                checkboxText.style.color = '#fff'
                checkboxText.innerHTML = control.text + ' '
                checkboxText.style.marginLeft = '5px'
                checkboxText.style.marginRight = '15px'
                optionsChildren.push(checkboxText)
            }
        })
        addDevSection(elDebuggo, 'Options', optionsChildren, false)

        showDebugInfo = true

    } else {
        elDebuggo.style = "display:none"
    }

    if (params.has('perf')) {
        doPerf = true
    }

    for (let i = 0; i < NUM_BUFFERS; ++i) {
        greyscaleBuffers[i] = new Float32Array(640 * 480)
        for (let index = 0; index < 640*480; ++index) greyscaleBuffers[i][index] = 1
        bufferLocks[i] = false
    }

    ctx.drawImage(overlays.opening, 0, 0)
    if (showDebugInfo) {
        if (!reloadBlocksNoPlay) transport.setIsPlaying(false)
        if (params.has('time')) {
            update()
        }
    } 
}

function drawBallsNegative(balls, textureName) {
    const texture = getTexture(textureName)
    const texSize = texture.size
    const texData = texture.data

    const greyscaleBuffer = greyscaleBuffers[getOutputBufferId()]
    const halfBallSize = texSize / 2

    for (let i = 0; i < balls.length; ++i) {
        const [x, y, brightness, size] = balls[i]

        const range = Math.floor(texSize * size)
        const halfRange = Math.floor(range / 2)

        if (x+halfBallSize < 0 || x-halfBallSize > 640 ||
            y+halfBallSize < 0 || y-halfBallSize > 480 ||
            size <= 0) {
                continue
        }

        const iHalfRangeTimeHalfBallSize = 1 / halfRange

        for (let py = -halfRange; py <= +halfRange; ++py) {
            if (y+py >= 0 && y+py < 480) {
                let ty = Math.floor(py * iHalfRangeTimeHalfBallSize * halfBallSize + halfBallSize)
                //ty = Math.min(Math.max(ty, 0), texSize - 1)
                ty = Math.min(ty, texSize-1)
                const dy = (y + py) * 640
                for (let px = -halfRange; px <= +halfRange; ++px) {
                    if (x+px >= 0 && x+px < 640) {
                        let tx = Math.floor(px * iHalfRangeTimeHalfBallSize * halfBallSize + halfBallSize)
                        //tx = Math.min(Math.max(tx, 0), texSize - 1)
                        tx = Math.min(tx, texSize-1)
                        greyscaleBuffer[(x + px) + dy] = Math.max(0, greyscaleBuffer[(x + px) + dy] - texData[tx + ty * texSize] * brightness)
                    }
                }
            }
        }
    }
}

function transformBalls(balls3d) {
    const balls2d = []
    balls3d.forEach(ball => {
        const [pos, brightness, size] = ball
        const v = m4.transformVector(camViewPersp, pos)
        if (v[2] > 1) {
            const w = v[3]
            const scaledSize = 30*size/w
            if (scaledSize > 0) {
                balls2d.push([Math.floor(v[0]*320/w) + 320, Math.floor(v[1]*240/w) + 240, scaledSize, brightness])
            }
        }
    })
    return balls2d
}

function drawBalls(balls, textureName) {
    const texture = getTexture(textureName)
    const texSize = texture.size
    const texData = texture.data

    const greyscaleBuffer = greyscaleBuffers[getOutputBufferId()]
    const halfBallSize = texSize / 2
    for (let i = 0; i < balls.length; ++i) {
        let [x, y, size, brightness] = balls[i]
        if (size > 0.2) {
            brightness = Math.max(0, brightness)

            const range = Math.floor(texSize * size)
            const halfRange = Math.floor(range / 2)
            const iHalfRangeTimesHalfBallSize = 1 / halfRange * halfBallSize
            for (let py = -halfRange; py <= +halfRange; ++py) {
                if (y+py >= 0 && y+py < 480) {
                    let ty = Math.floor(py * iHalfRangeTimesHalfBallSize + halfBallSize)
                    ty = Math.min(Math.max(ty, 0), texSize - 1)
                    const dy = (y + py) * 640
                    for (let px = -halfRange; px <= +halfRange; ++px) {
                        if (x+px >= 0 && x+px < 640) {
                            let tx = Math.floor(px * iHalfRangeTimesHalfBallSize + halfBallSize)
                            tx = Math.min(Math.max(tx, 0), texSize - 1)
                            greyscaleBuffer[(x + px) + dy] += texData[tx + ty * texSize] * brightness
                        }
                    }
                }
            }
        }
    }
}

let camViewPersp = m4.identity()

function openFullscreen() {
    if (canvas.requestFullscreen) {
        canvas.requestFullscreen()
    } else if (canvas.webkitRequestFullscreen) { /* Safari */
        canvas.webkitRequestFullscreen()
    } else if (canvas.msRequestFullscreen) { /* IE11 */
        canvas.msRequestFullscreen()
    }
}

function fitScreen() {
    const params = new URLSearchParams(window.location.search)
    if (params.has('dev') || params.has('capture')) return

    const aspect = 640 / 480
    const windowWidth = window.innerWidth
    const windowHeight = window.innerHeight
    const windowAspect = windowWidth / windowHeight
    if (windowAspect >= aspect) {
        canvas.style.width = windowHeight * aspect
        canvas.style.height = windowHeight
        canvas.style.top = 0
        canvas.style.left = (windowWidth - (windowHeight * aspect)) / 2
    } else {
        canvas.style.width = windowWidth
        canvas.style.height = windowWidth / aspect
        canvas.style.top = (windowHeight - (windowWidth / aspect)) / 2
        canvas.style.left = 0
    }
    canvas.style.position = 'absolute'
    canvas.style.cursor = 'none'
}

let firstUpdate = false
let currentFrameTime = 0
let lastFrameTime = 0
async function update () {
    if (!firstUpdate) {
        console.log('Kick out the jams..')
        firstUpdate = true
    }

    if (loopEndPoint && transport.getCurrentTime() > loopEndPoint) {
        transport.seekExact(loopStartPoint)
    }
    transport.update()

    const frameTime = transport.getCurrentTime() / 60 * bpm // + 0.5 // sync fix?
    currentFrameTime = frameTime

    renderer.restoreDefaultState()

    const startTime = window.performance.now()
    modulesRunStrings = []
    resetBuffers()
    resetBufferLocks()
    setOutputBufferId(lockBuffer())
    await runModules(frameTime, script)
    const endTime = window.performance.now()

    if (doPerf) {
        let renderTimings = []
        for (let i = 0; i < 50; ++i) {
            modulesRunStrings = []
            resetBuffers()
            resetBufferLocks()
            setOutputBufferId(lockBuffer())
            const startTime = window.performance.now()
            await runModules(frameTime, script)
            const endTime = window.performance.now()
            const renderTime = endTime - startTime
            renderTimings.push(renderTime)
        }
        const min = Math.min(...renderTimings)
        const max = Math.max(...renderTimings)
        renderTimings = renderTimings.filter(x => !(x === min || x === max))
        console.log('Max:', max.toFixed(2))
        console.log('Min:', min.toFixed(2))
        console.log('Filtered avg:', (renderTimings.reduce((p, c) => p + c, 0) / renderTimings.length).toFixed(2))
    }

    if (showDebugInfo) {
        //if (isDevSectionVisible('Transport')) {
            const newFrameTime = window.performance.now()
            const totalFrameTime = newFrameTime - lastFrameTime
            lastFrameTime = newFrameTime
            const fps = 1000 / totalFrameTime
            const debugText = 'BEAT: ' + frameTime.toFixed(2) + '\n' +
                (endTime - startTime).toFixed(2) + 'ms\n' +
                fps.toFixed(2) + 'fps\n' +
                (transport.getIsPlaying() ? 'PLAYING' : 'PAUSED') + '\n'
            document.getElementById('transportText').innerText = debugText
        //}
  
        //if (isDevSectionVisible('Modules')) {
            document.getElementById('moduleText').innerText = modulesRunStrings.join('\n')
        //}

        if (isDevSectionVisible('Syncs')) {
            showTimelines(frameTime)
        }
    }

    if (transport.getIsPlaying()) {
        requestUpdate()
    }
}

let timelineCanvasCtx = null
let timelineCanvasBuffer = 0
function showTimelines (frameTime) {
    const numRows = Object.values(instrumentIds).length
    const rowHeight = 13
    const width = 320
    const height = numRows*rowHeight
    const playHeadY = width * 0.25

    if (!timelineCanvasCtx) {
        const canvas = document.getElementById('timeline')
        canvas.width = width
        canvas.height = height
        timelineCanvasCtx = canvas.getContext('2d', { alpha: false })
        timelineCanvasBuffer = timelineCanvasCtx.createImageData(width, height)
        for (var i = 0; i < timelineCanvasBuffer.data.length; ++i) {
            timelineCanvasBuffer.data[i] = 255
        }
    }

    // clear background
    let index = 0
    for (let i = 0; i < width * height; ++i) {
        timelineCanvasBuffer.data[index++] = 0
        timelineCanvasBuffer.data[index++] = 0
        timelineCanvasBuffer.data[index++] = 0
        index++
    }

    // syncs
    const syncsOffset = Math.floor(frameTime * 100 - playHeadY)
    let instrumentRow = 0
    Object.values(instrumentIds).forEach(instrumentData => {
        for (let x = 0; x < width; ++x) {
            const syncValue = instrumentData[3][syncsOffset + x]
            const syncHeight = Math.floor(syncValue * (rowHeight-2)) + 1
            index = (x + width*(rowHeight*(instrumentRow+1)-1)) * 4
            for (let y = 0; y < syncHeight; ++y) {
                timelineCanvasBuffer.data[index++] = 150
                timelineCanvasBuffer.data[index++] = 150
                timelineCanvasBuffer.data[index++] = 150
                index -= width*4 + 3
            }
        }
        ++instrumentRow
    })

    // bars and beats
    let beatPos = playHeadY - Math.floor((frameTime % 3) * 100)
    let beat = 0
    while (true) {
        if (beatPos >= width) break
        if (beatPos >= 0) {
            index = beatPos * 4
            const colour = beat === 0 ? 250 : 150
            for (let i = 0; i < height; ++i) {
                timelineCanvasBuffer.data[index++] = colour
                timelineCanvasBuffer.data[index++] = colour
                index += (width*4)-2
            }
        }
        beatPos += 100
        beat = (beat + 1) % 3
    }

    // playhead
    index = playHeadY*4
    for (let i = 0; i < height; ++i) {
        timelineCanvasBuffer.data[index++] = 250
        timelineCanvasBuffer.data[index++] = 50
        timelineCanvasBuffer.data[index++] = 50
        index += (width*4)-3
    }
    
    // copy to canvas
    timelineCanvasCtx.putImageData(timelineCanvasBuffer, 0, 0)

    // sync names
    timelineCanvasCtx.font = `${rowHeight}px sans-serif`
    timelineCanvasCtx.fillStyle = '#fff'
    timelineCanvasCtx.textAlign = 'start'
    timelineCanvasCtx.textBaseline = 'top'
    let textPos = 0
    Object.keys(instrumentIds).forEach(instrumentId => {
        timelineCanvasCtx.fillText(instrumentId, 0, textPos)
        textPos += rowHeight
    })
}
