#!/usr/bin/python3
import glob
import os
import re
import subprocess
import hashlib
import sys

# arguments
if len(sys.argv) != 2:
    print("Usage: %s output_directory" % sys.argv[0])
    exit(1)
output_directory = sys.argv[1]
# check path is valid
if not os.path.isdir(output_directory):
    print("Error: '%s' is not a valid directory" % output_directory)
    exit(1)

# parse models.txt
g_models = []
with open('models.txt') as fp:
    for line in fp:
        # we unpack and repack 1) to make the format obvious 2) to catch errors
        mid,name = line.rstrip().split(",")
        g_models.append({'mid': int(mid, 0), 'name': name})
# parse series.txt
g_series = []
g_series_codename = set()
with open('series.txt') as fp:
    for line in fp:
        # we unpack and repack 1) to make the format obvious 2) to catch errors
        arr = line.rstrip().split(",")
        codename = arr[0]
        name = arr[1]
        models = arr[2:]
        # handle empty list
        if len(models) == 1 and models[0] == "":
            models = []
        models = [int(mid,0) for mid in models]
        g_series.append({'codename': codename, 'name': name, 'models': models})
        g_series_codename.add(codename)
# parse all maps in nvp/
# since most nvps are the same, what we actually do is to compute the md5sum hash
# of all files, to identify groups and then each entry in the name is in fact the
# hash, and we only parse one file per hash group
g_hash_nvp = dict() # hash -> nvp
g_nvp_hash = dict() # codename -> hash
HASH_SIZE=6
map_files = glob.glob('nvp/nw*.txt') + glob.glob('nvp/dmp*.txt')
for f in map_files:
    h = hashlib.md5()
    h.update(open(f, "rb").read())
    hash = h.hexdigest()
    codename = re.search('nvp/([^\.]*)\.txt', f).group(1)
    # sanity check
    if not (codename in g_series_codename):
        print("Warning: file %s does not have a match series in series.txt" % f)
    hash = hash[:HASH_SIZE]
    # only keep one file
    if not (hash in g_hash_nvp):
        g_hash_nvp[hash] = set()
    g_hash_nvp[hash].add(codename);
    g_nvp_hash[codename] = hash
# we have some file nodes (nodes-*) but not necessarily for all series
# so for each hash group, try to find at least one
for hash in g_hash_nvp:
    # look at all codename and see if we can find one with a node file
    node_codename = ""
    for codename in g_hash_nvp[hash]:
        if os.path.isfile("nvp/nodes-%s.txt" % codename):
            node_codename = codename
            break
    # if we didn't find one, we just keep the first one
    # otherwise keep the one we found
    if node_codename == "":
        node_codename = g_hash_nvp[hash].pop()
    g_hash_nvp[hash] = node_codename
# for each entry in g_hash_nvp, replace the file name by the actual table
# that we parse, and compute all nvp names at the same time
g_nvp_names = set() # set of all nvp names
g_nvp_desc = dict() # name -> set of all description of a node
g_nvp_size = dict() # name -> set of all possible sizes of a node
for hash in g_hash_nvp:
    codename = g_hash_nvp[hash]
    # extract codename from file
    # parse file
    map = dict()
    with open("nvp/%s.txt" % codename) as fp:
        for line in fp:
            # we unpack and repack 1) to make the format obvious 2) to catch errors
            name,index = line.rstrip().split(",")
            # convert node to integer but be careful of leading 0 (ie 010 is actually
            # 10 in decimal, it is not in octal)
            index = int(index, 10)
            map[index] = name
            g_nvp_names.add(name)
    # parse node map if any
    node_map = dict()
    if os.path.isfile("nvp/nodes-%s.txt" % codename):
        with open("nvp/nodes-%s.txt" % codename) as fp:
            for line in fp:
                # we unpack and repack 1) to make the format obvious 2) to catch errors
                index,size,desc = line.rstrip().split(",")
                # convert node to integer but be careful of leading 0 (ie 010 is actually
                # 10 in decimal, it is not in octal)
                index = int(index, 10)
                desc = desc.rstrip()
                node_map[index] = {'size': size, 'desc': desc}
    # compute final nvp
    nvp = dict()
    for index in map:
        size = 0
        desc = ""
        name = map[index]
        if index in node_map:
            size = node_map[index]["size"]
            desc = node_map[index]["desc"]
        nvp[name] = index
        if not (name in g_nvp_desc):
            g_nvp_desc[name] = set()
        if len(desc) != 0:
            g_nvp_desc[name].add(desc)
        if not (name in g_nvp_size):
            g_nvp_size[name] = set()
        if size != 0:
            g_nvp_size[name].add(size)
    g_hash_nvp[hash] = nvp

#
# generate header
#
header_begin = \
"""\
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \\
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2016 Amaury Pouly
 *
 * This program 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; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#ifndef __NWZ_DB_H__
#define __NWZ_DB_H__

/** /!\ This file was automatically generated, DO NOT MODIFY IT DIRECTLY /!\ */

/* List of all known NVP nodes */
enum nwz_nvp_node_t
{
"""

