#include <assert.h>
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#ifdef WIN32
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
#include <lcm/windows/WinPorting.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <inttypes.h>
#include <unistd.h>
#endif

#include <glib.h>

#include "lcmgen.h"

#define INDENT(n) (4 * (n))

#define emit_start(n, ...)                \
    do {                                  \
        fprintf(f, "%*s", INDENT(n), ""); \
        fprintf(f, __VA_ARGS__);          \
    } while (0)
#define emit_continue(...)       \
    do {                         \
        fprintf(f, __VA_ARGS__); \
    } while (0)
#define emit_end(...)            \
    do {                         \
        fprintf(f, __VA_ARGS__); \
        fprintf(f, "\n");        \
    } while (0)
#define emit(n, ...)                      \
    do {                                  \
        fprintf(f, "%*s", INDENT(n), ""); \
        fprintf(f, __VA_ARGS__);          \
        fprintf(f, "\n");                 \
    } while (0)

#define err(...) fprintf(stderr, __VA_ARGS__)

static void mkdir_with_parents(const char *path, mode_t mode)
{
#ifdef WIN32
    g_mkdir_with_parents(path, 0755);
#else
    int len = strlen(path);
    for (int i = 0; i < len; i++) {
        if (path[i] == '/') {
            char *dirpath = (char *) malloc(i + 1);
            strncpy(dirpath, path, i);
            dirpath[i] = 0;

            mkdir(dirpath, mode);
            free(dirpath);

            i++;  // skip the '/'
        }
    }
#endif
}

static char *build_filenamev(char **parts)
{
    char **p = parts;
    int total_len = 0;
    for (p = parts; *p; p++) {
        total_len += strlen(*p) + strlen(G_DIR_SEPARATOR_S);
    }
    total_len++;
    char *result = (char *) malloc(total_len);
    memset(result, 0, total_len);
    for (p = parts; *p; p++) {
        if (!strlen(*p))
            continue;
        strncat(result, *p, total_len);
        if (*(p + 1))
            strncat(result, G_DIR_SEPARATOR_S, total_len);
    }
    return result;
}

static void get_all_vals_helper(gpointer key, gpointer value, gpointer user_data)
{
    GPtrArray *vals = (GPtrArray *) user_data;

    g_ptr_array_add(vals, value);
}

static GPtrArray *_hash_table_get_vals(GHashTable *hash_table)
{
    GPtrArray *vals = g_ptr_array_sized_new(g_hash_table_size(hash_table));
    g_hash_table_foreach(hash_table, get_all_vals_helper, vals);
    return vals;
}

void setup_python_options(getopt_t *gopt)
{
    getopt_add_string(gopt, 0, "ppath", "", "Python destination directory");
    getopt_add_bool(gopt, 0, "python-no-init", 0, "Do not create __init__.py");
}

static int is_same_type(const lcm_typename_t *tn1, const lcm_typename_t *tn2)
{
    return !strcmp(tn1->lctypename, tn2->lctypename);
}

// static int
// is_same_package (const lcm_typename_t *tn1, const lcm_typename_t *tn2) {
//    return ! strcmp (tn1->package, tn2->package);
//}

static char _struct_format(lcm_member_t *lm)
{
    const char *tn = lm->type->lctypename;
    if (!strcmp("byte", tn))
        return 'B';
    if (!strcmp("boolean", tn))
        return 'b';
    if (!strcmp("int8_t", tn))
        return 'b';
    if (!strcmp("int16_t", tn))
        return 'h';
    if (!strcmp("int32_t", tn))
        return 'i';
    if (!strcmp("int64_t", tn))
        return 'q';
    if (!strcmp("float", tn))
        return 'f';
    if (!strcmp("double", tn))
        return 'd';
    return 0;
}

static int _primitive_type_size(const char *tn)
{
    if (!strcmp("byte", tn))
        return 1;
    if (!strcmp("boolean", tn))
        return 1;
    if (!strcmp("int8_t", tn))
        return 1;
    if (!strcmp("int16_t", tn))
        return 2;
    if (!strcmp("int32_t", tn))
        return 4;
    if (!strcmp("int64_t", tn))
        return 8;
    if (!strcmp("float", tn))
        return 4;
    if (!strcmp("double", tn))
        return 8;
    assert(0);
    return 0;
}

