#!/usr/bin/env python3

import os
import sys

name_map = {
    'uint8_t': 'uint8',

    'int16_t': 'int16',
    'uint16_t': 'uint16',

    'uint32_t': 'uint32',

    'gid_t': 'gid',
    'uid_t': 'uid',

    'size_t': 'size',
    'ssize_t': 'ssize',

    'int': 'int',
    'unsigned int': 'uint',

    'long': 'long',
    'unsigned long': 'ulong',

    'long long': 'longlong',
    'unsigned long long': 'ulonglong',
}

def is_unsigned(t):
    if t[0] == 'u':
        return True
    # We don't actually know the signedness of uid and gid. But worst case by
    # C standard 6.3.1.3 at worst we get an implementation defined signal.
    if t in ('size_t', 'gid_t', 'uid_t'):
        return True
    return False

def is_signed(t):
    if t.startswith('int'):
        return True
    if t.startswith('long'):
        return True
    if t in ('ssize_t',):
        return True
    return False

def type_max(t):
    return {
        'ssize_t': 'SSIZE_MAX',
        'size_t': 'SIZE_MAX',
        'int8_t': 'INT8_MAX',
        'uint8_t': 'UINT8_MAX',
        'uint16_t': 'UINT16_MAX',
        'int16_t': 'INT16_MAX',
        'uint32_t': 'UINT32_MAX',
        'int': 'INT_MAX',
        'unsigned int': 'UINT_MAX',
        'uid_t': 'UID_MAX',
        'gid_t': 'GID_MAX',
        'uid_t': 'UID_MAX',
        'long long': 'LLONG_MAX',
        'unsigned long': 'ULONG_MAX',
        'unsigned long long': 'ULONGLONG_MAX',
    }[t]

def main():
    d = os.path.dirname(sys.argv[0])
    with open(os.path.join(d, 'cast.c'), 'w') as fc:
        with open(os.path.join(d, 'cast.h'), 'w') as fh:
            codegen(fc, fh)

def codegen(fc, fh):
    # TODO: #define uintmax_t if it doesn't exist
    genheader = """/*
 * GENERATED FILE, DO NOT EDIT. Generated by mkcast.py
 */
"""
    fh.write(genheader)
    fc.write(genheader)
    fh.write("""#include "config.h"
#include <stdint.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
unsigned cast_float_unsigned(float x);
uint32_t cast_double_uint32(double x);
void cast_assert(int must, const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
""")
    fc.write("""
#include "cast.h"
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
unsigned
cast_float_unsigned(float x)
{
    cast_assert(!isnan(x), "Tried casting %f to unsigned. It's nan", x);
    cast_assert(x >= 0.0f, "Tried casting %f to unsigned. It's not positive", x);
    // Due to rounding errors, avoid comparing close to the edge.
    cast_assert(x <= (float)(UINT_MAX - 1000), "Tried casting %f to unsigned. It's too big", x);
    return (unsigned)x;
}
uint32_t
cast_double_uint32(double x)
{
    cast_assert(!isnan(x), "Tried casting %f to uint32_t. It's nan", x);
    cast_assert(x >= 0.0, "Tried casting %f to uint32_t. It's not positive", x);
    // Due to rounding errors, avoid comparing close to the edge.
    cast_assert(x <= (double)(UINT32_MAX - 1000), "Tried casting %f to uint32_t. It's too big", x);
    return (uint32_t)x;
}
void
cast_assert(int must, const char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    if (!must) {
        fprintf(stderr, "arping: runtime check: ");
        vfprintf(stderr, fmt, ap);
        fprintf(stderr, "\\n");
        exit(1);
    }
    va_end(ap);
}
""")

    for src, dst in [
            # from, to
            ('int16_t', 'uint16_t'),
            #        ('uint16_t', 'int16_t'),
            ('ssize_t', 'size_t'),
            ('size_t', 'ssize_t'),
            ('size_t', 'uint16_t'),
            ('size_t', 'uint32_t'),
            ('unsigned long', 'unsigned int'),
            ('long', 'int16_t'),
            ('int16_t', 'uint8_t'),
            ('int', 'unsigned int'),
            ('int', 'uint16_t'),
            ('int', 'size_t'),
            ('long', 'int'),
            ('long', 'uint8_t'),
            ('unsigned long', 'gid_t'),
            ('gid_t', 'long long'),
            ('uid_t', 'long long'),
            ('long long', 'unsigned long long'),
            ('long long', 'unsigned int'),
            ('int', 'int16_t'),
            ('long', 'unsigned long'),
            ('long', 'unsigned int'),
            ('int', 'unsigned long'),
    ]:
        keys = {
            'src': src,
            'dst': dst,
            'src_name':name_map[src],
            'dst_name':name_map[dst],
        }
        assert is_unsigned(src) ^ is_signed(src)
        assert is_unsigned(dst) ^ is_signed(dst)
        from_signed = is_signed(src)
        s2u = is_signed(src) and is_unsigned(dst)
        u2s = is_unsigned(src) and is_signed(dst)
        keys['dst_max'] = type_max(dst)

        if from_signed:
            keys['errstr'] = '"cast_{src_name}_{dst_name}(%"PRIdMAX"): %s", (intmax_t)from'.format(**keys)
        else:
            keys['errstr'] = '"cast_{src_name}_{dst_name}(%"PRIuMAX"): %s", (uintmax_t)from'.format(**keys)

        fh.write("""{dst} cast_{src_name}_{dst_name}({src} from, const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
""".format(**keys))
        fc.write("""
{dst}
cast_{src_name}_{dst_name}({src} from, const char* fmt, ...)
{{
    va_list ap;
    va_start(ap, fmt);
""".format(**keys))

        if s2u:
            fc.write('    cast_assert(from >= 0, {errstr}, "need >= 0");\n'.format(**keys));
        if u2s:
            fc.write('''
#pragma GCC diagnostic push
    // If the `from` type cannot represent `dst_max`, then GCC warns as
    // it always being true. So suppressing that warning.
    //
    // Silence both GCC and clang for the same error.
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wtype-limits"
#pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare"
    cast_assert(from <= {dst_max}, {errstr}, "value won't fit in {dst}");
#pragma GCC diagnostic pop
'''.format(**keys));

        fc.write("    const {dst} to = ({dst})from;\n".format(**keys))

        if u2s:
            fc.write('    cast_assert(to >= 0, {errstr}, "wrapped after casting");\n'.format(**keys));

        fc.write("""    if (from != ({src})to) {{
        fprintf(stderr, "arping: ");
        if (fmt) {{
          vfprintf(stderr, fmt, ap);
          fprintf(stderr, ": value won't fit in {dst}");
        }} else {{
          fprintf(stderr, {errstr}, "value won't fit in {dst}\\n");
        }}
        fprintf(stderr, "\\n");
        exit(1);
    }}
    va_end(ap);
    return to;
}}""".format(**keys))

if __name__ == '__main__':
    main()
