#!/usr/bin/env python
#-*- encoding: utf-8 -*-
#
# gen_savestruct.py
# Extracts structures from a C header file and generates save/load code.
#
# Copyright (c) 2008 Pierre "delroth" Bourdon <root@delroth.is-a-geek.org>
# Copyright (c) 2009 Arthur Huillet
# Copyright (c) 2011,2017 Samuel Degrande
#
# 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 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import sys, re

#==============================================================================
# User modifiable lists.
#=====

# List of the 'root' structures for which a read/write function is to be created

dump_structs = [
                 "gps", "point", "pointf",
                 "tux_t", "item", "enemy", "bullet", "blast",
                 "obstacle", "volatile_obstacle", "event_trigger_state",
                 "spell", "melee_shot", "mission", "npc",
                 "upgrade_socket", "keybind_t", "configuration_for_freedroid",
                 "event_timer"
               ]

# For some structures, a missing attribute during file loading is not an
# issue, and no warning is needed

no_warm_for_structs = [ "configuration_for_freedroid" ]

# A list of forbidden types in the structures.
# If such a type is encountered, the script will output an error message, and exit with failure.

forbidden_types = [ "char" ]

# Some types need specific read/write functions (defined in savestruct_internal.c).
# This list prevents the script to auto-generate the read/write functions
# of those types.

hardcoded_types = [ "list_head_t", "keybind_t_array" ]

# Some types are not found inside the dump_structs, but need however read/write
# functions (main use-case: arrays of structures)
# This list contains types for which read/write functions will be added to the
# generated files.

additional_types = [ "bullet_sparse_dynarray", "blast_sparse_dynarray", "spell_sparse_dynarray",
                     "melee_shot_sparse_dynarray", "event_timer_list" ]

# Special types replacements

special_types = {
    #32 bit integers
    'unsigned_int': 'uint32_t',
    'unsigned_long' : 'uint32_t',
    'long' : 'int32_t',
    'int' : 'int32_t',
    #16 bit
    'short_int': 'int16_t',
    'unsigned_short_int' : 'uint16_t',
    'short': 'int16_t',
    #8 bit
    'signed_char' : 'char',
    'unsigned_char' : 'uint8_t'
}

#=====
# End of user modifiable part
#==============================================================================

# The saved structures can contain array or dynarray of a given type.
# The functions reading/writing arrays/dynarrays are defined through some macros
# (define_write_xxxx_array() for example).
# While the content of a structure is scanned, we will fill a dictionary, to
# keep track of the different types used in the structures (unless the type is
# in the forbidden_list).
# Based on that dict, declarations and definitions of functions reading/writing
# arrays or dynarrays will be added to the generated savestruct.[ch] files
#
# Notes:
#   - additional_types are first added to the dict.

tracked_types = {}
for t in additional_types:
    tracked_types[t] = 1

# Parts of regexps
c_id = r'[\w\d]+' # Standard C identifier
c_type = r'((?:(?:unsigned|signed|short|long)\s+)*' + c_id + r'(?:(?:\s+)|(?:\s*\*\s*)))' # C type

# Regexp which search for a structure
find_structure_typedef_rxp = re.compile(r'typedef struct .+?'
                                r'\{'
                                r'([^\}]+)'
                                r'\}'
                                r'\s*(' + c_id + ').*?;', re.M | re.S)
find_structure_notypedef_rxp = re.compile(
                                r'^struct (' + c_id + ').'
                                r'\{'
                                r'([^\}]+)'
                                r'\};', re.M | re.S)

# Regexp which search for a field
find_members_rxp = re.compile(r'\s*' + c_type + r'\s*(' + c_id + r')(?:\s*\[(.+)\])?\s*?;.*')

def write_files_header(output_c, output_h, output_fn):

    output_c.write('#include "' + output_fn + '.h"\n\n')

    output_h.write('''
/**
 * \\file savestruct.h
 * \\brief Lua based save/load subsystem's definitions
 */
#include "savestruct_internal.h"

/// \defgroup genrw Auto-generated read/write of C struct
/// \ingroup luasaveload
///
/// Functions used to read and write the C structures defined in struct.h.
/// These functions are auto-generated by the gen_savestruct.py python script.

''')