static void _emit_decode_one(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls, lcm_member_t *lm,
                             const char *accessor, int indent, const char *sfx)
{
    const char *tn = lm->type->lctypename;
    const char *mn = lm->membername;
    const char *sn = lm->type->shortname;
    if (!strcmp("string", tn)) {
        emit(indent, "__%s_len = struct.unpack('>I', buf.read(4))[0]", mn);
        emit(indent, "%sbuf.read(__%s_len)[:-1].decode('utf-8', 'replace')%s", accessor, mn, sfx);
    } else if (!strcmp("byte", tn)) {
        emit(indent, "%sstruct.unpack('B', buf.read(1))[0]%s", accessor, sfx);
    } else if (!(strcmp("boolean", tn))) {
        emit(indent, "%sbool(struct.unpack('b', buf.read(1))[0])%s", accessor, sfx);
    } else if (!strcmp("int8_t", tn)) {
        emit(indent, "%sstruct.unpack('b', buf.read(1))[0]%s", accessor, sfx);
    } else if (!strcmp("int16_t", tn)) {
        emit(indent, "%sstruct.unpack('>h', buf.read(2))[0]%s", accessor, sfx);
    } else if (!strcmp("int32_t", tn)) {
        emit(indent, "%sstruct.unpack('>i', buf.read(4))[0]%s", accessor, sfx);
    } else if (!strcmp("int64_t", tn)) {
        emit(indent, "%sstruct.unpack('>q', buf.read(8))[0]%s", accessor, sfx);
    } else if (!strcmp("float", tn)) {
        emit(indent, "%sstruct.unpack('>f', buf.read(4))[0]%s", accessor, sfx);
    } else if (!strcmp("double", tn)) {
        emit(indent, "%sstruct.unpack('>d', buf.read(8))[0]%s", accessor, sfx);
    } else {
        if (is_same_type(lm->type, ls->structname)) {
            emit(indent, "%s%s._decode_one(buf)%s", accessor, sn, sfx);
        } else {
            emit(indent, "%s%s._decode_one(buf)%s", accessor, tn, sfx);
        }
    }
}

static void _emit_decode_list(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls, lcm_member_t *lm,
                              const char *accessor, int indent, int is_first, const char *len,
                              int fixed_len)
{
    const char *tn = lm->type->lctypename;
    const char *suffix = "";
    if (!is_first) {
        suffix = ")";
    }
    if (!strcmp("byte", tn)) {
        emit(indent, "%sbuf.read(%s%s)%s", accessor, fixed_len ? "" : "self.", len, suffix);
    } else if (!strcmp("boolean", tn)) {
        if (fixed_len) {
            emit(indent, "%smap(bool, struct.unpack('>%s%c', buf.read(%d)))%s", accessor, len,
                 _struct_format(lm), atoi(len) * _primitive_type_size(tn), suffix);
        } else {
            emit(indent, "%smap(bool, struct.unpack('>%%d%c' %% self.%s, buf.read(self.%s)))%s",
                 accessor, _struct_format(lm), len, len, suffix);
        }
    } else if (!strcmp("int8_t", tn) || !strcmp("int16_t", tn) || !strcmp("int32_t", tn) ||
               !strcmp("int64_t", tn) || !strcmp("float", tn) || !strcmp("double", tn)) {
        if (fixed_len) {
            emit(indent, "%sstruct.unpack('>%s%c', buf.read(%d))%s", accessor, len,
                 _struct_format(lm), atoi(len) * _primitive_type_size(tn), suffix);
        } else {
            if (_primitive_type_size(tn) > 1) {
                emit(indent, "%sstruct.unpack('>%%d%c' %% self.%s, buf.read(self.%s * %d))%s",
                     accessor, _struct_format(lm), len, len, _primitive_type_size(tn), suffix);
            } else {
                emit(indent, "%sstruct.unpack('>%%d%c' %% self.%s, buf.read(self.%s))%s", accessor,
                     _struct_format(lm), len, len, suffix);
            }
        }
    } else {
        assert(0);
    }
}

static void _flush_read_struct_fmt(const lcmgen_t *lcm, FILE *f, GQueue *formats, GQueue *members)
{
    int nfmts = g_queue_get_length(formats);
    assert(nfmts == g_queue_get_length(members));
    if (nfmts == 0)
        return;

    fprintf(f, "        ");
    int fmtsize = 0;
    while (!g_queue_is_empty(members)) {
        lcm_member_t *lm = (lcm_member_t *) g_queue_pop_head(members);
        emit_continue("self.%s", lm->membername);
        if (!g_queue_is_empty(members)) {
            emit_continue(", ");
        }
        fmtsize += _primitive_type_size(lm->type->lctypename);
    }
    emit_continue(" = struct.unpack(\">");
    while (!g_queue_is_empty(formats)) {
        emit_continue("%c", GPOINTER_TO_INT(g_queue_pop_head(formats)));
    }
    emit_end("\", buf.read(%d))%s", fmtsize, nfmts == 1 ? "[0]" : "");
}

