package io.legado.app.help.coroutine

import android.util.Log
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext


class Coroutine<T>(scope: CoroutineScope, block: suspend CoroutineScope.() -> T) {

    companion object {

        val DEFAULT = MainScope()

        fun <T> async(scope: CoroutineScope = DEFAULT, block: suspend CoroutineScope.() -> T): Coroutine<T> {
            return Coroutine(scope, block)
        }

    }

    private val job: Job

    private var start: VoidCallback? = null
    private var success: Callback<T?>? = null
    private var error: Callback<Throwable>? = null
    private var finally: VoidCallback? = null

    private var timeMillis: Long? = null
    private var errorReturn: Result<T>? = null

    val isCancelled: Boolean
        get() = job.isCancelled

    val isActive: Boolean
        get() = job.isActive

    val isCompleted: Boolean
        get() = job.isCompleted

    init {
        this.job = executeInternal(scope, block)
    }

    fun timeout(timeMillis: () -> Long): Coroutine<T> {
        this.timeMillis = timeMillis()
        return this@Coroutine
    }

    fun timeout(timeMillis: Long): Coroutine<T> {
        this.timeMillis = timeMillis
        return this@Coroutine
    }

    fun onErrorReturn(value: () -> T?): Coroutine<T> {
        this.errorReturn = Result(value())
        return this@Coroutine
    }

    fun onErrorReturn(value: T?): Coroutine<T> {
        this.errorReturn = Result(value)
        return this@Coroutine
    }

    fun onStart(
        context: CoroutineContext? = null,
        block: (suspend CoroutineScope.() -> Unit)
    ): Coroutine<T> {
        this.start = VoidCallback(context, block)
        return this@Coroutine
    }

    fun onSuccess(
        context: CoroutineContext? = null,
        block: suspend CoroutineScope.(T?) -> Unit
    ): Coroutine<T> {
        this.success = Callback(context, block)
        return this@Coroutine
    }

    fun onError(
        context: CoroutineContext? = null,
        block: suspend CoroutineScope.(Throwable) -> Unit
    ): Coroutine<T> {
        this.error = Callback(context, block)
        return this@Coroutine
    }

    fun onFinally(
        context: CoroutineContext? = null,
        block: suspend CoroutineScope.() -> Unit
    ): Coroutine<T> {
        this.finally = VoidCallback(context, block)
        return this@Coroutine
    }

    //取消当前任务
    fun cancel(cause: CancellationException? = null) {
        job.cancel(cause)
    }

    fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {
        return job.invokeOnCompletion(handler)
    }

    private fun executeInternal(scope: CoroutineScope, block: suspend CoroutineScope.() -> T): Job {
        return scope.plus(Dispatchers.Main).launch {
            try {
                start?.let { dispatchVoidCallback(this, it) }
                val value = executeBlock(scope, timeMillis ?: 0L, block)
                success?.let { dispatchCallback(this, value, it) }
            } catch (e: Throwable) {
                val consume: Boolean = errorReturn?.value?.let { value ->
                    success?.let { dispatchCallback(this, value, it) }
                    true
                } ?: false

                if (!consume) {
                    error?.let { dispatchCallback(this, e, it) }
                }
            } finally {
                finally?.let { dispatchVoidCallback(this, it) }
            }
        }
    }

    private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) {
        if (null == callback.context) {
            callback.block.invoke(scope)
        } else {
            withContext(scope.coroutineContext.plus(callback.context)) {
                callback.block.invoke(this)
            }
        }
    }

    private suspend inline fun <R> dispatchCallback(
        scope: CoroutineScope,
        value: R,
        callback: Callback<R>
    ) {
        if (null == callback.context) {
            callback.block.invoke(scope, value)
        } else {
            withContext(scope.coroutineContext.plus(callback.context)) {
                callback.block.invoke(this, value)
            }
        }
    }

    private suspend inline fun executeBlock(
        scope: CoroutineScope,
        timeMillis: Long,
        noinline block: suspend CoroutineScope.() -> T
    ): T? {
        return withContext(scope.coroutineContext.plus(Dispatchers.IO)) {
            if (timeMillis > 0L) withTimeout(timeMillis) {
                block()
            } else block()
        }
    }

    private data class Result<out T>(val value: T?)

    private inner class VoidCallback(
        val context: CoroutineContext?,
        val block: suspend CoroutineScope.() -> Unit
    )

    private inner class Callback<VALUE>(
        val context: CoroutineContext?,
        val block: suspend CoroutineScope.(VALUE) -> Unit
    )
}
