/**
 *  Class for packaging various data types into an array of bytes.
 *      Copyright 2020 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class BitStreamReader extends Object;

//  Array of bytes to read from
var private array<byte> stream;
var private int         currentBytePointer;
var private int         currentBitPointer;
//  Write `byte` from `stream[currentBytePointer]` to avoid unnecessary
//  array access calls.
var private byte        currentByte;

var private const array<byte> bitMask;
var private const array<byte> bitMaskBefore;
var private const array<byte> bitMaskAfter;

var private int     tempInt;
var private string  tempString;
var private byte    temp;
var private byte    result;
var private byte    rightBoundary;
var private byte    remainingToRead;
var private byte    byte1, byte2, byte3, byte4;

public final function Initialize(array<byte> newStream)
{
    stream = newStream;
    currentBytePointer  = 0;
    currentBitPointer   = 0;
    if (stream.length > 0) {
        currentByte = stream[0];
    }
}

private final function ShiftBytePointer()
{
    currentBitPointer = 0;
    currentBytePointer += 1;
    if (currentBytePointer < stream.length) {
        currentByte = stream[currentBytePointer];
    }
    else {
        currentByte = 0;
    }
}

public final function byte ReadBit()
{
    if (currentBytePointer >= stream.length) return 0;
    if ((currentByte & bitMask[currentBitPointer]) > 0) {
        currentBitPointer += 1;
        if (currentBitPointer >= 8) {
            ShiftBytePointer();
        }
        return 1;
    }
    currentBitPointer += 1;
    if (currentBitPointer >= 8) {
        ShiftBytePointer();
    }
    return 0;
}

public final function bool ReadBool()
{
    if (currentBytePointer >= stream.length) return false;
    if ((currentByte & bitMask[currentBitPointer]) > 0) {
        currentBitPointer += 1;
        if (currentBitPointer >= 8) {
            ShiftBytePointer();
        }
        return true;
    }
    currentBitPointer += 1;
    if (currentBitPointer >= 8) {
        ShiftBytePointer();
    }
    return false;
}

public final function byte ReadByte(optional int bitLimit)
{
    if (currentBytePointer >= stream.length) return 0;
    //  Default `bitLimit`
    if (bitLimit <= 0 || bitLimit > 8) {
        bitLimit = 8;
    }
    //  Throw away already read part
    result = currentByte & bitMaskAfter[currentBitPointer];
    if (bitLimit <= 8 - currentBitPointer)
    {
        //  If all we are asked to read is contained in cuurent `byte` -
        //  just throw away excessive info by shifting to the right
        result = result >>> (8 - currentBitPointer - bitLimit);
        currentBitPointer += bitLimit;
        if (currentBitPointer >= 8) {
            ShiftBytePointer();
        }
        return result;
    }
    //  Otherwise shift already read part up
    rightBoundary = bitLimit - (8 - currentBitPointer);
    result = result << rightBoundary;
    //  Move to the next byte
    ShiftBytePointer();
    currentBitPointer = rightBoundary;
    //  Record required part from the second byte
    temp = currentByte & bitMaskBefore[rightBoundary];
    temp = temp >>> (8 - rightBoundary);
    result = result | temp;
    return result;
}

public final function int ReadInt(optional int bitLimit)
{
    //  Default `bitLimit`
    if (bitLimit <= 0 || bitLimit > 32) {
        bitLimit = 32;
    }
    byte1 = ReadByte(bitLimit);
    if (bitLimit <= 8) {
        return byte1;
    }
    bitLimit -= 8;
    byte2 = ReadByte(bitLimit);
    if (bitLimit <= 8)
    {
        tempInt = byte1 << bitLimit;
        tempInt = tempInt | byte2;
        return tempInt;
    }
    bitLimit -= 8;
    byte3 = ReadByte(bitLimit);
    if (bitLimit <= 8)
    {
        tempInt = byte1 << (bitLimit + 8);
        tempInt = tempInt | (byte2 << bitLimit);
        tempInt = tempInt | byte3;
        return tempInt;
    }
    bitLimit -= 8;
    byte4 = ReadByte(bitLimit);
    tempInt = byte1 << (bitLimit + 16);
    tempInt = tempInt | (byte2 << (bitLimit + 8));
    tempInt = tempInt | (byte3 << bitLimit);
    tempInt = tempInt | byte4;
    return tempInt;
}

public final function float ReadFloat(
    int             precisionLevel,
    optional byte   bitLimit)
{
    if (bitLimit < 0 || bitLimit > 32) {
        bitLimit = 32;
    }
    precisionLevel = Max(0, precisionLevel);
    return float(ReadInt(bitLimit)) * (0.1 ** precisionLevel);
}

public final function string ReadString(int length)
{
    if (length <= 0) return "";
    while (length > 0)
    {
        length -= 1;
        tempString $= Chr(ReadByte());
    }
    return tempString;
}

//      String representation of `class`es has a more limited character range,
//  which allows us to fit every chgaracter in `class`' name into 6 bits,
//  saving 25% space.
//      Allowed characters are: digits, upper and lower case letters, dot '.'
//  and underscore '_'.
//      Any other symbol is converted into an underscore.
private final function byte CompressClassCharacter(byte source)
{
    //  26 upper case character
    if (source >= 65 && source <= 90) {
        return source - 65;
    }
    //  26 lower case character // 52 total
    if (source >= 97 && source <= 122) {
        return 26 + (source - 97);
    }
    //  10 digits // 62 total
    if (source >= 48 && source <= 57) {
        return 52 + (source - 48);
    }
    //  dot
    if (source == 46) {
        return 62;
    }
    //  underscore and everything else
    return 63;
}

private final function byte DecompressClassCharacter(byte source)
{
    if (source >= 63) return 95;
    //  26 upper case character
    if (source <= 25) {
        return source + 65;
    }
    //  26 lower case character
    if (source <= 51) {
        return source + 71;
    }
    //  10 digits
    if (source <= 61) {
        return source - 4;
    }
    //  dot
    //  if (source == 62)
    return 46;
}

public final function string ReadClassName(int length)
{
    if (length <= 0) return "";
    while (length > 0)
    {
        length -= 1;
        tempString $= Chr(DecompressClassCharacter(ReadByte(6)));
    }
    return tempString;
}

defaultproperties
{
    bitMask(0) = 128        //  1 0 0 0 0 0 0 0
    bitMask(1) = 64         //  0 1 0 0 0 0 0 0
    bitMask(2) = 32         //  0 0 1 0 0 0 0 0
    bitMask(3) = 16         //  0 0 0 1 0 0 0 0
    bitMask(4) = 8          //  0 0 0 0 1 0 0 0
    bitMask(5) = 4          //  0 0 0 0 0 1 0 0
    bitMask(6) = 2          //  0 0 0 0 0 0 1 0
    bitMask(7) = 1          //  0 0 0 0 0 0 0 1
    bitMaskBefore(0) = 128  //  1 0 0 0 0 0 0 0
    bitMaskBefore(1) = 192  //  1 1 0 0 0 0 0 0
    bitMaskBefore(2) = 224  //  1 1 1 0 0 0 0 0
    bitMaskBefore(3) = 240  //  1 1 1 1 0 0 0 0
    bitMaskBefore(4) = 248  //  1 1 1 1 1 0 0 0
    bitMaskBefore(5) = 252  //  1 1 1 1 1 1 0 0
    bitMaskBefore(6) = 254  //  1 1 1 1 1 1 1 0
    bitMaskBefore(7) = 255  //  1 1 1 1 1 1 1 1
    bitMaskAfter(0) = 255   //  1 1 1 1 1 1 1 1
    bitMaskAfter(1) = 127   //  0 1 1 1 1 1 1 1
    bitMaskAfter(2) = 63    //  0 0 1 1 1 1 1 1
    bitMaskAfter(3) = 31    //  0 0 0 1 1 1 1 1
    bitMaskAfter(4) = 15    //  0 0 0 0 1 1 1 1
    bitMaskAfter(5) = 7     //  0 0 0 0 0 1 1 1
    bitMaskAfter(6) = 3     //  0 0 0 0 0 0 1 1
    bitMaskAfter(7) = 1     //  0 0 0 0 0 0 0 1
}