static void emit_python_decode_one(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    emit(1, "def _decode_one(buf):");
    emit(2, "self = %s()", ls->structname->shortname);

    GQueue *struct_fmt = g_queue_new();
    GQueue *struct_members = g_queue_new();

    for (unsigned int m = 0; m < g_ptr_array_size(ls->members); m++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, m);
        char fmt = _struct_format(lm);

        if (!lm->dimensions->len) {
            if (fmt && strcmp(lm->type->lctypename, "boolean")) {
                g_queue_push_tail(struct_fmt, GINT_TO_POINTER((int) fmt));
                g_queue_push_tail(struct_members, lm);
            } else {
                _flush_read_struct_fmt(lcm, f, struct_fmt, struct_members);
                char *accessor = g_strdup_printf("self.%s = ", lm->membername);
                _emit_decode_one(lcm, f, ls, lm, accessor, 2, "");
                g_free(accessor);
            }
        } else {
            _flush_read_struct_fmt(lcm, f, struct_fmt, struct_members);

            GString *accessor = g_string_new("");
            g_string_append_printf(accessor, "self.%s", lm->membername);

            // iterate through the dimensions of the member, building up
            // an accessor string, and emitting for loops
            unsigned int n;
            for (n = 0; n < lm->dimensions->len - 1; n++) {
                lcm_dimension_t *dim = (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, n);

                if (n == 0) {
                    emit(2, "%s = []", accessor->str);
                } else {
                    emit(2 + n, "%s.append([])", accessor->str);
                }

                if (dim->mode == LCM_CONST) {
                    emit(2 + n, "for i%d in range(%s):", n, dim->size);
                } else {
                    emit(2 + n, "for i%d in range(self.%s):", n, dim->size);
                }

                if (n > 0 && n < lm->dimensions->len - 1) {
                    g_string_append_printf(accessor, "[i%d]", n - 1);
                }
            }

            // last dimension.
            lcm_dimension_t *last_dim =
                (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, lm->dimensions->len - 1);
            int last_dim_fixed_len = last_dim->mode == LCM_CONST;

            if (lcm_is_primitive_type(lm->type->lctypename) &&
                0 != strcmp(lm->type->lctypename, "string")) {
                // member is a primitive non-string type.  Emit code to
                // decode a full array in one call to struct.unpack
                if (n == 0) {
                    g_string_append_printf(accessor, " = ");
                } else {
                    g_string_append_printf(accessor, ".append(");
                }

                _emit_decode_list(lcm, f, ls, lm, accessor->str, 2 + n, n == 0, last_dim->size,
                                  last_dim_fixed_len);
            } else {
                // member is either a string type or an inner LCM type.  Each
                // array element must be decoded individually
                if (n == 0) {
                    emit(2, "%s = []", accessor->str);
                } else {
                    emit(2 + n, "%s.append ([])", accessor->str);
                    g_string_append_printf(accessor, "[i%d]", n - 1);
                }
                if (last_dim_fixed_len) {
                    emit(2 + n, "for i%d in range(%s):", n, last_dim->size);
                } else {
                    emit(2 + n, "for i%d in range(self.%s):", n, last_dim->size);
                }
                g_string_append_printf(accessor, ".append(");
                _emit_decode_one(lcm, f, ls, lm, accessor->str, n + 3, ")");
            }
            g_string_free(accessor, TRUE);
        }
    }
    _flush_read_struct_fmt(lcm, f, struct_fmt, struct_members);
    emit(2, "return self");

    g_queue_free(struct_fmt);
    g_queue_free(struct_members);
    emit(1, "_decode_one = staticmethod(_decode_one)");
    fprintf(f, "\n");
}

static void emit_python_decode(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    // clang-format off
    emit(1, "def decode(data):");
    emit(2,     "if hasattr(data, 'read'):");
    emit(3,         "buf = data");
    emit(2,     "else:");
    emit(3,         "buf = BytesIO(data)");
    emit(2,     "if buf.read(8) != %s._get_packed_fingerprint():", ls->structname->shortname);
    emit(3,         "raise ValueError(\"Decode error\")");
    emit(2,     "return %s._decode_one(buf)", ls->structname->shortname);
    emit(1, "decode = staticmethod(decode)");
    fprintf (f, "\n");
    // clang-format on
}

static void _emit_encode_one(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls, lcm_member_t *lm,
                             const char *accessor, int indent)
{
    const char *tn = lm->type->lctypename;
    const char *mn = lm->membername;
    if (!strcmp("string", tn)) {
        emit(indent, "__%s_encoded = %s.encode('utf-8')", mn, accessor);
        emit(indent, "buf.write(struct.pack('>I', len(__%s_encoded)+1))", mn);
        emit(indent, "buf.write(__%s_encoded)", mn);
        emit(indent, "buf.write(b\"\\0\")");
    } else if (!strcmp("byte", tn)) {
        emit(indent, "buf.write(struct.pack('B', %s))", accessor);
    } else if (!strcmp("int8_t", tn) || !strcmp("boolean", tn)) {
        emit(indent, "buf.write(struct.pack('b', %s))", accessor);
    } else if (!strcmp("int16_t", tn)) {
        emit(indent, "buf.write(struct.pack('>h', %s))", accessor);
    } else if (!strcmp("int32_t", tn)) {
        emit(indent, "buf.write(struct.pack('>i', %s))", accessor);
    } else if (!strcmp("int64_t", tn)) {
        emit(indent, "buf.write(struct.pack('>q', %s))", accessor);
    } else if (!strcmp("float", tn)) {
        emit(indent, "buf.write(struct.pack('>f', %s))", accessor);
    } else if (!strcmp("double", tn)) {
        emit(indent, "buf.write(struct.pack('>d', %s))", accessor);
    } else {
        const char *sn = lm->type->shortname;
        const char *gpf = "_get_packed_fingerprint()";
        if (is_same_type(lm->type, ls->structname)) {
            emit(indent, "assert %s.%s == %s.%s", accessor, gpf, sn, gpf);
        } else {
            emit(indent, "assert %s.%s == %s.%s", accessor, gpf, lm->type->lctypename, gpf);
        }
        emit(indent, "%s._encode_one(buf)", accessor);
    }
}