def main():

    if len(sys.argv) < 3:
        print("Usage: %s <input.h> <output>") % sys.argv[0]
        sys.exit(1)

    # Open files

    input_fn, output_fn = sys.argv[1:]
    input_f = open(input_fn, 'r')
    output_c = open(output_fn+'.c', 'w')
    output_h = open(output_fn+'.h', 'w')

    # Files prelude

    write_files_header(output_c, output_h, output_fn)

    # Read the whole input file and extract (content, name) pairs of data structures

    input_content = input_f.read()

    structures = find_structure_typedef_rxp.findall(input_content)
    for s in find_structure_notypedef_rxp.findall(input_content):
        structures.append((s[1], 'struct ' + s[0]))

    # Scan the structures' content and for each structure, fill a list of (type, field, size) tuples
    # defining the structure fields.
    # Also keep track of all types used by the structures, by filling the 3 track_simple, track_array
    # and track_dynarray dicts.

    data = {}

    for s in structures:
        # s is a tuple which contains (content, name) with content = the content inside the structure
        content, name = s
        if name not in dump_structs:
            continue

        data[name] = []

        # Extract each line of the structure content, avoiding comments
        lines = []
        in_comment_block = False
        for l in content.split('\n'):
            l = l.split('//')[0]
            if "/*" in l:
                l = l.split('/*')[0]
                lines.append(l)
                in_comment_block = True
            elif "*/" in l:
                in_comment_block = False
                l = l.split('*/')[1]
            if not in_comment_block: lines.append(l)

        # This list will contain all the (type, field, size) tuples
        a = []
        for l in lines:
            m = find_members_rxp.findall(l)
            if len(m) == 0: continue
            else: a.append(m[0])

        # Transform the raw tuple
        for f in a:
            type, field, size = f
            type = type.strip()

            # Replace spaces with '_', replace special type names
            type = type.replace(' ', '_')
            if type in special_types.keys(): type = special_types[type]
            if type in forbidden_types:
                print('Found a "%s" forbidden type while scanning "%s" data structure' % (type, name))
                sys.exit(1)

            # Postfix type name on arrays
            if not size: size = 0
            else: type += '_array'

            #print("got type " + str(type) + " field is " + str(field) + " size is " + str(size))
            data[name].append((type, size, field))

            # Fill the tracking dicts
            if '*' in type:
                continue
            else:
                if type in hardcoded_types: continue
                elif type in tracked_types: continue
                else: tracked_types[type] = 1

    # Writing loop

    for s_name in sorted(data.keys()):

        func_name = s_name.replace('struct ', '')
        must_warn = not s_name in no_warm_for_structs

        header_str  = '/*! \ingroup genrw */\n'
        header_str += 'void write_%s(struct auto_string *, %s *);\n'  % (func_name, s_name)
        header_str += '/*! \ingroup genrw */\n'
        header_str += 'void read_%s(lua_State *, int, %s *);\n' % (func_name, s_name)

        output_h.write(header_str)

        write_str  = 'void write_%s(struct auto_string *strout, %s *data)\n'  % (func_name, s_name)
        write_str += '{\n'
        write_str += '    autostr_append(strout, "{\\n" '
        for (type, size, field) in data[s_name]:
            if not '*' in type:
                # Pointers are not saved (it does not make sense).
                write_str += '"%s = ");\n' % field
                write_str += '    write_%s(strout, %sdata->%s%s);\n' % (type, '' if size else '&', field, (', %s' % size) if size else '')
                write_str += '    autostr_append(strout, ",\\n" '
        write_str += '"}");\n'
        write_str += '}\n\n'

        output_c.write(write_str)

        read_str  = 'void read_%s(lua_State* L, int index, %s *data)\n' % (func_name, s_name)
        read_str += '{\n'
        for (type, size, field) in data[s_name]:
            if '*' in type:
                # Pointers are not saved (it does not make sense). They are initialized to NULL during loading
                if size == 0: read_str += '    data->%s = NULL;\n' % field
                else:         read_str += '    memcpy(data->%s, 0, %s * sizeof(%s));\n' % (field, size, type)
            else:
                if must_warn:
                    read_str += '    if (lua_getfield_or_warn(L, index, "%s")) {\n' % field
                    read_str += '        read_%s(L, -1, %sdata->%s%s);\n' % (type, '' if size else '&', field, (', %s' % size) if size else '')
                    read_str += '        lua_pop(L, 1);\n'
                    read_str += '    }\n'
                else:
                    read_str += '    lua_getfield(L, index, "%s");\n' % field
                    read_str += '    if (!lua_isnil(L, -1)) {\n'
                    read_str += '        read_%s(L, -1, %sdata->%s%s);\n' % (type, '' if size else '&', field, (', %s' % size) if size else '')
                    read_str += '    }\n'
                    read_str += '    lua_pop(L, 1);\n'
        read_str += '}\n\n'

        output_c.write(read_str)

    # Declare and define the functions needed to read/write arrays and dynarrays (based on
    # the content of the type usage tracking dicts.

    header_str = ""
    impl_str = ""

    for s_name in sorted(tracked_types.keys()):

        if '_array' in s_name:

            s_name = s_name.replace('_array', '')
            func_name = s_name.replace('struct ', '')

            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void write_%s_array(struct auto_string *, %s *, int);\n' % (func_name, s_name)
            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void read_%s_array(lua_State *, int, %s *, int);\n' % (func_name, s_name)

            impl_str += 'define_write_xxx_array(%s);\n' % func_name
            impl_str += 'define_read_xxx_array(%s);\n' % func_name

        elif '_sparse_dynarray' in s_name:

            s_name = s_name.replace('_sparse_dynarray', '')
            func_name = s_name.replace('struct ', '')

            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void write_%s_sparse_dynarray(struct auto_string *, %s_sparse_dynarray *);\n' % (func_name, s_name)
            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void read_%s_sparse_dynarray(lua_State *, int, %s_sparse_dynarray *);\n' % (func_name, s_name)

            impl_str += 'define_write_xxx_sparse_dynarray(%s);\n' % s_name
            impl_str += 'define_read_xxx_sparse_dynarray(%s);\n' % s_name

        elif '_dynarray' in s_name:

            s_name = s_name.replace('_dynarray', '')
            func_name = s_name.replace('struct ', '')

            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void write_%s_dynarray(struct auto_string *, %s_dynarray *);\n' % (func_name, s_name)
            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void read_%s_dynarray(lua_State *, int, %s_dynarray *);\n' % (func_name, s_name)

            impl_str += 'define_write_xxx_dynarray(%s);\n' % s_name
            impl_str += 'define_read_xxx_dynarray(%s);\n' % s_name

        elif '_list' in s_name:

            s_name = s_name.replace('_list', '')
            func_name = s_name.replace('struct ', '')

            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void write_%s_list(struct auto_string *, %s_list *);\n' % (func_name, s_name)
            header_str += '/*! \ingroup genrw */\n'
            header_str += 'void read_%s_list(lua_State *, int, %s_list *);\n' % (func_name, s_name)

            impl_str += 'define_write_xxx_list(%s);\n' % s_name
            impl_str += 'define_read_xxx_list(%s);\n' % s_name

    output_h.write(header_str)
    output_c.write(impl_str) 

if __name__ == '__main__': main()
