/**
 *  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 BitStreamWriter extends Object;

//  Array of fully written bytes
var private array<byte> stream;
//      Last byte that still has some space to fit data into.
//      We start writing data into it's least significant bits and shift them up
//  when we want to write more data inside or when we are asked to return
//  recorded data.
var private byte        unfinishedByte;
//  How much space is left in `unfinishedByte`
var private byte        bitsLeft;

//  Can be used to erase unnecessary bits from a byte
var private const array<byte> bitMaskAfter;

//  We'll declare all auxiliary variables as globals to avoid their
//  unnecessary creation inside functions, to make them more lightweight.
var private byte    shift, altShift;
var private byte    temp;
var private int     tempInt;
var private byte    byte1, byte2, byte3, byte4;

/**
 *  Resets contents of the `BitStreamWriter`, making it the same as a
 *  brand new writer.
 *
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter InitializeStream()
{
    stream.length   = 0;
    unfinishedByte  = 0;
    bitsLeft        = 8;
    return self;
}

/**
 *  Records a bit inside a `BitStreamWriter`.
 *
 *  @param  source  Byte that defines a bit. We don't look at any actual bits in
 *      `source`'s representation, but simply consired the bit to be
 *      `1` if `source > 0` and `0` if `source == 0`.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteBit(byte source)
{
    unfinishedByte = unfinishedByte << 1;
    if (source > 0) {
        unfinishedByte += 1;
    }
    bitsLeft -= 1;
    if (bitsLeft <= 0) {
        stream[stream.length] = unfinishedByte;
        unfinishedByte  = 0;
        bitsLeft        = 8;
    }
    return self;
}

/**
 *  Records a bit inside a `BitStreamWriter`.
 *
 *  @param  source  Boolean value that defines a bit. `true` means bit is equal
 *      to `1` and `false` means that it is equal to `0`.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteBoolean(bool isOne)
{
    unfinishedByte = unfinishedByte << 1;
    if (isOne) {
        unfinishedByte += 1;
    }
    //  Update storage
    bitsLeft -= 1;
    if (bitsLeft <= 0) {
        stream[stream.length] = unfinishedByte;
        unfinishedByte  = 0;
        bitsLeft        = 8;
    }
    return self;
}

/**
 *  Records a `byte` inside a `BitStreamWriter`.
 *
 *  @param  source      Boolean value that defines a bit. `true` means bit is
 *      equal to `1` and `false` means that it is equal to `0`.
 *  @param  bitLimit    How many of the (least significant) bits to record;
 *      can be used to compress values that don't need the full `byte`
 *      value range.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteByte(
    byte            source,
    optional byte   bitLimit)
{
    //  Default `bitLimit`
    if (bitLimit < 0 || bitLimit > 8) {
        bitLimit = 8;
    }
    //  Zero unnecessary bits
    source = source & bitMaskAfter[8 - bitLimit];
    if (bitLimit < bitsLeft)
    {
        //  We have enough space to fit all the bits in `unfinishedByte`
        unfinishedByte = unfinishedByte << bitLimit;
        unfinishedByte = unfinishedByte | source;
        //  Update storage
        bitsLeft -= bitLimit;
        if (bitsLeft <= 0)
        {
            stream[stream.length] = unfinishedByte;
            unfinishedByte  = 0;
            bitsLeft        = 8;
        }
        return self;
    }
    //  If we don't have enough space, - record what can fit in
    //  current `unfinishedByte`
    unfinishedByte = unfinishedByte << bitsLeft;
    altShift = bitLimit - bitsLeft;
    temp = source >>> altShift;
    unfinishedByte = unfinishedByte | temp;
    //  And add it to the storage
    stream[stream.length] = unfinishedByte;
    //  Create new byte by erasing recorded bits from the `source`
    temp = temp << altShift;
    unfinishedByte  = source ^ temp;
    bitsLeft        = 8 - altShift;
    return self;
}

/**
 *  Records an `int` inside a `BitStreamWriter`.
 *
 *  @param  source      Integer value to record into `BitStreamWriter`.
 *  @param  bitLimit    How many of the (least significant) bits to record;
 *      can be used to compress values that don't need the full `int`
 *      value range.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteInt(
    int             source,
    optional byte   bitLimit)
{
    //  Default `bitLimit`
    if (bitLimit == 0) {
        bitLimit = 32;
    }
    byte1 = byte((source & 0xff000000) >>> 24);
    byte2 = byte((source & 0x00ff0000) >>> 16);
    byte3 = byte((source & 0x0000ff00) >>> 8);
    byte4 = source & 0x000000ff;
    if (bitLimit > 24) {
        WriteByte(byte1, bitLimit - 24);
    }
    if (bitLimit > 16) {
        WriteByte(byte2, bitLimit - 16);
    }
    if (bitLimit > 8) {
        WriteByte(byte3, bitLimit - 8);
    }
    WriteByte(byte4, bitLimit);
    return self;
}

/**
 *  Records an `float` inside a `BitStreamWriter`.
 *
 *      `floats` can only be recorded with a specified amount of decimal places.
 *  This is because we can't really get precise float bit representation or
 *  easily truncate it's value bit-wise, so we multiply it by a specified
 *  power of 10 and then transfer integer part of the result as `int`.
 *      Specified precision level IS NOT recorded into `BitStreamWriter`.
 *
 *  @param  source          Integer value to record into `BitStreamWriter`.
 *  @param  precisionLevel  How many decimal places after the dot to record.
 *  @param  bitLimit        How many of the (least significant) bits to record;
 *      can be used to compress values that don't need the full value range of
 *      truncated float.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteFloat(
    float           source,
    int             precisionLevel,
    optional byte   bitLimit)
{
    if (bitLimit < 0 || bitLimit > 32) {
        bitLimit = 32;
    }
    precisionLevel = Max(0, precisionLevel);
    tempInt = int(Round( source * (10 ** precisionLevel) ));
    WriteInt(tempInt, bitLimit);
    return self;
}

/**
 *  Records an `string` inside a `BitStreamWriter`, treating each code point as
 *  a byte, which will lead to loss of data when recording `string`s that
 *  contain code points with values `> 255`.
 *
 *  Does not record `string`'s length or where it ends.
 *
 *  @param  source  `string` value to record into `BitStreamWriter`.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteString(string source)
{
    while (source != "")
    {
        WriteByte(Asc(source));
        source = Mid(0, 1);
    }
    return self;
}

/**
 *  Records an string representation of a `class` name inside
 *  a `BitStreamWriter`, compressing it's characters to 6 bit representation.
 *
 *      Allowed characters are: digits, upper and lower case letters, dot '.'
 *  and underscore '_'.
 *      Any other character will be converted into underscore.
 *
 *  Does not record `string`'s length or where it ends.
 *
 *  @param  source  String representation of a `class`' name value to record
 *      into `BitStreamWriter`.
 *  @return `BitStreamWriter` to allow for function chaining.
 */
