#!/usr/bin/env python3

# Copyright (c) 2016, Suprock Technologies
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
# OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import os.path


def output_shift(f, input_index, left_shift_count, mask=0xFF, t="uint32_t"):
    if mask == 0xFF:
        input_str = "input[{}]".format(input_index)
    else:
        input_str = "(input[{}] & 0x{:02x})".format(input_index, mask)

    if left_shift_count == 0:
        f.write("(({}){})".format(t, input_str))
    elif left_shift_count > 0:
        f.write("(({}){} << {})".format(t, input_str, left_shift_count))
    else:
        f.write("(({}){} >> {})".format(t, input_str, -left_shift_count))


def generate_single_unpack(f, packsize, offset, output_index, output_type):
    """
    Generate instructions for unpacking a single value.
    """
    if packsize == 0:
        f.write("\toutput[{}] = ({})0;\n".format(output_index, output_type))
        return

    f.write("\t// read output[{}]: from bit {} to {}\n".format(
            output_index, offset, offset + packsize - 1))
    f.write("\tvalue = ")

    remaining = packsize
    collected = 0
    while remaining + (offset % 8) >= 8:
        if offset % 8 == 0:
            # use all bits of leading byte
            collected += 8
            output_shift(f, offset // 8, 32 - collected)
            offset += 8
            remaining -= 8
        else:
            # use partial bits of leading byte
            to_collect = 8 - (offset % 8)
            mask = 0xFF >> (8 - to_collect)
            collected += to_collect
            output_shift(f, offset // 8, 32 - collected, mask)
            offset += to_collect
            remaining -= to_collect

        if remaining > 0:
            f.write(" |\n\t\t\t")

    if remaining > 0:
        # collect trailing bits.
        mask = ((0xFF << (8 - remaining)) & 0xFF) >> (offset % 8)
        collected += 8 - (offset % 8)  # we're collecting more than remaining
        output_shift(f, offset // 8, 32 - collected, mask)

    f.write(";\n")
    right_shift_count = 32 - packsize
    if right_shift_count != 0:
        f.write("\toutput[{}] = ({})(value >> {});\n".format(
                output_index, output_type, right_shift_count))
    else:
        f.write("\toutput[{}] = ({})value;\n".format(
                output_index, output_type))


def generate_unpack_function(f, func_name, count, signed, packsize, offset,
                             output_type):
    """
    Generate instructions for unpacking a whole array of values.
    """

    signed_str = "signed" if signed else "unsigned"
    temp_type = "int32_t" if signed else "uint32_t"

    s = "static void {}(const uint8_t * input, {} * output) {{\n"
    f.write(s.format(func_name, output_type))
    f.write("\t// Unpack {} values into output\n".format(count))
    f.write("\t// Values are {}-bit {}\n".format(packsize, signed_str))
    f.write("\t// Skip {} bits, read {} bits of data.\n".format(
            offset, packsize * count))
    f.write("\t// Input must be at least {} bytes long\n".format(
            (packsize * count + offset + 7) // 8))
    f.write("\n")
    if packsize == 0:
        f.write("\t(void)input; // suppress unused parameter warning\n")
    else:
        f.write("\t{} value;\n".format(temp_type))

    for i in range(count):
        f.write("\n")
        generate_single_unpack(f, packsize, offset, i, output_type)
        offset += packsize

    f.write("}\n")


def generate_c_file(f, header_name, packsize, maxcount):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#include <stdlib.h>\n")
    f.write("#include <stdint.h>\n")
    f.write("\n")
    f.write('#include "{}"\n'.format(header_name))
    f.write("\n")
    f.write("\n")

    output_type = "double"

    for count in range(1, maxcount + 1):
        if packsize == 0:
            func_name = "unpack_{}_{}bit".format(count, packsize)
            generate_unpack_function(f, func_name, count, False, packsize, 0,
                                     output_type)
        else:
            for offset in range(8):
                for signed in [True, False]:
                    if packsize == 1 and signed:
                        continue
                    f.write("\n")
                    signed_str = "signed" if signed else "unsigned"
                    func_name = "unpack_{}_{}bit_{}_{}off".format(count, packsize,
                                                                  signed_str,
                                                                  offset)
                    generate_unpack_function(f, func_name, count, signed, packsize,
                                             offset, output_type)

    f.write("\n")
    f.write("\n")
    s = "unpack_func_t unpack_{}bit[UNPACK_{}BIT_MAX_COUNT][8][2] = {{\n"
    f.write(s.format(packsize, packsize))

    for count in range(1, maxcount + 1):
        f.write("\t{\n")
        for offset in range(8):
            f.write("\t\t{\n")
            for signed in [False, True]:
                signed_str = "signed" if signed else "unsigned"
                if packsize == 0:
                    func_name = "unpack_{}_{}bit".format(count, packsize)
                elif packsize == 1 and signed:
                    func_name = "NULL"
                else:
                    func_name = "unpack_{}_{}bit_{}_{}off".format(count, packsize,
                                                                  signed_str,
                                                                  offset)
                f.write("\t\t\t{},\n".format(func_name))
            f.write("\t\t},\n")
        f.write("\t},\n")
    f.write("};\n")


def generate_h_file(f, header_name, packsize, maxcount):
    header_define = header_name.upper().replace(".", "_") + "_"

    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#ifndef {}\n".format(header_define))
    f.write("#define {}\n".format(header_define))
    f.write("\n")
    f.write("\n")
    f.write('#include "unpack.h"\n')
    f.write("\n")
    f.write("\n")
    f.write("#define UNPACK_{}BIT_MAX_COUNT {}\n".format(packsize, maxcount))
    f.write("\n")
    s = "extern unpack_func_t unpack_{}bit[UNPACK_{}BIT_MAX_COUNT][8][2];"
    f.write(s.format(packsize, packsize))
    f.write("\n")
    f.write("\n")
    f.write("#endif /* {} */\n".format(header_define))


def generate_main_h(f):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#ifndef UNPACK_H_\n")
    f.write("#define UNPACK_H_\n")
    f.write("\n")
    f.write("\n")
    f.write('#include <stdint.h>\n')
    f.write("\n")
    f.write("\n")
    f.write("#ifdef __cplusplus\n")
    f.write('extern "C" {\n')
    f.write("#endif\n")
    f.write("\n")
    f.write("\n")
    f.write("// cross platform definitions for shared/static library support\n")
    f.write("// see https://gcc.gnu.org/wiki/Visibility\n")
    f.write("#if defined _WIN32 || defined __CYGWIN__\n")
    f.write("#  define UNPACK_HELPER_DLL_IMPORT __declspec(dllimport)\n")
    f.write("#  define UNPACK_HELPER_DLL_EXPORT __declspec(dllexport)\n")
    f.write("#  define UNPACK_HELPER_DLL_LOCAL\n")
    f.write("#else\n")
    f.write("#  if __GNUC__ >= 4\n")
    f.write('#    define UNPACK_HELPER_DLL_IMPORT __attribute__ ((visibility ("default")))\n')
    f.write('#    define UNPACK_HELPER_DLL_EXPORT __attribute__ ((visibility ("default")))\n')
    f.write('#    define UNPACK_HELPER_DLL_LOCAL  __attribute__ ((visibility ("hidden")))\n')
    f.write("#  else\n")
    f.write("#    define UNPACK_HELPER_DLL_IMPORT\n")
    f.write("#    define UNPACK_HELPER_DLL_EXPORT\n")
    f.write("#    define UNPACK_HELPER_DLL_LOCAL\n")
    f.write("#  endif\n")
    f.write("#endif\n")
    f.write("\n")
    f.write("#ifdef UNPACK_STATIC_LIB // defined if unpack library is compiled as static lib\n")
    f.write("#  define UNPACK_API\n")
    f.write("#  define UNPACK_LOCAL\n")
    f.write("#else // UNPACK_STATIC_LIB is not defined: this means unpack is a DLL\n")
    f.write("#  ifdef UNPACK_API_EXPORTS // defined if we are building the library (instead of using it)\n")
    f.write("#    define UNPACK_API UNPACK_HELPER_DLL_EXPORT\n")
    f.write("#  else\n")
    f.write("#    define UNPACK_API UNPACK_HELPER_DLL_IMPORT\n")
    f.write("#  endif // UNPACK_API_EXPORTS\n")
    f.write("#  define UNPACK_LOCAL UNPACK_HELPER_DLL_LOCAL\n")
    f.write("#endif // UNPACK_STATIC_LIB\n")
    f.write("\n")
    f.write("\n")
    f.write("typedef void (*unpack_func_t)(const uint8_t *, double *);\n")
    f.write("typedef uint64_t (*unwrap_func_t)(const uint8_t *, uint64_t);\n")
    f.write("typedef uint8_t (*unpack_id_func_t)(const uint8_t *);\n")
    f.write("\n")
    f.write("UNPACK_API unpack_func_t find_unpack(int count, int bits, int is_signed, int offset);\n")
    f.write("UNPACK_API unwrap_func_t find_unwrap(int bits, int offset);\n")
    f.write("UNPACK_API unpack_id_func_t find_unpack_id(int bits, int offset);\n")
    f.write("\n")
    f.write("\n")
    f.write("#ifdef __cplusplus\n")
    f.write("}\n")
    f.write("#endif\n")
    f.write("\n")
    f.write("\n")
    f.write("#endif /* UNPACK_H_ */\n")


def generate_main_c(f, sizes):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#include <stdlib.h>\n")
    f.write('#include <stdint.h>\n')
    f.write("\n")
    f.write('#include "unpack.h"\n')
    f.write('#include "unpack_all_sizes.h"\n')
    f.write('#include "unwrap.h"\n')
    f.write('#include "unpack_id.h"\n')
    f.write("\n")
    f.write("\n")
    f.write("UNPACK_API unpack_func_t find_unpack(int count, int bits, int is_signed, int offset) {\n")
    f.write("\tif (0 <= offset && offset <= 7) {\n")
    f.write("\t\tswitch (bits) {\n")

    for packsize in sizes:
        f.write("\t\t\tcase {}:\n".format(packsize))
        f.write("\t\t\t\tif (1 <= count && count <= UNPACK_{}BIT_MAX_COUNT) {{\n".format(packsize))
        f.write("\t\t\t\t\treturn unpack_{}bit[count - 1][offset][is_signed != 0];\n".format(packsize))
        f.write("\t\t\t\t}\n")
        f.write("\t\t\t\tbreak;\n")

    f.write("\t\t}\n")
    f.write("\t}\n")
    f.write("\n")
    f.write("\treturn NULL;\n")
    f.write("}\n")
    f.write("\n")
    f.write("UNPACK_API unwrap_func_t find_unwrap(int bits, int offset) {\n")
    f.write("\tif (0 <= offset && offset <= 7) {\n")
    f.write("\t\tif (UNWRAP_MIN_COUNT <= bits && bits <= UNWRAP_MAX_COUNT) {\n")
    f.write("\t\t\treturn unwrap[bits - UNWRAP_MIN_COUNT][offset];\n")
    f.write("\t\t}\n")
    f.write("\t}\n")
    f.write("\n")
    f.write("\treturn NULL;\n")
    f.write("}\n")
    f.write("\n")
    f.write("UNPACK_API unpack_id_func_t find_unpack_id(int bits, int offset) {\n")
    f.write("\tif (0 <= offset && offset <= 7) {\n")
    f.write("\t\tif (UNPACK_ID_MIN_COUNT <= bits && bits <= UNPACK_ID_MAX_COUNT) {\n")
    f.write("\t\t\treturn unpack_id[bits - UNPACK_ID_MIN_COUNT][offset];\n")
    f.write("\t\t}\n")
    f.write("\t}\n")
    f.write("\n")
    f.write("\treturn NULL;\n")
    f.write("}\n")


def generate_all_sizes_h(f, sizes):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#ifndef UNPACK_ALL_SIZES_H_\n")
    f.write("#define UNPACK_ALL_SIZES_H_\n")
    f.write("\n")
    f.write("\n")
    for packsize in sizes:
        f.write('#include "unpack{}.h"\n'.format(packsize))
    f.write("\n")
    f.write("\n")
    f.write("#endif /* UNPACK_ALL_SIZES_H_ */\n")


def generate_unwrap_counter_function(f, func_name, counter_size, offset):
    """
    Generate instructions for a single counter size.
    """

    f.write("static uint64_t {}(const uint8_t * input, uint64_t last) {{\n".format(
            func_name))
    f.write("\t// Unpack a single value\n".format(counter_size))
    f.write("\t// Skip {} bit, read {} bits of data\n".format(offset,
                                                              counter_size))
    f.write("\n")
    f.write("\tuint64_t new_counter = ")

    remaining = counter_size
    collected = 0
    while remaining >= 8:
        if offset % 8 == 0:
            # use all bits of leading byte
            collected += 8
            output_shift(f, offset // 8, counter_size - collected)
            offset += 8
            remaining -= 8
        else:
            # use partial bits of leading byte
            to_collect = 8 - (offset % 8)
            mask = 0xFF >> (8 - to_collect)
            collected += to_collect
            output_shift(f, offset // 8, counter_size - collected, mask)
            offset += to_collect
            remaining -= to_collect

        if remaining > 0:
            f.write(" |\n\t\t\t")

    if remaining > 0:
        # collect trailing bits. Offset is a multiple of 8 at this point
        # no need for a mask, the unneeded bit will be right shifted out
        collected += 8  # we're collecting more than remaining
        output_shift(f, offset // 8, counter_size - collected)

    f.write(";\n")
    f.write("\n")
    f.write("\tuint64_t mask = ((uint64_t)1 << {}) - 1;\n".format(
            counter_size))
    f.write("\n")
    f.write("\t// do the unwrapping\n")
    f.write("\tif (new_counter <= (last & mask)) {\n")
    f.write('\t\t// set the "carry" bit\n')
    f.write("\t\tnew_counter |= (uint64_t)1 << {};\n".format(counter_size))
    f.write("\t}\n")
    f.write("\n")
    f.write("\t// add higher bits\n")
    f.write("\tnew_counter += last & (~mask);\n")
    f.write("\n")
    f.write("\treturn new_counter;\n")
    f.write("}\n")


def generate_unwrap_counter_c(f, counter_sizes, header_name):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#include <stdint.h>")
    f.write("\n")
    f.write('#include "{}"'.format(header_name))
    f.write("\n")

    for counter_size in counter_sizes:
        for offset in range(8):
            f.write("\n")
            func_name = "unwrap_{}bit_counter_{}off".format(counter_size,
                                                            offset)
            generate_unwrap_counter_function(f, func_name, counter_size,
                                             offset)

    f.write("\n")
    f.write("\n")
    f.write("unwrap_func_t unwrap[UNWRAP_MAX_COUNT - UNWRAP_MIN_COUNT + 1][8] = {\n")

    for counter_size in counter_sizes:
        f.write("\t{\n")
        for offset in range(8):
            func_name = "unwrap_{}bit_counter_{}off".format(counter_size,
                                                            offset)
            f.write("\t\t{},\n".format(func_name))
        f.write("\t},\n")
    f.write("};\n")


def generate_unwrap_counter_h(f, counter_sizes, header_name):
    header_define = header_name.upper().replace(".", "_") + "_"

    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#ifndef {}\n".format(header_define))
    f.write("#define {}\n".format(header_define))
    f.write("\n")
    f.write("\n")
    f.write('#include "unpack.h"\n')
    f.write("\n")
    f.write("\n")
    f.write("#define UNWRAP_MIN_COUNT {}\n".format(counter_sizes[0]))
    f.write("#define UNWRAP_MAX_COUNT {}\n".format(counter_sizes[-1]))
    f.write("\n")
    f.write("extern unwrap_func_t unwrap[UNWRAP_MAX_COUNT - UNWRAP_MIN_COUNT + 1][8];\n")
    f.write("\n")
    f.write("\n")
    f.write("#endif /* {} */\n".format(header_define))


def generate_unpack_id_function(f, func_name, id_size, offset):
    """
    Generate instructions for a single id size.
    """

    f.write("static uint8_t {}(const uint8_t * input) {{\n".format(
            func_name))
    f.write("\t// Unpack a single value\n")
    f.write("\t// Skip {} bit, read {} bits of data\n".format(offset,
                                                              id_size))
    f.write("\n")
    f.write("\tuint8_t id = ")

    if id_size == 0:
        f.write("0")
    else:
        # create a 16 bit mask representing the bits to use
        mask_shift = (16 - id_size - offset)
        mask = ((1 << id_size) - 1) << mask_shift
        output_shift(f, 0, 8 - mask_shift, mask >> 8, "uint8_t")
        if mask & 0xFF != 0:
            f.write(" |\n\t\t\t")
            output_shift(f, 1, -mask_shift, mask & 0xFF, "uint8_t")

    f.write(";\n")
    f.write("\n")

    if id_size == 0:
        f.write("\t(void)input; // suppress unused parameter warning\n")
        f.write("\n")

    f.write("\treturn id;\n")
    f.write("}\n")


def generate_unpack_id_c(f, id_sizes, header_name):
    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#include <stdint.h>")
    f.write("\n")
    f.write('#include "{}"'.format(header_name))
    f.write("\n")

    for id_size in id_sizes:
        if id_size == 0:
            f.write("\n")
            func_name = "unpack_{}bit_id".format(id_size)
            generate_unpack_id_function(f, func_name, id_size, 0)
            continue
        for offset in range(8):
            f.write("\n")
            func_name = "unpack_{}bit_id_{}off".format(id_size, offset)
            generate_unpack_id_function(f, func_name, id_size, offset)

    f.write("\n")
    f.write("\n")
    f.write("unpack_id_func_t unpack_id[UNPACK_ID_MAX_COUNT - UNPACK_ID_MIN_COUNT + 1][8] = {\n")

    for id_size in id_sizes:
        f.write("\t{\n")
        for offset in range(8):
            if id_size == 0:
                func_name = "unpack_{}bit_id".format(id_size)
            else:
                func_name = "unpack_{}bit_id_{}off".format(id_size, offset)
            f.write("\t\t{},\n".format(func_name))
        f.write("\t},\n")
    f.write("};\n")


def generate_unpack_id_h(f, counter_sizes, header_name):
    header_define = header_name.upper().replace(".", "_") + "_"

    f.write("// AUTOGENERATED FILE. DO NOT MODIFY.\n")
    f.write("\n")
    f.write("#ifndef {}\n".format(header_define))
    f.write("#define {}\n".format(header_define))
    f.write("\n")
    f.write("\n")
    f.write('#include "unpack.h"\n')
    f.write("\n")
    f.write("\n")
    f.write("#define UNPACK_ID_MIN_COUNT {}\n".format(counter_sizes[0]))
    f.write("#define UNPACK_ID_MAX_COUNT {}\n".format(counter_sizes[-1]))
    f.write("\n")
    f.write("extern unpack_id_func_t unpack_id[UNPACK_ID_MAX_COUNT - UNPACK_ID_MIN_COUNT + 1][8];\n")
    f.write("\n")
    f.write("\n")
    f.write("#endif /* {} */\n".format(header_define))


def generate_files(path):
    sizes = list(range(0, 32 + 1))

    with open(os.path.join(path, "inc/unpack.h"), "w") as main_h_file:
        generate_main_h(main_h_file)
    with open(os.path.join(path, "src/unpack.c"), "w") as main_c_file:
        generate_main_c(main_c_file, sizes)

    for packsize in sizes:
        if packsize in [16, 24, 32]:
            maxcount = 128 * 8 // packsize
        elif packsize >= 8:
            maxcount = 64 * 8 // packsize
        else:
            maxcount = 8

        h_name = "unpack{}.h".format(packsize)
        c_name = "unpack{}.c".format(packsize)

        with open(os.path.join(path, "src", h_name), "w") as h_file:
            generate_h_file(h_file, h_name, packsize, maxcount)
        with open(os.path.join(path, "src", c_name), "w") as c_file:
            generate_c_file(c_file, h_name, packsize, maxcount)

    with open(os.path.join(path, "src/unpack_all_sizes.h"), "w") as all_h_file:
        generate_all_sizes_h(all_h_file, sizes)

    counter_sizes = list(range(8, 32 + 1))
    unwrap_header_name = "unwrap.h"
    with open(os.path.join(path, "src", unwrap_header_name), "w") as unwrap_h_file:
        generate_unwrap_counter_h(unwrap_h_file, counter_sizes,
                                  unwrap_header_name)
    with open(os.path.join(path, "src", "unwrap.c"), "w") as unwrap_c_file:
        generate_unwrap_counter_c(unwrap_c_file, counter_sizes,
                                  unwrap_header_name)

    id_sizes = list(range(0, 8 + 1))
    unpack_id_header_name = "unpack_id.h"
    with open(os.path.join(path, "src", unpack_id_header_name), "w") as unpack_id_h_file:
        generate_unpack_id_h(unpack_id_h_file, id_sizes, unpack_id_header_name)
    with open(os.path.join(path, "src", "unpack_id.c"), "w") as unpack_id_c_file:
        generate_unpack_id_c(unpack_id_c_file, id_sizes, unpack_id_header_name)

if __name__ == "__main__":
    generate_files(".")
