/*
 * Copyright (C) 2007-2024 C# Port
 * Original Copyright (C) 2007-2018 Siemens AG
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 */

namespace V2GDecoderNet.EXI
{
    /// <summary>
    /// Bit input stream for reading EXI encoded data
    /// </summary>
    public class BitInputStream
    {
        private readonly byte[] _buffer;
        private int _position;
        private int _bitPosition;
        private readonly int _size;

        public BitInputStream(byte[] buffer)
        {
            _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
            _size = buffer.Length;
            _position = 0;
            _bitPosition = 0;
        }

        public int Position => _position;
        public int BitPosition => _bitPosition;
        public int Size => _size;
        public bool IsEOF => _position >= _size;

        /// <summary>
        /// Read a single bit
        /// </summary>
        /// <returns>Bit value (0 or 1), or -1 on EOF</returns>
        public int ReadBit()
        {
            if (_position >= _size)
                return -1;

            int bit = (_buffer[_position] >> (7 - _bitPosition)) & 1;
            
            _bitPosition++;
            if (_bitPosition == 8)
            {
                _bitPosition = 0;
                _position++;
            }
            
            return bit;
        }

        /// <summary>
        /// Read multiple bits as unsigned integer
        /// </summary>
        /// <param name="numBits">Number of bits to read (1-32)</param>
        /// <returns>Unsigned integer value</returns>
        public uint ReadBits(int numBits)
        {
            if (numBits < 1 || numBits > 32)
                throw new ArgumentException("Number of bits must be between 1 and 32", nameof(numBits));

            uint result = 0;
            
            for (int i = 0; i < numBits; i++)
            {
                int bit = ReadBit();
                if (bit == -1)
                    throw new EXIException(EXIErrorCodes.EXI_ERROR_INPUT_STREAM_EOF);
                    
                result = (result << 1) | (uint)bit;
            }
            
            return result;
        }

        /// <summary>
        /// Read unsigned integer using EXI encoding
        /// </summary>
        /// <returns>Unsigned integer value</returns>
        public uint ReadUnsignedInteger()
        {
            uint result = 0;
            bool continueBit;
            
            do
            {
                if (_position >= _size)
                    throw new EXIException(EXIErrorCodes.EXI_ERROR_INPUT_STREAM_EOF);

                byte currentByte = _buffer[_position++];
                continueBit = (currentByte & 0x80) != 0;
                result = (result << 7) | (uint)(currentByte & 0x7F);
                
            } while (continueBit);
            
            return result;
        }

        /// <summary>
        /// Read signed integer using EXI encoding
        /// </summary>
        /// <returns>Signed integer value</returns>
        public int ReadInteger()
        {
            uint unsignedValue = ReadUnsignedInteger();
            
            // Check sign bit (LSB)
            bool isNegative = (unsignedValue & 1) != 0;
            int value = (int)(unsignedValue >> 1);
            
            return isNegative ? -value : value;
        }

        /// <summary>
        /// Read a byte aligned to byte boundary
        /// </summary>
        /// <returns>Byte value</returns>
        public byte ReadByte()
        {
            // Align to byte boundary
            if (_bitPosition != 0)
            {
                _bitPosition = 0;
                _position++;
            }
            
            if (_position >= _size)
                throw new EXIException(EXIErrorCodes.EXI_ERROR_INPUT_STREAM_EOF);
                
            return _buffer[_position++];
        }

        /// <summary>
        /// Read multiple bytes
        /// </summary>
        /// <param name="count">Number of bytes to read</param>
        /// <returns>Byte array</returns>
        public byte[] ReadBytes(int count)
        {
            if (count < 0)
                throw new ArgumentException("Count cannot be negative", nameof(count));

            // Align to byte boundary
            if (_bitPosition != 0)
            {
                _bitPosition = 0;
                _position++;
            }
            
            if (_position + count > _size)
                throw new EXIException(EXIErrorCodes.EXI_ERROR_INPUT_STREAM_EOF);

            var result = new byte[count];
            Array.Copy(_buffer, _position, result, 0, count);
            _position += count;
            
            return result;
        }

        /// <summary>
        /// Skip to next byte boundary
        /// </summary>
        public void AlignToByteBank()
        {
            if (_bitPosition != 0)
            {
                _bitPosition = 0;
                _position++;
            }
        }

        /// <summary>
        /// Reset stream position to beginning
        /// </summary>
        public void Reset()
        {
            _position = 0;
            _bitPosition = 0;
        }

        /// <summary>
        /// Set stream position
        /// </summary>
        /// <param name="position">Byte position</param>
        /// <param name="bitPosition">Bit position within byte (0-7)</param>
        public void SetPosition(int position, int bitPosition = 0)
        {
            if (position < 0 || position > _size)
                throw new ArgumentException("Position out of range", nameof(position));
                
            if (bitPosition < 0 || bitPosition > 7)
                throw new ArgumentException("Bit position must be 0-7", nameof(bitPosition));
                
            _position = position;
            _bitPosition = bitPosition;
        }

        /// <summary>
        /// Get remaining bytes in stream
        /// </summary>
        /// <returns>Number of remaining bytes</returns>
        public int GetRemainingBytes()
        {
            int remaining = _size - _position;
            if (_bitPosition > 0 && remaining > 0)
                remaining--;
            return Math.Max(0, remaining);
        }
    }
}