/*
 * 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 output stream for writing EXI encoded data
    /// </summary>
    public class BitOutputStream
    {
        private byte[] _buffer;
        private int _position;
        private int _bitPosition;
        private int _capacity;

        public BitOutputStream(int capacity = EXIConstants.BUFFER_SIZE)
        {
            _capacity = capacity;
            _buffer = new byte[capacity];
            _position = 0;
            _bitPosition = 0;
        }

        public int Position => _position;
        public int BitPosition => _bitPosition;
        public int Capacity => _capacity;

        /// <summary>
        /// Write a single bit
        /// </summary>
        /// <param name="bit">Bit value (0 or 1)</param>
        public void WriteBit(int bit)
        {
            if (bit != 0 && bit != 1)
                throw new ArgumentException("Bit value must be 0 or 1", nameof(bit));

            EnsureCapacity(_position + 1);

            if (bit == 1)
            {
                _buffer[_position] |= (byte)(1 << (7 - _bitPosition));
            }

            _bitPosition++;
            if (_bitPosition == 8)
            {
                _bitPosition = 0;
                _position++;
            }
        }

        /// <summary>
        /// Write multiple bits from unsigned integer
        /// </summary>
        /// <param name="value">Value to write</param>
        /// <param name="numBits">Number of bits to write (1-32)</param>
        public void WriteBits(uint value, int numBits)
        {
            if (numBits < 1 || numBits > 32)
                throw new ArgumentException("Number of bits must be between 1 and 32", nameof(numBits));

            for (int i = numBits - 1; i >= 0; i--)
            {
                int bit = (int)((value >> i) & 1);
                WriteBit(bit);
            }
        }

        /// <summary>
        /// Write unsigned integer using EXI encoding
        /// </summary>
        /// <param name="value">Unsigned integer value</param>
        public void WriteUnsignedInteger(uint value)
        {
            if (value == 0)
            {
                WriteByte(0);
                return;
            }

            // Calculate number of bytes needed
            var bytes = new List<byte>();
            
            while (value > 0)
            {
                byte currentByte = (byte)(value & 0x7F);
                value >>= 7;
                
                if (value > 0)
                    currentByte |= 0x80; // Set continuation bit
                    
                bytes.Add(currentByte);
            }

            // Write bytes in reverse order (big-endian)
            for (int i = bytes.Count - 1; i >= 0; i--)
            {
                WriteByte(bytes[i]);
            }
        }

        /// <summary>
        /// Write signed integer using EXI encoding
        /// </summary>
        /// <param name="value">Signed integer value</param>
        public void WriteInteger(int value)
        {
            // Encode sign in LSB, shift value
            uint unsignedValue;
            if (value < 0)
            {
                unsignedValue = ((uint)(-value) << 1) | 1;
            }
            else
            {
                unsignedValue = (uint)value << 1;
            }

            WriteUnsignedInteger(unsignedValue);
        }

        /// <summary>
        /// Write a byte aligned to byte boundary
        /// </summary>
        /// <param name="value">Byte value</param>
        public void WriteByte(byte value)
        {
            // Align to byte boundary
            if (_bitPosition != 0)
            {
                _bitPosition = 0;
                _position++;
            }

            EnsureCapacity(_position + 1);
            _buffer[_position++] = value;
        }

        /// <summary>
        /// Write multiple bytes
        /// </summary>
        /// <param name="data">Byte array to write</param>
        public void WriteBytes(byte[] data)
        {
            if (data == null || data.Length == 0)
                return;

            // Align to byte boundary
            if (_bitPosition != 0)
            {
                _bitPosition = 0;
                _position++;
            }

            EnsureCapacity(_position + data.Length);
            Array.Copy(data, 0, _buffer, _position, data.Length);
            _position += data.Length;
        }

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

        /// <summary>
        /// Get the written data as byte array
        /// </summary>
        /// <returns>Byte array containing written data</returns>
        public byte[] ToArray()
        {
            int length = _position + (_bitPosition > 0 ? 1 : 0);
            var result = new byte[length];
            Array.Copy(_buffer, result, length);
            return result;
        }

        /// <summary>
        /// Get the current buffer length in bytes
        /// </summary>
        /// <returns>Length in bytes</returns>
        public int GetLength()
        {
            return _position + (_bitPosition > 0 ? 1 : 0);
        }

        /// <summary>
        /// Reset the stream position to beginning
        /// </summary>
        public void Reset()
        {
            _position = 0;
            _bitPosition = 0;
            Array.Clear(_buffer, 0, _buffer.Length);
        }

        /// <summary>
        /// Ensure buffer has enough capacity
        /// </summary>
        /// <param name="requiredSize">Required size in bytes</param>
        private void EnsureCapacity(int requiredSize)
        {
            if (requiredSize > _capacity)
            {
                int newCapacity = Math.Max(_capacity * 2, requiredSize);
                var newBuffer = new byte[newCapacity];
                Array.Copy(_buffer, newBuffer, _position);
                _buffer = newBuffer;
                _capacity = newCapacity;
            }
        }

        /// <summary>
        /// Get current buffer usage statistics
        /// </summary>
        /// <returns>Usage information</returns>
        public (int UsedBytes, int TotalCapacity, double UsagePercentage) GetUsageStats()
        {
            int usedBytes = GetLength();
            double usage = (double)usedBytes / _capacity * 100.0;
            return (usedBytes, _capacity, usage);
        }
    }
}