static void _emit_encode_list(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls, lcm_member_t *lm,
                              const char *accessor, int indent, const char *len, int fixed_len)
{
    const char *tn = lm->type->lctypename;
    if (!strcmp("byte", tn)) {
        emit(indent, "buf.write(bytearray(%s[:%s%s]))", accessor, (fixed_len ? "" : "self."), len);
        return;
    } else if (!strcmp("boolean", tn) || !strcmp("int8_t", tn) || !strcmp("int16_t", tn) ||
               !strcmp("int32_t", tn) || !strcmp("int64_t", tn) || !strcmp("float", tn) ||
               !strcmp("double", tn)) {
        if (fixed_len) {
            emit(indent, "buf.write(struct.pack('>%s%c', *%s[:%s]))", len, _struct_format(lm),
                 accessor, len);
        } else {
            emit(indent, "buf.write(struct.pack('>%%d%c' %% self.%s, *%s[:self.%s]))",
                 _struct_format(lm), len, accessor, len);
        }
    } else {
        assert(0);
    }
}

static void _flush_write_struct_fmt(FILE *f, GQueue *formats, GQueue *members)
{
    assert(g_queue_get_length(formats) == g_queue_get_length(members));
    if (g_queue_is_empty(formats))
        return;
    emit_start(2, "buf.write(struct.pack(\">");
    while (!g_queue_is_empty(formats)) {
        emit_continue("%c", GPOINTER_TO_INT(g_queue_pop_head(formats)));
    }
    emit_continue("\", ");
    while (!g_queue_is_empty(members)) {
        lcm_member_t *lm = (lcm_member_t *) g_queue_pop_head(members);
        emit_continue("self.%s", lm->membername);
        if (!g_queue_is_empty(members)) {
            emit_continue(", ");
        }
    }
    emit_end("))");
}

static void emit_python_encode_one(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    emit(1, "def _encode_one(self, buf):");
    if (!g_ptr_array_size(ls->members))
        emit(2, "pass");

    GQueue *struct_fmt = g_queue_new();
    GQueue *struct_members = g_queue_new();

    for (unsigned int m = 0; m < g_ptr_array_size(ls->members); m++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, m);
        char fmt = _struct_format(lm);

        if (!lm->dimensions->len) {
            if (fmt) {
                g_queue_push_tail(struct_fmt, GINT_TO_POINTER((int) fmt));
                g_queue_push_tail(struct_members, lm);
            } else {
                _flush_write_struct_fmt(f, struct_fmt, struct_members);
                char *accessor = g_strdup_printf("self.%s", lm->membername);
                _emit_encode_one(lcm, f, ls, lm, accessor, 2);
                g_free(accessor);
            }
        } else {
            _flush_write_struct_fmt(f, struct_fmt, struct_members);
            GString *accessor = g_string_new("");
            g_string_append_printf(accessor, "self.%s", lm->membername);

            unsigned int n;
            for (n = 0; n < lm->dimensions->len - 1; n++) {
                lcm_dimension_t *dim = (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, n);

                g_string_append_printf(accessor, "[i%d]", n);
                if (dim->mode == LCM_CONST) {
                    emit(2 + n, "for i%d in range(%s):", n, dim->size);
                } else {
                    emit(2 + n, "for i%d in range(self.%s):", n, dim->size);
                }
            }

            // last dimension.
            lcm_dimension_t *last_dim =
                (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, lm->dimensions->len - 1);
            int last_dim_fixed_len = last_dim->mode == LCM_CONST;

            if (lcm_is_primitive_type(lm->type->lctypename) &&
                0 != strcmp(lm->type->lctypename, "string")) {
                _emit_encode_list(lcm, f, ls, lm, accessor->str, 2 + n, last_dim->size,
                                  last_dim_fixed_len);
            } else {
                if (last_dim_fixed_len) {
                    emit(2 + n, "for i%d in range(%s):", n, last_dim->size);
                } else {
                    emit(2 + n, "for i%d in range(self.%s):", n, last_dim->size);
                }
                g_string_append_printf(accessor, "[i%d]", n);
                _emit_encode_one(lcm, f, ls, lm, accessor->str, n + 3);
            }

            g_string_free(accessor, TRUE);
        }
    }
    _flush_write_struct_fmt(f, struct_fmt, struct_members);

    g_queue_free(struct_fmt);
    g_queue_free(struct_members);
    fprintf(f, "\n");
}

