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

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

package std.io

/**
 * A BufferedInputStream has a built-in buffer to cache the content of the InputStream.
 * BufferedInputStream is meant for cache the InputStream.
 */
public class BufferedInputStream<T> <: InputStream where T <: InputStream {
    var inputStream: T
    let inBuf: Array<Byte>
    var curPos: Int64 = 0
    var availLen: Int64 = 0

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

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

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

    /**
     * Read the current InputSteam into the buffer.
     *
     * @params buffer - Will read from InputStream to the buffer.
     *
     * @return Size read into the buffer.
     * @throws IllegalArgumentException - If the `buffer` is empty.
     */
    public func read(buffer: Array<Byte>): Int64 {
        if (availLen == -1) {
            return 0
        }

        var len = buffer.size
        if (len == 0) {
            throw IllegalArgumentException("The buffer cannot be empty: `buffer.size() = 0`!")
        }

        var count = 0
        while (len > 0) {
            if (availLen == 0) {
                fillInBuf()
                if (availLen == -1) {
                    break
                }
            }

            let copyLen = if (availLen <= len) {
                availLen
            } else {
                len
            }

            inBuf.copyTo(buffer, curPos, count, copyLen)
            curPos += copyLen
            availLen -= copyLen
            count += copyLen
            len -= copyLen
        }

        count
    }

    public func readByte(): ?Byte {
        if (availLen == -1) {
            return None
        }

        if (availLen == 0) {
            fillInBuf()
            if (availLen == -1) {
                return None
            }
        }

        availLen--
        let readByte = inBuf[curPos]
        curPos++

        readByte
    }

    /**
     * Bind this.inputStream to the new InputStream.
     * Will not change the capacity.
     *
     * @params input - The new InputStream
     */
    public func reset(input: T): Unit {
        inputStream = input
        curPos = 0
        availLen = 0
    }

    func fillInBuf(): Unit {
        let readNum = inputStream.read(inBuf)
        if (readNum > 0) {
            curPos = 0
            availLen = readNum
        } else {
            availLen = -1
        }
    }
}

extend<T> BufferedInputStream<T> <: Resource where T <: Resource {
    /**
     * Close the current stream.
     */
    public func close(): Unit {
        inputStream.close()
    }

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

extend<T> BufferedInputStream<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 {
        let pos = inputStream.seek(sp)
        curPos = 0
        availLen = 0

        pos
    }

    /**
     * @return the position of the current cursor in the stream.
     */
    public prop position: Int64 {
        get() {
            inputStream.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 = inputStream.seek(Current(0))
            let length = inputStream.seek(End(0))
            if (length != oldPos) {
                inputStream.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 = inputStream.seek(Current(0))
            let length = inputStream.seek(End(0))
            if (length != oldPos) {
                inputStream.seek(Begin(oldPos))
            }

            length
        }
    }
}