#include <string.h>
#include <jim.h>

/* Provides the [pack] and [unpack] commands to pack and unpack
 * a binary string to/from arbitrary width integers and strings.
 *
 * This may be used to implement the [binary] command.
 */

/**
 * Big endian bit test.
 *
 * Considers 'bitvect' as a big endian bit stream and returns
 * bit 'b' as zero or non-zero.
 */
static int JimTestBitBigEndian(const unsigned char *bitvec, int b)
{
    div_t pos = div(b, 8);
    return bitvec[pos.quot] & (1 << (7 - pos.rem));
}

/**
 * Little endian bit test.
 *
 * Considers 'bitvect' as a little endian bit stream and returns
 * bit 'b' as zero or non-zero.
 */
static int JimTestBitLittleEndian(const unsigned char *bitvec, int b)
{
    div_t pos = div(b, 8);
    return bitvec[pos.quot] & (1 << pos.rem);
}

/**
 * Sign extends the given value, 'n' of width 'width' bits.
 *
 * For example, sign extending 0x80 with a width of 8, produces -128
 */
static jim_wide JimSignExtend(jim_wide n, int width)
{
    if (width == sizeof(jim_wide) * 8) {
        /* Can't sign extend the maximum size integer */
        return n;
    }
    if (n & ((jim_wide)1 << (width - 1))) {
        /* Need to extend */
        n -= ((jim_wide)1 << width);
    }

    return n;
}

/**
 * Big endian integer extraction.
 *
 * Considers 'bitvect' as a big endian bit stream.
 * Returns an integer of the given width (in bits)
 * starting at the given position (in bits).
 *
 * The pos/width must represent bits inside bitvec,
 * and the width be no more than the width of jim_wide.
 */
static jim_wide JimBitIntBigEndian(const unsigned char *bitvec, int pos, int width)
{
    jim_wide result = 0;
    int i;

    /* Aligned, byte extraction */
    if (pos % 8 == 0 && width % 8 == 0) {
        for (i = 0; i < width; i += 8) {
            result = (result << 8) + bitvec[(pos + i) / 8];
        }
        return result;
    }

    /* Unaligned */
    for (i = 0; i < width; i++) {
        if (JimTestBitBigEndian(bitvec, pos + width - i - 1)) {
            result |= ((jim_wide)1 << i);
        }
    }

    return result;
}

/**
 * Little endian integer extraction.
 *
 * Like JimBitIntBigEndian() but considers 'bitvect' as a little endian bit stream.
 */
static jim_wide JimBitIntLittleEndian(const unsigned char *bitvec, int pos, int width)
{
    jim_wide result = 0;
    int i;

    /* Aligned, byte extraction */
    if (pos % 8 == 0 && width % 8 == 0) {
        for (i = 0; i < width; i += 8) {
            result += (jim_wide)bitvec[(pos + i) / 8] << i;
        }
        return result;
    }

    /* Unaligned */
    for (i = 0; i < width; i++) {
        if (JimTestBitLittleEndian(bitvec, pos + i)) {
            result |= ((jim_wide)1 << i);
        }
    }

    return result;
}

/**
 * Big endian bit set.
 *
 * Considers 'bitvect' as a big endian bit stream and sets
 * bit 'b' to 'bit'
 */
static void JimSetBitBigEndian(unsigned char *bitvec, int b, int bit)
{
    div_t pos = div(b, 8);
    if (bit) {
        bitvec[pos.quot] |= (1 << (7 - pos.rem));
    }
    else {
        bitvec[pos.quot] &= ~(1 << (7 - pos.rem));
    }
}

/**
 * Little endian bit set.
 *
 * Considers 'bitvect' as a little endian bit stream and sets
 * bit 'b' to 'bit'
 */
static void JimSetBitLittleEndian(unsigned char *bitvec, int b, int bit)
{
    div_t pos = div(b, 8);
    if (bit) {
        bitvec[pos.quot] |= (1 << pos.rem);
    }
    else {
        bitvec[pos.quot] &= ~(1 << pos.rem);
    }
}