static void emit_python_encode(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    emit(1, "def encode(self):");
    emit(2, "buf = BytesIO()");
    emit(2, "buf.write(%s._get_packed_fingerprint())", ls->structname->shortname);
    emit(2, "self._encode_one(buf)");
    emit(2, "return buf.getvalue()");
    fprintf(f, "\n");
}

static void emit_member_initializer(const lcmgen_t *lcm, FILE *f, lcm_member_t *lm, int dim_num)
{
    if (dim_num == lm->dimensions->len) {
        const char *tn = lm->type->lctypename;
        const char *initializer = NULL;
        if (!strcmp(tn, "byte"))
            initializer = "0";
        else if (!strcmp(tn, "boolean"))
            initializer = "False";
        else if (!strcmp(tn, "int8_t"))
            initializer = "0";
        else if (!strcmp(tn, "int16_t"))
            initializer = "0";
        else if (!strcmp(tn, "int32_t"))
            initializer = "0";
        else if (!strcmp(tn, "int64_t"))
            initializer = "0";
        else if (!strcmp(tn, "float"))
            initializer = "0.0";
        else if (!strcmp(tn, "double"))
            initializer = "0.0";
        else if (!strcmp(tn, "string"))
            initializer = "\"\"";

        if (initializer != NULL) {
            fprintf(f, "%s", initializer);
        } else {
            fprintf(f, "%s()", tn);
        }
        return;
    }
    if (dim_num == lm->dimensions->len - 1 &&
        // Arrays of bytes get treated as strings, so that they can be more
        // efficiently packed and unpacked.
        !strcmp(lm->type->lctypename, "byte")) {
        fprintf(f, "\"\"");
        return;
    }
    lcm_dimension_t *dim = (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, dim_num);
    if (dim->mode == LCM_VAR) {
        fprintf(f, "[]");
    } else {
        fprintf(f, "[ ");
        emit_member_initializer(lcm, f, lm, dim_num + 1);
        fprintf(f, " for dim%d in range(%s) ]", dim_num, dim->size);
    }
}

static void emit_python_init(const lcmgen_t *lcm, FILE *f, lcm_struct_t *lr)
{
    fprintf(f, "    def __init__(self):\n");
    unsigned int member;
    for (member = 0; member < lr->members->len; member++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(lr->members, member);
        fprintf(f, "        self.%s = ", lm->membername);

        emit_member_initializer(lcm, f, lm, 0);
        fprintf(f, "\n");
    }
    if (0 == member) {
        fprintf(f, "        pass\n");
    }
    fprintf(f, "\n");
}

static void emit_python_fingerprint(const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    const char *sn = ls->structname->shortname;
    emit(1, "def _get_hash_recursive(parents):");
    emit(2, "if %s in parents: return 0", sn);
    for (unsigned int m = 0; m < ls->members->len; m++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, m);
        if (!lcm_is_primitive_type(lm->type->lctypename)) {
            emit(2, "newparents = parents + [%s]", sn);
            break;
        }
    }
    emit_start(2, "tmphash = (0x%" PRIx64, ls->hash);
    for (unsigned int m = 0; m < ls->members->len; m++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, m);
        const char *msn = lm->type->shortname;
        if (!lcm_is_primitive_type(lm->type->lctypename)) {
            const char *ghr = "_get_hash_recursive(newparents)";
            if (is_same_type(lm->type, ls->structname) || strlen(lm->type->package) == 0) {
                emit_continue("+ %s.%s", msn, ghr);
            } else {
                emit_continue("+ %s.%s.%s", lm->type->package, msn, ghr);
            }
        }
    }
    emit_end(") & 0xffffffffffffffff");
    // clang-format off
    emit(2,     "tmphash  = (((tmphash<<1)&0xffffffffffffffff) + "
                             "(tmphash>>63)) & 0xffffffffffffffff");
    emit(2,     "return tmphash");
    emit(1, "_get_hash_recursive = staticmethod(_get_hash_recursive)");
    emit(1, "_packed_fingerprint = None");
    emit(0, "");
    emit(1, "def _get_packed_fingerprint():");
    emit(2,     "if %s._packed_fingerprint is None:", sn);
    emit(3,         "%s._packed_fingerprint = struct.pack("
                             "\">Q\", %s._get_hash_recursive([]))", sn, sn);
    emit(2,     "return %s._packed_fingerprint", sn);
    emit(1, "_get_packed_fingerprint = staticmethod(_get_packed_fingerprint)");
    // clang-format off
    fprintf (f, "\n");

    emit(1, "def get_hash(self):");
    emit(2,     "\"\"\"Get the LCM hash of the struct\"\"\"");
    emit(2,     "return struct.unpack(\">Q\", %s._get_packed_fingerprint())[0]", sn);
    fprintf(f, "\n");
}