header_end = \
"""\
    NWZ_NVP_COUNT /* Number of nvp nodes */
};

/* Invalid NVP index */
#define NWZ_NVP_INVALID     -1 /* Non-existent entry */
/* Number of models */
#define NWZ_MODEL_COUNT     %s
/* Number of series */
#define NWZ_SERIES_COUNT    %s

/* NVP node info */
struct nwz_nvp_info_t
{
    const char *name; /* Sony's name: "bti" */
    unsigned long size; /* Size in bytes */
    const char *desc; /* Description: "bootloader image" */
};

/* NVP index map (nwz_nvp_node_t -> index) */
typedef int nwz_nvp_index_t[NWZ_NVP_COUNT];

/* Model info */
struct nwz_model_info_t
{
    unsigned long mid; /* Model ID: first 4 bytes of the NVP mid entry */
    const char *name; /* Human name: "NWZ-E463" */
};

/* Series info */
struct nwz_series_info_t
{
    const char *codename; /* Rockbox codename: nwz-e460 */
    const char *name; /* Human name: "NWZ-E460 Series" */
    int mid_count; /* number of entries in mid_list */
    unsigned long *mid; /* List of model IDs */
    /* Pointer to a name -> index map, nonexistent entries map to NWZ_NVP_INVALID */
    nwz_nvp_index_t *nvp_index;
};

/* List of all NVP entries, indexed by nwz_nvp_node_t */
extern struct nwz_nvp_info_t nwz_nvp[NWZ_NVP_COUNT];
/* List of all models, sorted by increasing values of model ID */
extern struct nwz_model_info_t nwz_model[NWZ_MODEL_COUNT];
/* List of all series */
extern struct nwz_series_info_t nwz_series[NWZ_SERIES_COUNT];

#endif /* __NWZ_DB_H__ */
"""

with open(os.path.join(output_directory, "nwz-db.h"), "w") as fp:
    fp.write(header_begin)
    # generate list of all nvp nodes
    for name in sorted(g_nvp_names):
        # create comment to explain the meaning, gather several meaning together
        # if there are more than one (sorted to keep a stable order when we update)
        explain = ""
        if name in g_nvp_desc:
            explain = " | ".join(sorted(list(g_nvp_desc[name])))
        # overwrite desc set with a single string for later
        g_nvp_desc[name] = explain
        fp.write("    NWZ_NVP_%s, /* %s */\n" % (name.upper(), explain))
    fp.write(header_end % (len(g_models), len(g_series)))

#
# generate tables
#
impl_begin = \
"""\
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \\
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2016 Amaury Pouly
 *
 * This program 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; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

/** /!\ This file was automatically generated, DO NOT MODIFY IT DIRECTLY /!\ */

#include "nwz-db.h"

struct nwz_model_info_t nwz_model[NWZ_MODEL_COUNT] =
{
"""

def by_mid(model):
    return model["mid"]

def by_name(nvp_entry):
    return nvp_entry["name"]

def codename_to_c(codename):
    return re.sub('[^a-zA-Z0-9]', '_', codename, 0)

with open(os.path.join(output_directory, "nwz-db.c"), "w") as fp:
    fp.write(impl_begin)
    # generate model list (sort by mid)
    for model in sorted(g_models, key = by_mid):
        fp.write("    { %s, \"%s\" },\n" % (hex(model["mid"]), model["name"]))
    fp.write("};\n")
    # generate nvps
    for hash in sorted(g_hash_nvp):
        nvp = g_hash_nvp[hash]
        fp.write("\nstatic int nvp_index_%s[NWZ_NVP_COUNT] =\n" % hash)
        fp.write("{\n")
        for name in sorted(g_nvp_names):
            index = "NWZ_NVP_INVALID"
            if name in nvp:
                index = nvp[name]
            fp.write("    [NWZ_NVP_%s] = %s,\n" % (name.upper(), index))
        fp.write("};\n")
    # generate nvp info
    fp.write("\nstruct nwz_nvp_info_t nwz_nvp[NWZ_NVP_COUNT] =\n")
    fp.write("{\n")
    for name in sorted(g_nvp_names):
        size = 0
        if name in g_nvp_size:
            size_set = g_nvp_size[name]
            if len(size_set) == 0:
                size = 0
            elif len(size_set) == 1:
                size = next(iter(size_set))
            else:
                print("Warning: nvp node \"%s\" has several possible sizes: %s"
                    % (name, size_set))
                size = 0
        desc = ""
        if name in g_nvp_desc:
            desc = g_nvp_desc[name]
        fp.write("    [NWZ_NVP_%s] = { \"%s\", %s, \"%s\" },\n" % (name.upper(),
            name, size, desc))
    fp.write("};\n")
    # generate list of models for each series
    for series in g_series:
        c_codename = codename_to_c(series["codename"])
        list = [hex(mid) for mid in series["models"]]
        limit = 3
        c_list = ""
        while len(list) != 0:
            if len(list) <= limit:
                c_list = c_list + ", ".join(list)
                list = []
            else:
                c_list = c_list + ", ".join(list[:limit]) + ",\n    "
                list = list[limit:]
                limit = 6
        fp.write("\nstatic unsigned long models_%s[] = { %s };\n" % (c_codename, c_list))
    # generate series list
    fp.write("\nstruct nwz_series_info_t nwz_series[NWZ_SERIES_COUNT] =\n{\n")
    for series in g_series:
        name = series["name"]
        codename = series["codename"]
        c_codename = codename_to_c(codename)
        nvp = "0"
        if codename in g_nvp_hash:
            nvp = "&nvp_index_%s" % g_nvp_hash[codename]
        fp.write("    { \"%s\", \"%s\", %s, models_%s, %s },\n" % (codename,
            name, len(series["models"]), c_codename, nvp))
    fp.write("};\n")
