/*
 * 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.
 */

package stdx.encoding.json.stream

import std.collection.ArrayList
import std.io.OutputStream
import std.time.*

enum JsonWriterState {
    | EmptyArray
    | NoEmptyArray
    | EmptyObject
    | NoEmptyObject
    | EmptyText
    | NoEmptyText
}

public class JsonWriter {
    static const DEFAULT_CAPACITY: Int64 = 4096
    static const FLUSH_THRESHOLD: Int64 = 3500

    var out: OutputStream
    var stack: ArrayList<JsonWriterState> = ArrayList<JsonWriterState>()

    var haveName = false

    var buffer = Array<UInt8>(DEFAULT_CAPACITY, repeat: 0)
    var curPos = 0

    public var writeConfig: WriteConfig = WriteConfig.compact

    public init(out: OutputStream) {
        this.out = out
        stack.add(EmptyText)
    }

    public func flush(): Unit {
        out.write(buffer[0..curPos])
        curPos = 0
        out.flush()
    }

    @OverflowWrapping
    public func jsonValue(value: String): JsonWriter {
        beforeValue()
        if (value.size > JsonWriter.FLUSH_THRESHOLD) {
            flushOutBuf()
            unsafe { out.write(value.rawData()) }
        } else {
            if (curPos + value.size > JsonWriter.DEFAULT_CAPACITY - 1) {
                flushOutBuf()
            }
            unsafe { value.rawData().copyTo(buffer, 0, curPos, value.size) }
            curPos += value.size
        }
        this
    }

    func flushOutBuf() {
        out.write(buffer[0..curPos])
        curPos = 0
        0
    }

    public func writeValue<T>(v: T): JsonWriter where T <: JsonSerializable {
        v.toJson(this)
        haveName = false
        this
    }

    @OverflowWrapping
    public func writeNullValue(): JsonWriter {
        beforeValue()
        buffer[curPos] = b'n'
        buffer[curPos + 1] = b'u'
        buffer[curPos + 2] = b'l'
        buffer[curPos + 3] = b'l'
        curPos += 4
        this
    }

    protected func writeWrap(): JsonWriter {
        if (curPos + 1 > JsonWriter.DEFAULT_CAPACITY - 1) {
            flushOutBuf()
        }
        buffer[curPos] = b'\n'
        curPos += 1
        this
    }

    @OverflowWrapping
    public func writeName(name: String): JsonWriter {
        beforeName()
        haveName = true
        name.toJson(this)
        buffer[curPos] = b':'
        curPos++
        haveName = true // reuse String.toJson, need to keep name state
        useSpaceAfterSeparators()
        this
    }

    @OverflowWrapping
    public func startArray(): Unit {
        beforeValue()
        buffer[curPos] = b'['
        curPos++
        put(EmptyArray)
    }

    // end array need to check if the state is after an array
    @OverflowWrapping
    public func endArray(): Unit {
        if (curPos > FLUSH_THRESHOLD) {
            flushOutBuf()
        }
        match (top()) {
            case EmptyArray | NoEmptyArray =>
                newlineWithIndent(stack.size - 2)
                buffer[curPos] = b']'
                curPos++
            case _ => throw IllegalStateException("End array must be directly after Start array")
        }
        pop()
    }

    @OverflowWrapping
    public func startObject(): Unit {
        beforeValue()
        buffer[curPos] = b'{'
        curPos++
        put(EmptyObject)
    }

    @OverflowWrapping
    public func endObject(): Unit {
        if (curPos > FLUSH_THRESHOLD) {
            flushOutBuf()
        }
        if (haveName) {
            throw IllegalStateException("End object cannot directly after a name.")
        }
        match (top()) {
            case EmptyObject | NoEmptyObject =>
                newlineWithIndent(stack.size - 2)
                buffer[curPos] = b'}'
                curPos++
            case _ => throw IllegalStateException("End object must be directly after Start object.")
        }
        pop()
    }

