/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights resvered.
 */

/**
 * @file
 * The file declars the AsyncTimeout class.
 */

package io4cj

let reentrantMutex = ReentrantMutex()

let IDLE_TIMEOUT_MILLIS: Duration = Duration.nanosecond * 6000

/**
 * The class is AsyncTimeout inherited from Timeout & Equatable<AsyncTimeout>
 * @author liyanqing14
 * @since 0.32.5
 */
open class AsyncTimeout <: Timeout & Equatable<AsyncTimeout> {
    static let TIMEOUT_WRITE_SIZE: Int64 = 64 * 1024;
    private var inQueue = false

    private let lock: ReentrantMutex = ReentrantMutex()

    private static let monitor: Monitor = Monitor()

    var head: ?AsyncTimeout = None
    var next: ?AsyncTimeout = None

    private var timeoutAt: Int64 = -1

    protected var fun: ?()->Unit = None

    /**
     * The Function is enter
     *
     * @since 0.32.5
     */
    @Frozen
    public open func enter() {
        if (inQueue) {
            throw IllegalArgumentException("Unbalanced enter/exit")
        }
        var timeoutNanos: Int64 = timeoutNanos()
        var hasDeadline = hasDeadline();
        if (timeoutNanos == 0 && !hasDeadline) {
            return; // No timeout and no deadline? Don't bother with the queue.
        }
        inQueue = true;
        scheduleTimeout(this, timeoutNanos, hasDeadline);
    }


    protected open func scheduleTimeout(node: AsyncTimeout, timeoutNanos:Int64, hasDeadline:Bool):Unit {
        synchronized(reentrantMutex) {
            match (this.head) {
                case None => 
                    this.head = AsyncTimeout()
                    this.watchdog()
                case _ => ()
            }
            let now = DateTime.now(timeZone:  TimeZone.Local).nanosecond
            if (timeoutNanos != 0 && hasDeadline) {
                node.timeoutAt = min(timeoutNanos, node.deadlineNanoTime() - now)
            } else if (timeoutNanos != 0) {
                node.timeoutAt = now + timeoutNanos
            } else if (hasDeadline) {
                node.timeoutAt = node.deadlineNanoTime()
            } else {
                throw Exception("Assertion scheduleTimeout")
            }
            let remainingNanos = node.remainingNanos(now)
            var prev = this.head.getOrThrow()
            while (true) {
                match (prev.next) {
                    case Some(v) => 
                        if (remainingNanos >= v.remainingNanos(now)) {
                            continue
                        }
                    case None => ()
                } 
                node.next = prev.next
                prev.next = node
                if (this.head == prev) {
                    monitor.notify()
                }
                break
            }
        }
    }

    @Frozen
    private func watchdog() {
        spawn { =>
            while (true) {
                var timedout: ?AsyncTimeout = None
                synchronized (lock) {
                    timedout = awaitTimeout()
                    match (timedout) {
                        case None => continue
                        case _ => ()
                    }
                    if (timedout == this.head) {
                        this.head = None
                        return
                    }
                }
                timedout.getOrThrow().timedOut()
            }
        }
    }

    /**
     * The Function is newTimeoutException
     *
     * @param e of Exception
     *
     * @return Type of Exception
     * @since 0.32.5
     */
    @Frozen
    public open func newTimeoutException(e: Exception): Exception {
        return InterruptedException("timeout ${e.toString()}")
    }

    /**
     * The Function is newTimeoutException
     *
     * @return Type of Exception
     * @since 0.32.5
     */
    @Frozen
    public open func newTimeoutException(): Exception {
        return InterruptedException("timeout")
    }

    protected open func timedOut(): Unit{
        match (fun) {
            case None => ()
            case Some(v) => v()
        }
    }