public final function BitStreamWriter WriteClassName(string source)
{
    while (source != "")
    {
        WriteByte(CompressClassCharacter(Asc(source)), 6);
        source = Mid(source, 1);
    }
    return self;
}

//      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 (`95`) and everything else
    return 63;
}

/**
 *  Returns all data recorded so far in a caller `BitStreamWriter` as
 *  an array of bytes. Data is written ion order, starting from bytes with
 *  lesser indecies and their most significant bits.
 *
 *  @return Array of bytes that contains all data written into caller
 *      `BitStreamWriter`.
 */
public final function array<byte> GetData()
{
    local array<byte> result;
    result = stream;
    if (bitsLeft < 8) {
        result[result.length] = unfinishedByte << bitsLeft;
    }
    return result;
}

/**
 *  Returns amount of data (in bits) recorded into caller `BitStreamWriter`.
 *
 *  @return Amount (in bits) of recorded data.
 */
public final function int GetSize()
{
    local int sizeInBits;
    sizeInBits = stream.length * 8;
    if (bitsLeft < 8) {
        sizeInBits += (8 - bitsLeft);
    }
    return sizeInBits;
}

/**
 *  Returns amount of data (in bytes) recorded into caller `BitStreamWriter`.
 *
 *  @param  onlyFullBytes   Only count filled bytes, i.e. if last byte only has
 *      5 bits (or any amount `<8`) of info written in it -
 *      method will not count it.
 *  @return Amount (in bytes) of recorded data.
 */
public final function int GetSizeInBytes(optional bool onlyFullBytes)
{
    local int sizeInBytes;
    sizeInBytes = stream.length;
    if (!onlyFullBytes && bitsLeft < 8) {
        sizeInBytes += 1;
    }
    return sizeInBytes;
}

defaultproperties
{
    //  With this we do not need to call `Initialize` to use a newly created
    //  `BitStreamWriter`
    bitsLeft = 8
    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
}