/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
 */
package cbor4cj

import std.collection.LinkedList
import std.collection.ArrayList
import std.io.IOException
import std.io.InputStream
import std.io.ByteBuffer

/**
 * Decoder for the CBOR format based.
 */
public class CborDecoder {
    private let inputStream: InputStream

    private var unsignedIntegerDecoder: ?UnsignedIntegerDecoder = None

    private var negativeIntegerDecoder: ?NegativeIntegerDecoder = None

    private var byteStringDecoder: ?ByteStringDecoder = None

    private var unicodeStringDecoder: ?UnicodeStringDecoder = None

    private var arrayDecoder: ?ArrayDecoder = None

    private var mapDecoder: ?MapDecoder = None

    private var tagDecoder: ?TagDecoder = None

    private var specialDecoder: ?SpecialDecoder = None

    private var autoDecodeInfinitiveArrays = true

    private var autoDecodeInfinitiveMaps = true

    private var autoDecodeInfinitiveByteStrings = true

    private var autoDecodeInfinitiveUnicodeStrings = true

    private var autoDecodeRationalNumbers = true

    private var autoDecodeLanguageTaggedStrings = true

    private var rejectDuplicateKeys = false

    public init(inputStream: InputStream) {
        //Objects.requireNonNull(inputStream)
        this.inputStream = inputStream
        unsignedIntegerDecoder = UnsignedIntegerDecoder(this, inputStream)
        unsignedIntegerDecoder = UnsignedIntegerDecoder(this, inputStream)
        unsignedIntegerDecoder = UnsignedIntegerDecoder(this, inputStream)
        negativeIntegerDecoder = NegativeIntegerDecoder(this, inputStream)
        byteStringDecoder = ByteStringDecoder(this, inputStream)
        unicodeStringDecoder = UnicodeStringDecoder(this, inputStream)
        arrayDecoder = ArrayDecoder(this, inputStream)
        mapDecoder = MapDecoder(this, inputStream)
        tagDecoder = TagDecoder(this, inputStream)
        specialDecoder = SpecialDecoder(this, inputStream)
    }

    /**
     * Convenience method to decode a byte array directly.
     *
     * @param bytes
     *            the CBOR encoded data
     * @return a list of {@link DataItem}s
     * @throws CborException
     *             if decoding failed
     */
    public static func decodeStatic(bytes: Array<UInt8>): LinkedList<DataItem> {
        var stream = ByteBuffer()
        stream.write(bytes)
        return CborDecoder(stream).decode()
    }

    /**
     * Decode the {@link InputStream} to a list of {@link DataItem}s.
     *
     * @return the list of {@link DataItem}s
     * @throws CborException
     *             if decoding failed
     */
    public func decode(): LinkedList<DataItem> {
        let dataItems = LinkedList<DataItem>()
        var dataItem: ?DataItem
        dataItem = decodeNext()
        while (dataItem.isSome()) {
            dataItems.addLast(dataItem.getOrThrow())
            dataItem = decodeNext()
        }
        return dataItems
    }

    /**
     * Streaming decoding of an input stream. On each decoded DataItem, the
     * callback listener is invoked.
     *
     * @param dataItemListener
     *            the callback listener
     * @throws CborException
     *             if decoding failed
     */
    public func decode(dataItemListener: DataItemListener): Unit {
        //Objects.requireNonNull(dataItemListener)
        var dataItem = decodeNext()
        while (dataItem.isNone() == false) {
            dataItemListener.onDataItem(dataItem.getOrThrow())
            dataItem = decodeNext()
        }
    }

    /**
     * Decodes exactly one DataItem from the input stream.
     *
     * @return a {@link DataItem} or null if end of stream has reached.
     * @throws CborException
     *             if decoding failed
     */
    public func decodeNext(): ?DataItem {
        let symbol: Int32
        var readbyte: Int64 = 0
        try {
            var arr = Array<UInt8>(1, repeat: 0)
            readbyte = inputStream.read(arr)
            symbol = Int32(arr[0])
        } catch (ioException: IOException) {
            throw CborException(ioException)
        }
        if (readbyte == 0) {
            return None
        }
       
        match (MajorType.ofByte(symbol).name) {
            case "ARRAY" => return arrayDecoder.getOrThrow().decode(symbol)
            case "BYTE_STRING" => return byteStringDecoder.getOrThrow().decode(symbol)
            case "MAP" => return mapDecoder.getOrThrow().decode(symbol)
            case "NEGATIVE_INTEGER" => return negativeIntegerDecoder.getOrThrow().decode(symbol)
            case "UNICODE_STRING" => return unicodeStringDecoder.getOrThrow().decode(symbol)
            case "UNSIGNED_INTEGER" => return unsignedIntegerDecoder.getOrThrow().decode(symbol)
            case "SPECIAL" => return specialDecoder.getOrThrow().decode(symbol)
            case "TAG" =>
                let tag = tagDecoder.getOrThrow().decode(symbol)
                let next = decodeNext()
                if (next.isNone()) {
                    throw CborException("Unexpected end of stream: tag without following data item.")
                } else {
                    if (autoDecodeRationalNumbers && tag.getValue() == 30) {
                        return decodeRationalNumber(next.getOrThrow())
                    } else if (autoDecodeLanguageTaggedStrings && tag.getValue() == 38) {
                        return decodeLanguageTaggedString(next.getOrThrow())
                    } else {
                        var itemToTag = next
                        while (itemToTag.getOrThrow().hasTag()) {
                            itemToTag = itemToTag.getOrThrow().getTag().getOrThrow()
                        }
                        itemToTag.getOrThrow().setTag(tag)
                        return next
                    }
                }
            case "INVALID" => throw CborException("Not implemented major type " + symbol.toString())
            case _ => throw CborException("Not implemented major type " + symbol.toString())
        }
    }