    /**
     * The Function is sink
     *
     * @param sink of Sink
     *
     * @return Type of Sink
     * @since 0.32.5
     */
    @Frozen
    public func sink(sink: Sink): Sink {
        func subWrite(source: Buffer, byteCount: Int64): Unit {
            Util.checkOffsetAndCount(source.size, 0, byteCount)
            while (byteCount >0) {
                var toWrite = 0
                var s = source.head.getOrThrow()
                while (toWrite < AsyncTimeout.TIMEOUT_WRITE_SIZE) {
                    toWrite += (s.limit - s.pos)
                    if (toWrite >= byteCount) {
                        toWrite = byteCount
                        break
                    }
                    s = s.next.getOrThrow()
                }
                var throwOnTimeout = false

            }
        }
        func subFlush (): Unit {
            var throwOnTimeout = false
            enter()
            try {
                sink.flush()
                throwOnTimeout = true
            } catch (e: Exception) {
                throw exit(e);
            } finally {
                exit(throwOnTimeout);
            }
        }
        func subTimeout (): Timeout {
            return this
        }
        let asink = AnonymousSink()
        asink.funWrite = subWrite
        asink.funFlush = subFlush
        asink.funTimeout = subTimeout
        return asink
    }

    /**
     * The Function is source
     *
     * @param source of Source
     *
     * @return Type of Source
     * @since 0.32.5
     */
    @Frozen
    public func source(source: Source): Source {
        func subRead (sink: Buffer, byteCount: Int64): Int64 {
            var throwOnTimeout = false
            enter()
            try {
                let result = source.read(sink, byteCount)
                throwOnTimeout = true
                return result
            } catch (e:Exception) {
                throw exit(e)
            } finally {
                exit(throwOnTimeout)
            }
        }
        func subTimeout(): Timeout {
            return this
        }
        let sourceOther = AnonymousSource()
        sourceOther.funRead = subRead
        sourceOther.funTimeout = subTimeout
        return sourceOther
    }

        func exit(e: Exception): Exception {
        if (!exit()) {
            return e
        }
        return newTimeoutException(e)
    }
        func exit(throwOnTimeout: Bool) {
        if (exit() && throwOnTimeout) {
            throw newTimeoutException()
        }
    }

    /**
     * The Function is exit
     *
     * @return Type of Bool
     * @since 0.32.5
     */
    @Frozen
    public func exit() : Bool {
        if (!inQueue) {
            return false
        }
        inQueue = false
        return cancelScheduledTimeout(this)
    }

    @Frozen
    private func cancelScheduledTimeout(node: AsyncTimeout): Bool {
        synchronized(reentrantMutex) {
            var node = node
            var prev = this.head
            while (true) {
                match (prev) {
                    case None => break
                    case Some(v) => 
                        if (v.next.getOrThrow() == node) {
                            v.next = node.next
                            node.next = None
                            return false
                        }
                }
            }
            return true
        }
    }

    /**
     * The Function is ==
     *
     * @param that of AsyncTimeout
     *
     * @return Type of Bool
     * @since 0.32.5
     */
    public operator func == (that: AsyncTimeout): Bool {
        return refEq(this, that)
    }
    /**
     * The Function is !=
     *
     * @param that of AsyncTimeout
     *
     * @return Type of Bool
     * @since 0.32.5
     */
    public operator func != (that: AsyncTimeout): Bool {
        return !refEq(this, that)
    }

    /**
     * The Function is awaitTimeout
     *
     * @return Type of ?AsyncTimeout
     * @since 0.32.5
     */
    @Frozen
    public func awaitTimeout(): ?AsyncTimeout {
        let node = head
        match (node) {
            case None => 
                let start = DateTime.now()
                monitor.wait(timeout: IDLE_TIMEOUT_MILLIS)
                if (match (head.getOrThrow().next) {
                    case None => true
                    case _ => false
                } &&  Duration.nanosecond >= IDLE_TIMEOUT_MILLIS) {
                    return head
                } else {
                    return None
                }
            case Some(v) =>
                var waitNanos = node.getOrThrow().remainingNanos(DateTime.now().nanosecond)
                if (waitNanos > 0) {
                    let waitMillis = waitNanos / 1000000
                    waitNanos = waitNanos - (waitMillis * 1000000)
                    monitor.wait(timeout: Duration.nanosecond * (waitNanos + waitMillis))
                    return None
                } 
                head.getOrThrow().next = node.getOrThrow().next
                node.getOrThrow().next = None
                return node
        }
    }

    @Frozen
    private func remainingNanos(now: Int64): Int64 {
        return this.timeoutAt - now
    }
}