    @OverflowWrapping
    func beforeName() {
        if (curPos > FLUSH_THRESHOLD) {
            flushOutBuf()
        }
        if (haveName) {
            throw IllegalStateException("Already wrote a name, expecting a value.")
        }
        match (top()) {
            case EmptyObject => stack[stack.size - 1] = NoEmptyObject
                newlineWithIndent(stack.size - 1)
            case NoEmptyObject =>
                buffer[curPos] = b','
                curPos++
                newlineWithIndent(stack.size - 1)
            case _ => throw IllegalStateException("The name must be within an object.")
        }
    }

    @OverflowWrapping
    func beforeValue() {
        if (curPos > FLUSH_THRESHOLD) {
            flushOutBuf()
        }
        match (top()) {
            case NoEmptyArray =>
                buffer[curPos] = b','
                curPos++

                if (writeConfig.newline.isEmpty()) {
                    useSpaceAfterSeparators()
                }
                newlineWithIndent(stack.size - 1)
            case EmptyArray => stack[stack.size - 1] = NoEmptyArray
                newlineWithIndent(stack.size - 1)
            case NoEmptyText => throw IllegalStateException("JSON text must have only one top-level value.")
            case EmptyText => stack[stack.size - 1] = NoEmptyText
            case EmptyObject => stack[stack.size - 1] = NoEmptyObject
            case NoEmptyObject => // write value in object must write name first
                if (!haveName) {
                    throw IllegalStateException("Value in Json object must after a name.")
                }
                haveName = false
        }
    }

    func top(): JsonWriterState {
        return stack[stack.size - 1]
    }

    func put(scope: JsonWriterState) {
        if (stack.size >= MAX_JSON_STREAM_DEPTH) {
            throw IllegalStateException("Json nested depth exceeds ${MAX_JSON_STREAM_DEPTH}.")
        }
        stack.add(scope)
    }

    func pop() {
        stack.remove(at: stack.size - 1)
    }

    func newlineWithIndent(indentLevel: Int64): Unit {
        // new line
        if (writeConfig.newline.isEmpty()) {
            return
        }
        for (b in writeConfig.newline) {
            buffer[curPos] = b
            curPos++

            if (curPos >= buffer.size) {
                flushOutBuf()
            }
        }

        // indent
        if (writeConfig.indent.isEmpty()) {
            return
        }
        for (_ in 0..indentLevel) {
            for (b in writeConfig.indent) {
                buffer[curPos] = b
                curPos++

                if (curPos >= buffer.size) {
                    flushOutBuf()
                }
            }
        }
    }

    func useSpaceAfterSeparators(): Unit {
        if (writeConfig.useSpaceAfterSeparators) {
            buffer[curPos] = b' '
            curPos++
        }
    }
}

public struct WriteConfig {
    private WriteConfig(
        var _newline!: String,
        var _indent!: String,
        var _usesSpaceAfterSeparators!: Bool,
        var _htmlSafe!: Bool,
        var _timeFormat!: String
    ) {}

    public mut prop newline: String {
        get() {
            _newline
        }
        set(v) {
            for (b in v where (b != b'\r' && b != b'\n')) {
                throw IllegalArgumentException("Invalid newline token: ${b}")
            }
            _newline = v
        }
    }

    public mut prop indent: String {
        get() {
            _indent
        }
        set(v) {
            for (b in v where (b != b' ' && b != b'\t')) {
                throw IllegalArgumentException("Invalid indent token: ${b}")
            }
            _indent = v
        }
    }

    public mut prop useSpaceAfterSeparators: Bool {
        get() {
            _usesSpaceAfterSeparators
        }
        set(v) {
            _usesSpaceAfterSeparators = v
        }
    }

    public mut prop htmlSafe: Bool {
        get() {
            _htmlSafe
        }
        set(v) {
            _htmlSafe = v
        }
    }

    public mut prop dateTimeFormat: String {
        get() {
            _timeFormat
        }
        set(v) {
            _timeFormat = v
        }
    }

    public static let compact: WriteConfig = WriteConfig(
        _newline: "",
        _indent: "",
        _usesSpaceAfterSeparators: false,
        _htmlSafe: false,
        _timeFormat: DateTimeFormat.RFC3339
    )

    public static let pretty: WriteConfig = WriteConfig(
        _newline: "\n",
        _indent: "    ",
        _usesSpaceAfterSeparators: true,
        _htmlSafe: false,
        _timeFormat: DateTimeFormat.RFC3339
    )
}