static void
emit_python_dependencies (const lcmgen_t *lcm, FILE *f, lcm_struct_t *ls)
{
    GHashTable *dependencies = g_hash_table_new (g_str_hash, g_str_equal);
    for (unsigned int m=0; m<ls->members->len; m++) {
        lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index (ls->members, m);
        if (! lcm_is_primitive_type (lm->type->lctypename) &&
            ! g_hash_table_lookup (dependencies, lm->type->lctypename)) {
            g_hash_table_insert (dependencies, lm->type->lctypename,
                                 lm->type->lctypename);
        }
    }

    GPtrArray *deps = _hash_table_get_vals (dependencies);
    for (int i=0; i<deps->len; i++) {
        const char *package = (char *) g_ptr_array_index (deps, i);
        emit(0, "import %s\n", package);
    }
    g_ptr_array_free (deps, TRUE);
    g_hash_table_destroy (dependencies);
}

typedef struct {
    char *name;
    GPtrArray *enums;
    GPtrArray *structs;
} _package_contents_t;

static _package_contents_t * _package_contents_new (const char *name)
{
    _package_contents_t *pc = (_package_contents_t *) malloc (sizeof(_package_contents_t));
    pc->enums = g_ptr_array_new ();
    pc->structs = g_ptr_array_new ();
    pc->name = strdup (name);
    return pc;
}

static void _package_contents_free (_package_contents_t *pc)
{
    g_ptr_array_free (pc->enums, TRUE);
    g_ptr_array_free (pc->structs, TRUE);
    free (pc->name);
    free (pc);
}