/**
 * Big endian integer packing.
 *
 * Considers 'bitvect' as a big endian bit stream.
 * Packs integer 'value' of the given width (in bits)
 * starting at the given position (in bits).
 *
 * The pos/width must represent bits inside bitvec,
 * and the width be no more than the width of jim_wide.
 */
static void JimSetBitsIntBigEndian(unsigned char *bitvec, jim_wide value, int pos, int width)
{
    int i;

    /* Common fast option */
    if (pos % 8 == 0 && width == 8) {
        bitvec[pos / 8] = value;
        return;
    }

    for (i = 0; i < width; i++) {
        int bit = !!(value & ((jim_wide)1 << i));
        JimSetBitBigEndian(bitvec, pos + width - i - 1, bit);
    }
}

/**
 * Little endian version of JimSetBitsIntBigEndian()
 */
static void JimSetBitsIntLittleEndian(unsigned char *bitvec, jim_wide value, int pos, int width)
{
    int i;

    /* Common fast option */
    if (pos % 8 == 0 && width == 8) {
        bitvec[pos / 8] = value;
        return;
    }

    for (i = 0; i < width; i++) {
        int bit = !!(value & ((jim_wide)1 << i));
        JimSetBitLittleEndian(bitvec, pos + i, bit);
    }
}

/**
 * Binary conversion of jim_wide integer to float
 *
 * Considers the least significant bits of
 * jim_wide 'value' as a IEEE float.
 *
 * Should work for both little- and big-endian platforms.
 */
static float JimIntToFloat(jim_wide value)
{
    int offs;
    float val;

    /* Skip offs to get to least significant bytes */
    offs = Jim_IsBigEndian() ? (sizeof(jim_wide) - sizeof(float)) : 0;

    memcpy(&val, (unsigned char *) &value + offs, sizeof(float));
    return val;
}

/**
 * Binary conversion of jim_wide integer to double
 *
 * Double precision version of JimIntToFloat
 */
static double JimIntToDouble(jim_wide value)
{
    int offs;
    double val;

    /* Skip offs to get to least significant bytes */
    offs = Jim_IsBigEndian() ? (sizeof(jim_wide) - sizeof(double)) : 0;

    memcpy(&val, (unsigned char *) &value + offs, sizeof(double));
    return val;
}

/**
 * Binary conversion of float to jim_wide integer
 *
 * Considers the bits of IEEE float 'value' as integer.
 * The integer is zero-extended to jim_wide.
 *
 * Should work for both little- and big-endian platforms.
 */
static jim_wide JimFloatToInt(float value)
{
    int offs;
    jim_wide val = 0;

    /* Skip offs to get to least significant bytes */
    offs = Jim_IsBigEndian() ? (sizeof(jim_wide) - sizeof(float)) : 0;

    memcpy((unsigned char *) &val + offs, &value, sizeof(float));
    return val;
}

/**
 * Binary conversion of double to jim_wide integer
 *
 * Double precision version of JimFloatToInt
 */
static jim_wide JimDoubleToInt(double value)
{
    int offs;
    jim_wide val = 0;

    /* Skip offs to get to least significant bytes */
    offs = Jim_IsBigEndian() ? (sizeof(jim_wide) - sizeof(double)) : 0;

    memcpy((unsigned char *) &val + offs, &value, sizeof(double));
    return val;
}

/**
 * [unpack]
 *
 * Usage: unpack binvalue -intbe|-intle|-uintbe|-uintle|-floatbe|-floatle|-str bitpos bitwidth
 *
 * Unpacks bits from $binvalue at bit position $bitpos and with $bitwidth.
 * Interprets the value according to the type and returns it.
 */
