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

/**
 * @file
 *
 * This is a library for BufferedOutputStream class.
 */

package std.io

/**
 * A BufferedOutputStream has a built-in buffer to cache the content of the OutputStream.
 * BufferedOutputStream is meant for cache the OutputStream.
 */
public class BufferedOutputStream<T> <: OutputStream where T <: OutputStream {
    var outputStream: T
    var outBuf: Array<Byte>
    var curPos: Int64

    /**
     * Constructor
     *
     *
     * @params output - The OutputStream
     */
    public init(output: T) {
        this(output, DEFAULT_BUFFER_CAPACITY)
    }

    public init(output: T, buffer: Array<Byte>) {
        if (buffer.size == 0) {
            throw IllegalArgumentException("The buffer cannot be empty.")
        }

        outputStream = output
        outBuf = buffer
        curPos = 0
    }

    /**
     * Constructor
     *
     * @params output - The OutputStream
     * @params capacity - Capacity of the buit-in buffer
     *
     * @throws IllegalArgumentException - If `capacity` less than 0.
     */
    public init(output: T, capacity: Int64) {
        if (capacity <= 0) {
            throw IllegalArgumentException("Invalid capacity size: capacity = ${capacity}.")
        }

        outputStream = output
        outBuf = Array<Byte>(capacity, repeat: 0)
        curPos = 0
    }

    /**
     * Write from buffer to the OutputStream.
     *
     * @params buffer - Will write to the OutputStream from the buffer.
     */
    public func write(buffer: Array<Byte>): Unit {
        let len = buffer.size
        if (len == 0) {
            return
        }

        let outBufSize = outBuf.size
        if (len >= outBufSize) {
            flushOutBuf()
            outputStream.write(buffer)
            return
        }

        if (len > outBufSize - curPos) {
            flushOutBuf()
        }
        buffer.copyTo(outBuf, 0, curPos, len)
        curPos += len
    }

    /**
     * Write a Byte to the OutputStream.
     *
     * @params v - The byte write to the OutputStream.
     */
    public func writeByte(v: Byte): Unit {
        let outBufSize = outBuf.size
        if (curPos == outBufSize) {
            flushOutBuf()
        }
        outBuf[curPos] = v
        curPos++
    }

    func flushOutBuf(): Unit {
        if (curPos > 0) {
            outputStream.write(outBuf.slice(0, curPos))
            curPos = 0
        }
    }

    /**
     * Flush the OutputStream.
     */
    public func flush(): Unit {
        flushOutBuf()
        outputStream.flush()
    }

    /**
     * Flush and bind this.outputSteam to the new OutputStream.
     * Will not change the capacity.
     *
     * @params output - The OutputStream
     */
    public func reset(output: T): Unit {
        flush()
        outputStream = output
        curPos = 0
    }
}

extend<T> BufferedOutputStream<T> <: Resource where T <: Resource {
    /**
     * Close the current stream.
     */
    public func close(): Unit {
        if (!isClosed()) {
            try {
                flush()
            } finally {
                outputStream.close()
            }
        }
    }

    /**
     * Returns whether the current flow is closed.
     *
     * @return true if the current stream has been closed, otherwise returns false.
     */
    public func isClosed(): Bool {
        outputStream.isClosed()
    }
}

extend<T> BufferedOutputStream<T> <: Seekable where T <: Seekable {
    /**
     * Seek to an offset, in bytes, in a stream.
     *
     * @params sp - Start position of the offset and size of the offset.
     *
     * @return the number of bytes in the stream from the beginning of the data to the cursor position.
     */
    public func seek(sp: SeekPosition): Int64 {
        this.flush()
        outputStream.seek(sp)
    }

    /**
     * @return the position of the current cursor in the stream.
     */
    public prop position: Int64 {
        get() {
            outputStream.seek(Current(0))
        }
    }

    /**
     * @return the number of data bytes from the current cursor position to the end of the file.
     */
    public prop remainLength: Int64 {
        get() {
            let oldPos = outputStream.seek(Current(0))
            let length = outputStream.seek(End(0))
            if (length != oldPos) {
                outputStream.seek(Begin(oldPos))
            }

            length - oldPos
        }
    }

    /**
     * @return the number of bytes from the file header to the file trailer.
     */
    public prop length: Int64 {
        get() {
            let oldPos = outputStream.seek(Current(0))
            let length = outputStream.seek(End(0))
            if (length != oldPos) {
                outputStream.seek(Begin(oldPos))
            }

            length
        }
    }
}