#!/usr/bin/python3

import itertools


###############################################################################
def chunks(values, size):
    it = iter(values)
    item = list(itertools.islice(it, size))

    while item:
        yield item
        item = list(itertools.islice(it, size))


###############################################################################
# Construct a variadic macro of the form:
#
# ARITY_DISPATCH(_0001, _0002, NAME, ...) NAME
#
# Where the numbered arguments is equal to the predefined maximum arity.
def generate_dispatch(num):
    values = itertools.chain(
        ("_%04u" % n for n in range (1, num+1)),
        ["NAME"]
    )

    yield "#define ARITY_DISPATCH(\\"
    for group in chunks(values, 8):
        yield ", ".join(group) + ", \\"
    yield "...) NAME"


###############################################################################
# Construct a variadic macro of the form:
#
# define REDUCE_1_0002(F,X,Y) F(X,Y)
# define REDUCE_1_0003(F,X,Y,...) REDUCE_2_0002(F,F(X,Y),__VA_ARGS__)
# define REDUCE_1_0004(F,X,Y,...) REDUCE_2_0003(F,F(X,Y),__VA_ARGS__)
#
# and so on
#
# We do not provide a definition for REDUCE_1_0000 because it does not
# actually reduce anything. Instead we define it as a static_assert to
# simplify the general dispatch generation code.
#
# The macro is defined as if it were arity 1, because the dispatch generation
# code operates in terms of the tail call and offsets the dispatch arguments
# by the arity.
def generate_reduce(num):
    yield '#define REDUCE_1_0000(...) STATIC_ASSERT("REDUCE requires at least two arguments")'
    yield "#define REDUCE_1_0001(F,X,Y) F(X,Y)"

    for n in range(2, num):
        yield "#define REDUCE_1_%04u(F,X,Y,...) REDUCE_1_%04u(F,F(X,Y),__VA_ARGS__)" % (n, n - 1)

    raise StopIteration


###############################################################################
# Generate a variadic macro of the form:
#
# define MAP_ARITY_0000(F,A,B) F(A,B)
# define MAP_ARITY_0001(F,A,B,...) F(X)(MAP_0000_0000(F,A,B,__VA_ARGS___)
# define MAP_ARITY_0002(F,A,B,...) F(X)(MAP_0000_0001(F,A,B,__VA_ARGS___)
#
# and so on...
#
# Note: the arguments A, B, etc are replaced with a quantity of arguments
# indicated by the `arity' parameter.
def generate_map(num, arity):
    args = ",".join("ARG%02u" % n for n in range(arity))
    if args:
        args += ','
    yield "#define MAP_%(arity)u_0001(F,%(args)sX) F(%(args)sX)" % { 'arity': arity, 'args': args }

    for n in range(2, num):
        yield "#define %(curr)s(F,%(args)sX,...) F(%(args)sX)%(next)s(F,%(args)s__VA_ARGS__)" % {
            'curr': 'MAP_%(arity)u_%(index)04u' % { 'arity': arity, 'index': n     },
            'next': 'MAP_%(arity)u_%(index)04u' % { 'arity': arity, 'index': n - 1 },
            'arity': arity,
            'args': args,
        }

    raise StopIteration


##-----------------------------------------------------------------------------
def dispatch(name, num, arity):
    yield "#define %(name)s%(arity)u(FUNC,...)\\" % { 'name': name, 'arity': arity }
    yield "ARITY_DISPATCH(__VA_ARGS__,\\"

    for group in chunks(range(num-arity,0,-1), 4):
        yield ",".join (
            "%(name)s_%(arity)u_%(n)04u" % {
                'name': name,
                'arity': arity,
                'n': n
            } for n in group
        ) + ",\\"

    yield ', STATIC_ASSERT("invalid arity for %s")\\' % name
    yield ')(FUNC,__VA_ARGS__)'


###############################################################################
def generate_argcount(num):
    yield "#define VA_ARGS_COUNT(...)\\"
    yield "ARITY_DISPATCH(__VA_ARGS__,\\"

    literals = ",".join(str(n) for n in range(num,0,-1))
    for group in chunks(range(num, 0, -1), 4):
        yield ", ".join('% 4u' % g for g in group) + ',\\'

    yield ', STATIC_ASSERT("invalid value for VA_ARGS_COUNT"))'


###############################################################################
import argparse


##-----------------------------------------------------------------------------
if __name__ == '__main__':
    #logging.getLogger().setLevel(logging.INFO)

    parser = argparse.ArgumentParser(description='Generate variadic macro functions')
    parser.add_argument('dst', type=str, help='the output path for the header')
    parser.add_argument('num', type=int, help='the maximum argument count')

    args = parser.parse_args()

    dst = open(args.dst, 'w')
    num = args.num
    arity = 3

    dst.write("""\
#ifndef CRUFT_UTIL_PREPROCESSOR_HPP
#define CRUFT_UTIL_PREPROCESSOR_HPP
    
///////////////////////////////////////////////////////////////////////////////
//
// This is autogenerated code, do not modify. Instead, look at preprocessor.py
//
///////////////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////////////
// Here be dragons. For the love of God, only use these macros for token
// pasting applications. Don't judge me...


///////////////////////////////////////////////////////////////////////////////
// Token concatenation wrapper that prevents macro expansion. You should not
// be using this outside of this header file (and probably not even then for
// the most part).
#define PASTE_DETAIL(x, y) x##y

// A token concatenation wrapper. You should prefer PASTE over PASTE2. This
// is defined purely so that PASTE (defined laterR) has something to give
// REDUCE1.
#define PASTE2(x,y) PASTE_DETAIL(x,y)

// Pair token concatenation with a trailing comma.
#define PASTE_LIST(x,y) PASTE2(x,y),

// Pair token concatenation, transforming into a namespace and type.
#define NAMESPACE_LIST(NS,KLASS) NS::KLASS,


#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)
#define STRINGIZE_LIST(x) STRINGIZE(x),


#define STATIC_ASSERT(MSG) static_assert(false, MSG);


///////////////////////////////////////////////////////////////////////////////
    """)

    dst.write("#define MAX_VA_DISPATCH_ARGS %u\n" % num)
    dst.write("#define MAX_VA_DISPATCH_ARITY %u\n" % arity)

    lines = itertools.chain (
        generate_dispatch(num),
        generate_map(num, 0),
        generate_map(num, 1),
        generate_map(num, 2),
        dispatch("MAP", num, 0),
        dispatch("MAP", num, 1),
        dispatch("MAP", num, 2),
        generate_reduce(num),
        dispatch("REDUCE", num, 1),
        [ "#define PASTE(...) REDUCE1(PASTE2,__VA_ARGS__)" ],
        generate_argcount(num)
    )

    for l in lines:
        dst.write(l)
        dst.write('\n')

    dst.writelines("#endif\n")