static int Jim_UnpackCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int option;
    static const char * const options[] = { "-intbe", "-intle", "-uintbe", "-uintle",
        "-floatbe", "-floatle", "-str", NULL };
    enum { OPT_INTBE, OPT_INTLE, OPT_UINTBE, OPT_UINTLE, OPT_FLOATBE, OPT_FLOATLE, OPT_STR, };
    jim_wide pos;
    jim_wide width;

    if (argc != 5) {
        Jim_WrongNumArgs(interp, 1, argv,
                "binvalue -intbe|-intle|-uintbe|-uintle|-floatbe|-floatle|-str bitpos bitwidth");
        return JIM_ERR;
    }
    if (Jim_GetEnum(interp, argv[2], options, &option, NULL, JIM_ERRMSG) != JIM_OK) {
        return JIM_ERR;
    }

    if (Jim_GetWide(interp, argv[3], &pos) != JIM_OK) {
        return JIM_ERR;
    }
    if (Jim_GetWide(interp, argv[4], &width) != JIM_OK) {
        return JIM_ERR;
    }

    if (option == OPT_STR) {
        int len;
        const char *str = Jim_GetString(argv[1], &len);

        if (width % 8 || pos % 8) {
            Jim_SetResultString(interp, "string field is not on a byte boundary", -1);
            return JIM_ERR;
        }

        if (pos >= 0 && width > 0 && pos < len * 8) {
            if (pos + width > len * 8) {
                width = len * 8 - pos;
            }
            Jim_SetResultString(interp, str + pos / 8, width / 8);
        }
        return JIM_OK;
    }
    else {
        int len;
        const unsigned char *str = (const unsigned char *)Jim_GetString(argv[1], &len);
        jim_wide result = 0;

        if (width > sizeof(jim_wide) * 8) {
            Jim_SetResultFormatted(interp, "int field is too wide: %#s", argv[4]);
            return JIM_ERR;
        }

        if (pos >= 0 && width > 0 && pos < len * 8) {
            if (pos + width > len * 8) {
                width = len * 8 - pos;
            }
            if (option == OPT_INTBE || option == OPT_UINTBE || option == OPT_FLOATBE) {
                result = JimBitIntBigEndian(str, pos, width);
            }
            else {
                result = JimBitIntLittleEndian(str, pos, width);
            }
            if (option == OPT_INTBE || option == OPT_INTLE) {
                result = JimSignExtend(result, width);
            }

        }

        if (option == OPT_FLOATBE || option == OPT_FLOATLE) {
            double fresult;
            if (width == 32) {
                fresult = (double) JimIntToFloat(result);
            } else if (width == 64) {
                fresult = JimIntToDouble(result);
            } else {
                Jim_SetResultFormatted(interp, "float field has bad bitwidth: %#s", argv[4]);
                return JIM_ERR;
            }
            Jim_SetResult(interp, Jim_NewDoubleObj(interp, fresult));
        } else {
            Jim_SetResultInt(interp, result);
        }
        return JIM_OK;
    }
}

/**
 * [pack]
 *
 * Usage: pack varname value -intbe|-intle|-floatle|-floatbe|-str width ?bitoffset?
 *
 * Packs the binary representation of 'value' into the variable of the given name.
 * The value is packed according to the given type, width and bitoffset.
 * The variable is created if necessary (like [append])
 * Ihe variable is expanded if necessary
 */
