/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

/*
 * Copyright 2016-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import kotlinx.coroutines.*
import kotlinx.html.*
import kotlinx.html.div
import kotlinx.html.dom.*
import kotlinx.html.js.onClickFunction
import org.w3c.dom.*
import kotlin.browser.*
import kotlin.coroutines.*
import kotlin.math.*

fun main(args: Array<String>) {
    println("Starting example application...")
    document.addEventListener("DOMContentLoaded", {
        Application().start()
    })
}

val Double.px get() = "${this}px"

private fun HTMLElement.setSize(w: Double, h: Double) {
    with(style) {
        width = w.px
        height = h.px
    }
}

private fun HTMLElement.setPosition(x: Double, y: Double) {
    with(style) {
        left = x.px
        top = y.px
    }
}

@Suppress("DEPRECATION")
private fun random() = kotlin.js.Math.random()

class Application : CoroutineScope {
    private val body get() = document.body!!
    private val scene get() = document.getElementById("scene") as HTMLElement
    private val sw = 800.0
    private val sh = 600.0
    private var animationIndex = 0
    private var job = Job()
    override val coroutineContext: CoroutineContext
        get() = job

    fun start() {
        body.append.div("content") {
            h1 {
                +"Kotlin Coroutines JS Example"
            }
            div {
                button {
                    +"Rect"
                    onClickFunction = { onRect() }
                }
                button {
                    +"Circle"
                    onClickFunction = { onCircle() }
                }
                button {
                    +"Clear"
                    onClickFunction = { onClear() }
                }
            }
            div {
                id = "scene"
            }
        }
        scene.setSize(sw, sh)
    }

    private fun animation(cls: String, size: Double, block: suspend CoroutineScope.(HTMLElement) -> Unit) {
        val elem = scene.append.div(cls)
        elem.setSize(size, size)
        val job = launch {
            block(elem)
        }

        job.invokeOnCompletion { scene.removeChild(elem) }
    }

    private fun onRect() {
        val index = ++animationIndex
        val speed = 0.3
        val rs = 20.0
        val turnAfter = 5000.0 // seconds
        var maxX = sw - rs
        val maxY = sh - rs
        animation("rect", rs) { rect ->
            println("Started new 'rect' coroutine #$index")
            val timer = AnimationTimer()
            var turnTime = timer.time + turnAfter
            val turnTimePhase = turnTime - floor(turnTime / turnAfter) * turnAfter
            var vx = speed
            var vy = speed
            var x = 0.0
            var y = 0.0
            while (true) {
                val dt = timer.await()
                x += vx * dt
                y += vy * dt
                if (x > maxX) {
                    x = 2 * maxX - x
                    vx = -vx
                }
                if (x < 0) {
                    x = -x
                    vx = -vx
                }
                if (y > maxY) {
                    y = 2 * maxY - y
                    vy = -vy
                }
                if (y < 0) {
                    y = -y
                    vy = -vy
                }
                rect.setPosition(x, y)
                if (timer.time >= turnTime) {
                    timer.delay(1000) // pause a bit
                    // flip direction
                    val t = vx
                    if (random() > 0.5) {
                        vx = vy
                        vy = -t
                    } else {
                        vx = -vy
                        vy = t
                    }
                    // reset time, but keep turning time phase
                    turnTime = ceil(timer.reset() / turnAfter) * turnAfter + turnTimePhase
                    println("Delayed #$index for a while at ${timer.time}, resumed and turned")
                }
            }
        }
    }

    private fun onCircle() {
        val index = ++animationIndex
        val acceleration = 5e-4
        val initialRange = 0.7
        val maxSpeed = 0.4
        val initialSpeed = 0.1
        val radius = 20.0
        animation("circle", radius) { circle ->
            println("Started new 'circle' coroutine #$index")
            val timer = AnimationTimer()
            val initialAngle = random() * 2 * PI
            var vx = sin(initialAngle) * initialSpeed
            var vy = cos(initialAngle) * initialSpeed
            var x = (random() * initialRange + (1 - initialRange) / 2) * sw
            var y = (random() * initialRange + (1 - initialRange) / 2) * sh
            while (true) {
                val dt = timer.await()
                val dx = sw / 2 - x
                val dy = sh / 2 - y
                val dn = sqrt(dx * dx + dy * dy)
                vx += dx / dn * acceleration * dt
                vy += dy / dn * acceleration * dt
                val vn = sqrt(vx * vx + vy * vy)
                val trim = vn.coerceAtMost(maxSpeed)
                vx = vx / vn * trim
                vy = vy / vn * trim
                x += vx * dt
                y += vy * dt
                circle.setPosition(x, y)
            }
        }

    }

    private fun onClear() {
        job.cancel()
        job = Job()
    }
}

class AnimationTimer {
    var time = window.performance.now()

    suspend fun await(): Double {
        val newTime = window.awaitAnimationFrame()
        val dt = newTime - time
        time = newTime
        return dt.coerceAtMost(200.0) // at most 200ms
    }

    fun reset(): Double {
        time = window.performance.now()
        return time
    }

    suspend fun delay(i: Int) {
        var dt = 0.0
        while (dt < i) {
            dt += await()
        }
    }
}