static int
emit_package (lcmgen_t *lcm, _package_contents_t *pc)
{
    // create the package directory, if necessary
    char **dirs = g_strsplit (pc->name, ".", 0);
    char *pdname = build_filenamev (dirs);
    char package_dir[PATH_MAX];
    char package_dir_prefix[PATH_MAX];
    int have_package = dirs[0] != NULL;
    int write_init_py = !getopt_get_bool(lcm->gopt, "python-no-init");

    sprintf (package_dir_prefix, "%s%s", getopt_get_string(lcm->gopt, "ppath"), 
            strlen(getopt_get_string(lcm->gopt, "ppath")) > 0 ? 
            G_DIR_SEPARATOR_S : "");
    sprintf(package_dir, "%s%s%s", package_dir_prefix, pdname,
            have_package ? G_DIR_SEPARATOR_S : "");
    free (pdname);
    if (strlen (package_dir)) {
        if (! g_file_test (package_dir, G_FILE_TEST_EXISTS)) {
//            g_mkdir_with_parents (package_dir, 0755);
            mkdir_with_parents (package_dir, 0755);
        }
        if (!g_file_test (package_dir, G_FILE_TEST_IS_DIR)) {
            err ("Could not create directory %s\n", package_dir);
            return -1;
        }
    }

    // write the package __init__.py files, if necessary
    FILE *init_py_fp = NULL;
    GHashTable * init_py_imports = g_hash_table_new_full(g_str_hash, 
            g_str_equal, free, NULL);
    if (have_package && write_init_py) {
        int ndirs = 0;
        for (ndirs=0; dirs[ndirs]; ndirs++);

        for (int i=0 ; i<ndirs; i++) {
            char *initpy_fname_parts[1024];
            assert(ndirs + 4 < 1024);

            initpy_fname_parts[0] = package_dir_prefix;
            for (int j=0; j<=i; j++) {
                initpy_fname_parts[j+1] = dirs[j];
            }
            initpy_fname_parts[i+2] = "__init__.py";
            initpy_fname_parts[i+3] = NULL;

            char *initpy_fname = build_filenamev (initpy_fname_parts);
            int created_initpy = 0;

            // close init_py_fp if already open
            if(init_py_fp) {
                fclose(init_py_fp);
                init_py_fp = NULL;
            }

            if (! g_file_test (initpy_fname, G_FILE_TEST_EXISTS)) {
                // __init__.py does not exist for this package.  Create it.
                created_initpy = 1;
                init_py_fp = fopen(initpy_fname, "w");
            } else {
                // open the existing __init__.py file, and make note of the
                // modules it imports
                created_initpy = 0;
                init_py_fp = fopen(initpy_fname, "r+");
            }

            if (!init_py_fp) {
                perror ("fopen");
                free (initpy_fname);
                return -1;
            }
#ifndef WIN32
            // lock __init__.py for exclusive write access
            // TODO do the equivalent in windows
            struct flock lockinfo;
            lockinfo.l_type = F_WRLCK;
            lockinfo.l_start = 0;
            lockinfo.l_whence = SEEK_SET;
            lockinfo.l_len = 0 ;
            lockinfo.l_pid = getpid();
            if(0 != fcntl(fileno(init_py_fp), F_SETLKW, &lockinfo)) {
                perror("locking __init__.py");
                free(initpy_fname);
                fclose(init_py_fp);
                return -1;
            }
#endif

            if(created_initpy) {
                fprintf (init_py_fp, "\"\"\"LCM package __init__.py file\n"
                        "This file automatically generated by lcm-gen.\n"
                        "DO NOT MODIFY BY HAND!!!!\n"
                        "\"\"\"\n\n");
            } else {
                while(!feof(init_py_fp)) {
                    char buf[4096];
                    memset(buf, 0, sizeof(buf));
                    char *result = fgets(buf, sizeof(buf)-1, init_py_fp);
                    if(!result)
                        break;

                    g_strstrip(buf);
                    char **words = g_strsplit(buf, " ", -1);
                    if(!words[0] || !words[1] || !words[2] || !words[3])
                        continue;
                    if(!strcmp(words[0], "from") && !strcmp(words[2], "import")) {
                        char *module_name = strdup(words[1]+1); // ignore leading dot
                        g_hash_table_replace(init_py_imports, module_name, 
                                module_name);
                    }

                    g_strfreev(words);
                }
            }
            free (initpy_fname);
        }
    }
    g_strfreev (dirs);

    ////////////////////////////////////////////////////////////
    // ENUMS
    for (int i=0; i<pc->enums->len; i++) {
        lcm_enum_t *le = (lcm_enum_t *) g_ptr_array_index (pc->enums, i);

        char path[PATH_MAX];
        sprintf (path, "%s%s.py", package_dir, le->enumname->shortname);

        if(init_py_fp && 
           !g_hash_table_lookup(init_py_imports, le->enumname->shortname))
            fprintf(init_py_fp, "from .%s import %s\n", 
                    le->enumname->shortname,
                    le->enumname->shortname);

        if (!lcm_needs_generation(lcm, le->lcmfile, path))
            continue;

        FILE *f = fopen(path, "w");
        if (f==NULL) return -1;

        fprintf(f, "\"\"\"LCM type definitions\n"
                "This file automatically generated by lcm.\n"
                "DO NOT MODIFY BY HAND!!!!\n"
                "\"\"\"\n"
                "\n"
                "try:\n"
                "    import cStringIO.StringIO as BytesIO\n"
                "except ImportError:\n"
                "    from io import BytesIO\n"
                "import struct\n\n");

        // enums always encoded as int32
        emit (0, "class %s(object):", le->enumname->shortname);
        emit (1, "__slots__ = [ \"value\" ]");
        for (unsigned int v = 0; v < le->values->len; v++) {
            lcm_enum_value_t *lev = (lcm_enum_value_t *) g_ptr_array_index(le->values, v);
            emit(1, "%s = %i", lev->valuename, lev->value);
        }

        // clang-format off
        emit(1, "_packed_fingerprint = struct.pack(\">Q\", 0x%"PRIx64")", le->hash);
        fprintf(f, "\n");

        emit(1, "def __init__ (self, value):");
        emit(2,     "self.value = value");
        fprintf(f, "\n");

        emit(1, "def _get_hash_recursive(parents):");
        emit(2,     "return 0x%"PRIx64, le->hash);
        emit(1, "_get_hash_recursive=staticmethod(_get_hash_recursive)");
        emit(1, "def _get_packed_fingerprint():");
        emit(2,     "return %s._packed_fingerprint", le->enumname->shortname);
        emit(1, "_get_packed_fingerprint = staticmethod(_get_packed_fingerprint)");
        fprintf(f, "\n");

        emit(1, "def encode(self):");
        emit(2,     "return struct.pack(\">Qi\", 0x%"PRIx64", self.value)", le->hash);

        emit(1, "def _encode_one(self, buf):");
        emit(2,     "buf.write (struct.pack(\">i\", self.value))");
        fprintf(f, "\n");

        emit(1, "def decode(data):");
        emit(2,     "if hasattr (data, 'read'):");
        emit(3,         "buf = data");
        emit(2,     "else:");
        emit(3,         "buf = BytesIO(data)");
        emit(2,     "if buf.read(8) != %s._packed_fingerprint:", le->enumname->shortname);
        emit(3,         "raise ValueError(\"Decode error\")");
        emit(2,     "return %s(struct.unpack(\">i\", buf.read(4))[0])", le->enumname->shortname);
        emit(1, "decode = staticmethod(decode)");

        emit(1, "def _decode_one(buf):");
        emit(2,     "return %s(struct.unpack(\">i\", buf.read(4))[0])", le->enumname->shortname);
        emit(1, "_decode_one = staticmethod(_decode_one)");
        // clang-format on

        fprintf(f, "\n");
        fclose(f);
    }

    ////////////////////////////////////////////////////////////
    // STRUCTS
    for (int i = 0; i < pc->structs->len; i++) {
        lcm_struct_t *ls = (lcm_struct_t *) g_ptr_array_index(pc->structs, i);

        char path[PATH_MAX];
        sprintf(path, "%s%s.py", package_dir, ls->structname->shortname);

        if (init_py_fp && !g_hash_table_lookup(init_py_imports, ls->structname->shortname))
            fprintf(init_py_fp, "from .%s import %s\n", ls->structname->shortname,
                    ls->structname->shortname);

        if (!lcm_needs_generation(lcm, ls->lcmfile, path))
            continue;

        FILE *f = fopen(path, "w");
        if (f == NULL)
            return -1;

        fprintf(f,
                "\"\"\"LCM type definitions\n"
                "This file automatically generated by lcm.\n"
                "DO NOT MODIFY BY HAND!!!!\n"
                "\"\"\"\n"
                "\n"
                "try:\n"
                "    import cStringIO.StringIO as BytesIO\n"
                "except ImportError:\n"
                "    from io import BytesIO\n"
                "import struct\n\n");

        emit_python_dependencies(lcm, f, ls);

        fprintf(f, "class %s(object):\n", ls->structname->shortname);
        fprintf(f, "    __slots__ = [");
        for (unsigned int member = 0; member < ls->members->len; member++) {
            lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, member);
            fprintf(f, "\"%s\"%s", lm->membername, member < ls->members->len - 1 ? ", " : "");
        }
        fprintf(f, "]\n\n");

        fprintf(f, "    __typenames__ = [");
        for (unsigned int member = 0; member < ls->members->len; member++) {
            lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, member);
            fprintf(f, "\"%s\"%s", lm->type->lctypename, member < ls->members->len - 1 ? ", " : "");
        }
        fprintf(f, "]\n\n");

        fprintf(f, "    __dimensions__ = [");
        for (unsigned int member = 0; member < ls->members->len; member++) {
            lcm_member_t *lm = (lcm_member_t *) g_ptr_array_index(ls->members, member);
            if (!lm->dimensions->len) {
                fprintf(f, "None");
            } else {
                fprintf(f, "[");
                for (int n = 0; n < lm->dimensions->len; n++) {
                    lcm_dimension_t *dim = (lcm_dimension_t *) g_ptr_array_index(lm->dimensions, n);
                    if (dim->mode == LCM_CONST) {
                        fprintf(f, "%s", dim->size);
                    } else {
                        fprintf(f, "\"%s\"", dim->size);
                    }
                    fprintf(f, "%s", (n < lm->dimensions->len - 1) ? ", " : "");
                }
                fprintf(f, "]");
            }
            fprintf(f, "%s", (member < ls->members->len - 1) ? ", " : "");
        }
        fprintf(f, "]\n\n");

        // CONSTANTS
        for (unsigned int cn = 0; cn < g_ptr_array_size(ls->constants); cn++) {
            lcm_constant_t *lc = (lcm_constant_t *) g_ptr_array_index(ls->constants, cn);
            assert(lcm_is_legal_const_type(lc->lctypename));
            emit(1, "%s = %s", lc->membername, lc->val_str);
        }
        if (g_ptr_array_size(ls->constants) > 0)
            emit(0, "");

        emit_python_init(lcm, f, ls);
        emit_python_encode(lcm, f, ls);
        emit_python_encode_one(lcm, f, ls);
        emit_python_decode(lcm, f, ls);
        emit_python_decode_one(lcm, f, ls);
        emit_python_fingerprint(lcm, f, ls);
        fclose(f);
    }

    if (init_py_fp)
        fclose(init_py_fp);
    g_hash_table_destroy(init_py_imports);
    return 0;
}

