/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.

package std.unittest

import std.collection.concurrent.NonBlockingQueue
import std.env.atExit
import std.sync.AtomicBool
import std.time.MonoTime
import std.unittest.common.PrettyText

private const UPDATE_FREQ_MS = 350
private const MIN_UPDATE_RELAX_MS = 100
private const ADJUST_TERMINAL_SIZE_FREQ_REDRAWS = 5

class ProgressReporter {
    private let reporter: UTProgressReporter
    private ProgressReporter() {
        this.reporter = match (getDefaultUTProgressReporter()) {
            case Some(r) => r
            case _ => throw Exception("Actual reporter does not exists! Should 'ProgressReporter' be created?")
        }
    }

    // Terminal size cache
    private var terminalHeight: ?UInt64 = None
    private var terminalWidth: ?UInt64 = None

    private var prevReportHeight = 0

    private let printer = TerminalPrettyPrinter.fromDefaultConfiguration()

    private var reporterJob: ?Future<Unit> = None

    private let isActive = AtomicBool(false)

    prop updateQueue: NonBlockingQueue<UTProgress> {
        get() {
            reporter.updateQueue
        }
    }

    /**
     * Start dynamic progress report for specified `reporter` until it will be stopped via `stopAndClear()` call.
     */
    func startReporting() {
        if (!isActive.compareAndSwap(false, true)) {
            return
        }

        atExit {
            stopAndClear()
        }

        reporterJob = spawn {
            reporter.start()
            printer.hideCursor()
            reportLoop {
                let report = reporter.report(terminalHeight, terminalWidth)
                let reportLinesCount = report.toString().count("\n") + 1
                let reportLinesCountTrimmed = if (let Some(theight) <- terminalHeight) {
                    min(reportLinesCount, Int64(theight))
                } else {
                    reportLinesCount
                }

                printer.exclusive { printer: TerminalPrettyPrinter =>
                    clearDynamicArea()

                    printer.append("\n" * reportLinesCountTrimmed) // dedicate lines for report
                    printer.up(reportLinesCountTrimmed)
                    printer.saveCursorPos()

                    if (reportLinesCountTrimmed < prevReportHeight) {
                        printer.append("\n" * (prevReportHeight - reportLinesCountTrimmed))
                        prevReportHeight = reportLinesCountTrimmed
                    }

                    printer.toBottom()
                    printer.up(reportLinesCountTrimmed - 1)
                    printer.append(report)

                    if (let Some(theight) <- terminalHeight) {
                        printer.setScrollableMargins(end: Int64(theight) - reportLinesCountTrimmed)
                    }
                    printer.restoreCursorPos()
                }
            }
        }
    }

    private func reportLoop(render: () -> Unit) {
        var tact = 0
        while (!Thread.currentThread.hasPendingCancellation) {
            let timeBeforeUpdate = MonoTime.now()

            /* Recache terminal size.
               Do it infrequently enough to now slow down due to system calls and have dynamically updated size of the report on terminal size changed.
             */
            if (tact % ADJUST_TERMINAL_SIZE_FREQ_REDRAWS == 0) {
                tact = 0
                terminalWidth = printer.terminalWidth
                terminalHeight = printer.terminalHeight
            }

            render()

            tact++
            let updateElapsed = MonoTime.now() - timeBeforeUpdate
            sleep(max(MIN_UPDATE_RELAX_MS, UPDATE_FREQ_MS - updateElapsed.toMilliseconds()) * Duration.millisecond)
        }
    }

    func stopAndClear() {
        if (!isActive.compareAndSwap(true, false)) { return }
        if (let Some(job) <- reporterJob) {
            job.cancel()
            job.get()
            reporterJob = None
        }

        reporter.reset()

        printer.exclusive { printer =>
            clearDynamicArea()
            printer.showCursor()
        }
    }

    private func clearDynamicArea() {
        printer.clearAhead()
        printer.saveCursorPos()
        printer.setScrollableMargins()
        printer.restoreCursorPos()
    }

    static func fromDefaultConfiguration(): ?ProgressReporter {
        let config = defaultConfiguration()
        if (!config.isDynamicProgressEnabled || TestProcessKind.fromDefaultConfiguration().isWorker) {
            return None
        }
        ProgressReporter()
    }
}

extend Configuration {
    prop isDynamicProgressEnabled: Bool {
        get() {
            !(get(KeyNoProgress.noProgress) ?? false) && !bench && !noRun
        }
    }

    prop isDynamicProgressBrief: Bool {
        get() {
            get(KeyProgressBrief.progressBrief) ?? false
        }
    }

    prop dynamicProgressEntriesLimit: ?UInt64 {
        get() {
            match (get(KeyProgressEntriesLimit.progressEntriesLimit)) {
                case Some(0) | None => None
                case Some(x) where x < 0 => throw UnittestCliOptionsFormatException(
                    "--${KeyProgressEntriesLimit.progressEntriesLimit.name}", actual: x.toString(),
                    expected: "Parameter format: non-negative integer value")
                case Some(x) => UInt64(x)
            }
        }
    }
}

interface UTProgressReporter {
    prop updateQueue: NonBlockingQueue<UTProgress>

    /**
     * Indicates start of the reported process.
     * Required initialization could happen here.
     */
    func start(): Unit {}

    func report(terminalHeight: ?UInt64, terminalWidth: ?UInt64): PrettyText

    /**
     * Reset the state of the reported.
     */
    func reset(): Unit {}
}

interface UTProgress {}

private func getDefaultUTProgressReporter(): ?UTProgressReporter {
    let defaultConfig = defaultConfiguration()
    match {
        case defaultConfig.bench => None
        case TestProcessKind.fromDefaultConfiguration().isWorker => None
        case defaultConfig.isDynamicProgressEnabled => TestProgressReporter(
            ParallelInfo.fromDefaultConfiguration().nWorkers,
            isBrief: defaultConfig.isDynamicProgressBrief,
            entriesLimit: defaultConfig.dynamicProgressEntriesLimit
        )
        case _ => None
    }
}