static int Jim_PackCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int option;
    static const char * const options[] = { "-intle", "-intbe", "-floatle", "-floatbe",
        "-str", NULL };
    enum { OPT_LE, OPT_BE, OPT_FLOATLE, OPT_FLOATBE, OPT_STR };
    jim_wide pos = 0;
    jim_wide width;
    jim_wide value;
    double fvalue;
    Jim_Obj *stringObjPtr;
    int len;
    int freeobj = 0;

    if (argc != 5 && argc != 6) {
        Jim_WrongNumArgs(interp, 1, argv,
                "varName value -intle|-intbe|-floatle|-floatbe|-str bitwidth ?bitoffset?");
        return JIM_ERR;
    }
    if (Jim_GetEnum(interp, argv[3], options, &option, NULL, JIM_ERRMSG) != JIM_OK) {
        return JIM_ERR;
    }
    if ((option == OPT_LE || option == OPT_BE) &&
            Jim_GetWide(interp, argv[2], &value) != JIM_OK) {
        return JIM_ERR;
    }
    if ((option == OPT_FLOATLE || option == OPT_FLOATBE) &&
            Jim_GetDouble(interp, argv[2], &fvalue) != JIM_OK) {
        return JIM_ERR;
    }
    if (Jim_GetWide(interp, argv[4], &width) != JIM_OK) {
        return JIM_ERR;
    }
    if (width <= 0 || (option == OPT_STR && width % 8) || (option != OPT_STR && width > sizeof(jim_wide) * 8) ||
       ((option == OPT_FLOATLE || option == OPT_FLOATBE) && width != 32 && width != 64)) {
        Jim_SetResultFormatted(interp, "bad bitwidth: %#s", argv[4]);
        return JIM_ERR;
    }
    if (argc == 6) {
        if (Jim_GetWide(interp, argv[5], &pos) != JIM_OK) {
            return JIM_ERR;
        }
        if (pos < 0 || (option == OPT_STR && pos % 8)) {
            Jim_SetResultFormatted(interp, "bad bitoffset: %#s", argv[5]);
            return JIM_ERR;
        }
    }

    stringObjPtr = Jim_GetVariable(interp, argv[1], JIM_UNSHARED);
    if (!stringObjPtr) {
        /* Create the string if it doesn't exist */
        stringObjPtr = Jim_NewEmptyStringObj(interp);
        freeobj = 1;
    }
    else if (Jim_IsShared(stringObjPtr)) {
        freeobj = 1;
        stringObjPtr = Jim_DuplicateObj(interp, stringObjPtr);
    }

    len = Jim_Length(stringObjPtr) * 8;

    /* Extend the string as necessary first */
    while (len < pos + width) {
        Jim_AppendString(interp, stringObjPtr, "", 1);
        len += 8;
    }

    Jim_SetResultInt(interp, pos + width);

    /* Now set the bits. Note that the the string *must* have no non-string rep
     * since we are writing the bytes directly.
     */
    Jim_AppendString(interp, stringObjPtr, "", 0);

    /* Convert floating point to integer if necessary */
    if (option == OPT_FLOATLE || option == OPT_FLOATBE) {
        /* Note that the following is slightly incompatible with Tcl behaviour.
         * In Tcl floating overflow gives FLT_MAX (cf. test binary-13.13).
         * In Jim Tcl it gives Infinity. This behavior may change.
         */
        value = (width == 32) ? JimFloatToInt((float)fvalue) : JimDoubleToInt(fvalue);
    }

    if (option == OPT_BE || option == OPT_FLOATBE) {
        JimSetBitsIntBigEndian((unsigned char *)stringObjPtr->bytes, value, pos, width);
    }
    else if (option == OPT_LE || option == OPT_FLOATLE) {
        JimSetBitsIntLittleEndian((unsigned char *)stringObjPtr->bytes, value, pos, width);
    }
    else {
        pos /= 8;
        width /= 8;

        if (width > Jim_Length(argv[2])) {
            width = Jim_Length(argv[2]);
        }
        memcpy(stringObjPtr->bytes + pos, Jim_String(argv[2]), width);
        /* No padding is needed since the string is already extended */
    }

    if (Jim_SetVariable(interp, argv[1], stringObjPtr) != JIM_OK) {
        if (freeobj) {
            Jim_FreeNewObj(interp, stringObjPtr);
            return JIM_ERR;
        }
    }
    return JIM_OK;
}

int Jim_packInit(Jim_Interp *interp)
{
    if (Jim_PackageProvide(interp, "pack", "1.0", JIM_ERRMSG)) {
        return JIM_ERR;
    }

    Jim_CreateCommand(interp, "unpack", Jim_UnpackCmd, NULL, NULL);
    Jim_CreateCommand(interp, "pack", Jim_PackCmd, NULL, NULL);
    return JIM_OK;
}