int emit_python(lcmgen_t *lcm)
{
    GHashTable *packages = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
                                                 (GDestroyNotify) _package_contents_free);

    // group the enums and structs by package
    for (unsigned int i = 0; i < lcm->enums->len; i++) {
        lcm_enum_t *le = (lcm_enum_t *) g_ptr_array_index(lcm->enums, i);
        _package_contents_t *pc =
            (_package_contents_t *) g_hash_table_lookup(packages, le->enumname->package);
        if (!pc) {
            pc = _package_contents_new(le->enumname->package);
            g_hash_table_insert(packages, pc->name, pc);
        }
        g_ptr_array_add(pc->enums, le);
    }

    for (unsigned int i = 0; i < lcm->structs->len; i++) {
        lcm_struct_t *ls = (lcm_struct_t *) g_ptr_array_index(lcm->structs, i);
        _package_contents_t *pc =
            (_package_contents_t *) g_hash_table_lookup(packages, ls->structname->package);
        if (!pc) {
            pc = _package_contents_new(ls->structname->package);
            g_hash_table_insert(packages, pc->name, pc);
        }
        g_ptr_array_add(pc->structs, ls);
    }

    GPtrArray *vals = _hash_table_get_vals(packages);

    for (int i = 0; i < vals->len; i++) {
        _package_contents_t *pc = (_package_contents_t *) g_ptr_array_index(vals, i);
        int status = emit_package(lcm, pc);
        if (0 != status)
            return status;
    }

    g_ptr_array_free(vals, TRUE);

    g_hash_table_destroy(packages);
    return 0;
}