    private func decodeLanguageTaggedString(dataItem: DataItem): DataItem {
        if (!(dataItem is CborArray)) {
            throw CborException("Error decoding LanguageTaggedString: not an array")
        }
        let array = (dataItem as CborArray).getOrThrow()
        if (Int32(array.getDataItems().size) != 2) {
            throw CborException("Error decoding LanguageTaggedString: array size is not 2")
        }
        let languageDataItem = array.getDataItems()[0]
        if (!(languageDataItem is UnicodeString)) {
            throw CborException("Error decoding LanguageTaggedString: first data item is not an UnicodeString")
        }
        let stringDataItem = array.getDataItems()[1]
        if (!(stringDataItem is UnicodeString)) {
            throw CborException("Error decoding LanguageTaggedString: second data item is not an UnicodeString")
        }
        let language = (languageDataItem as UnicodeString).getOrThrow()
        let string = (stringDataItem as UnicodeString).getOrThrow()
        return LanguageTaggedString(language, string)
    }

    private func decodeRationalNumber(dataItem: DataItem): DataItem {
        if (!(dataItem is CborArray)) {
            throw CborException("Error decoding RationalNumber: not an array")
        }
        let array = (dataItem as CborArray).getOrThrow()
        if (Int32(array.getDataItems().size) != 2) {
            throw CborException("Error decoding RationalNumber: array size is not 2")
        }
        let numeratorDataItem = array.getDataItems()[0]
        if (!(numeratorDataItem is Number)) {
            throw CborException("Error decoding RationalNumber: first data item is not a number")
        }
        let denominatorDataItem = array.getDataItems()[1]
        if (!(denominatorDataItem is Number)) {
            throw CborException("Error decoding RationalNumber: second data item is not a number")
        }
        let numerator = (numeratorDataItem as Number).getOrThrow()
        let denominator = (denominatorDataItem as Number).getOrThrow()
        return RationalNumber(numerator, denominator)
    }

    public func isAutoDecodeInfinitiveArrays(): Bool {
        return autoDecodeInfinitiveArrays
    }

    public func setAutoDecodeInfinitiveArrays(autoDecodeInfinitiveArrays: Bool): Unit {
        this.autoDecodeInfinitiveArrays = autoDecodeInfinitiveArrays
    }

    public func isAutoDecodeInfinitiveMaps(): Bool {
        return autoDecodeInfinitiveMaps
    }

    public func setAutoDecodeInfinitiveMaps(autoDecodeInfinitiveMaps: Bool): Unit {
        this.autoDecodeInfinitiveMaps = autoDecodeInfinitiveMaps
    }

    public func isAutoDecodeInfinitiveByteStrings(): Bool {
        return autoDecodeInfinitiveByteStrings
    }

    public func setAutoDecodeInfinitiveByteStrings(autoDecodeInfinitiveByteStrings: Bool): Unit {
        this.autoDecodeInfinitiveByteStrings = autoDecodeInfinitiveByteStrings
    }

    public func isAutoDecodeInfinitiveUnicodeStrings(): Bool {
        return autoDecodeInfinitiveUnicodeStrings
    }

    public func setAutoDecodeInfinitiveUnicodeStrings(autoDecodeInfinitiveUnicodeStrings: Bool): Unit {
        this.autoDecodeInfinitiveUnicodeStrings = autoDecodeInfinitiveUnicodeStrings
    }

    public func isAutoDecodeRationalNumbers(): Bool {
        return autoDecodeRationalNumbers
    }

    public func setAutoDecodeRationalNumbers(autoDecodeRationalNumbers: Bool): Unit {
        this.autoDecodeRationalNumbers = autoDecodeRationalNumbers
    }

    public func isAutoDecodeLanguageTaggedStrings(): Bool {
        return autoDecodeLanguageTaggedStrings
    }

    public func setAutoDecodeLanguageTaggedStrings(autoDecodeLanguageTaggedStrings: Bool): Unit {
        this.autoDecodeLanguageTaggedStrings = autoDecodeLanguageTaggedStrings
    }

    public func isRejectDuplicateKeys(): Bool {
        return rejectDuplicateKeys
    }

    public func setRejectDuplicateKeys(rejectDuplicateKeys: Bool): Unit {
        this.rejectDuplicateKeys = rejectDuplicateKeys
    }
}