#!/usr/bin/python
"""Utility to generate the header files for BOOST_METAPARSE_STRING"""

# Copyright Abel Sinkovics (abel@sinkovics.hu) 2016.
# Distributed under the Boost Software License, Version 1.0.
#    (See accompanying file LICENSE_1_0.txt or copy at
#          http://www.boost.org/LICENSE_1_0.txt)

import argparse
import math
import os
import sys


VERSION = 1


class Namespace(object):
    """Generate namespace definition"""

    def __init__(self, out_f, names):
        self.out_f = out_f
        self.names = names

    def begin(self):
        """Generate the beginning part"""
        self.out_f.write('\n')
        for depth, name in enumerate(self.names):
            self.out_f.write(
                '{0}namespace {1}\n{0}{{\n'.format(self.prefix(depth), name)
            )

    def end(self):
        """Generate the closing part"""
        for depth in xrange(len(self.names) - 1, -1, -1):
            self.out_f.write('{0}}}\n'.format(self.prefix(depth)))

    def prefix(self, depth=None):
        """Returns the prefix of a given depth. Returns the prefix code inside
        the namespace should use when depth is None."""
        if depth is None:
            depth = len(self.names)
        return '  ' * depth

    def __enter__(self):
        self.begin()
        return self

    def __exit__(self, typ, value, traceback):
        self.end()


def write_autogen_info(out_f):
    """Write the comment about the file being autogenerated"""
    out_f.write(
        '\n'
        '// This is an automatically generated header file.\n'
        '// Generated with the tools/string_headers.py utility of\n'
        '// Boost.Metaparse\n'
    )


class IncludeGuard(object):
    """Generate include guards"""

    def __init__(self, out_f):
        self.out_f = out_f

    def begin(self):
        """Generate the beginning part"""
        name = 'BOOST_METAPARSE_V1_CPP11_IMPL_STRING_HPP'
        self.out_f.write('#ifndef {0}\n#define {0}\n'.format(name))
        write_autogen_info(self.out_f)

    def end(self):
        """Generate the closing part"""
        self.out_f.write('\n#endif\n')

    def __enter__(self):
        self.begin()
        return self

    def __exit__(self, typ, value, traceback):
        self.end()


def macro_name(name):
    """Generate the full macro name"""
    return 'BOOST_METAPARSE_V{0}_{1}'.format(VERSION, name)


def define_macro(out_f, (name, args, body), undefine=False, check=True):
    """Generate a macro definition or undefinition"""
    if undefine:
        out_f.write(
            '#undef {0}\n'
            .format(macro_name(name))
        )
    else:
        if args:
            arg_list = '({0})'.format(', '.join(args))
        else:
            arg_list = ''

        if check:
            out_f.write(
                '#ifdef {0}\n'
                '#  error {0} already defined.\n'
                '#endif\n'
                .format(macro_name(name))
            )

        out_f.write(
            '#define {0}{1} {2}\n'.format(macro_name(name), arg_list, body)
        )


def filename(out_dir, name, undefine=False):
    """Generate the filename"""
    if undefine:
        prefix = 'undef_'
    else:
        prefix = ''
    return os.path.join(out_dir, '{0}{1}.hpp'.format(prefix, name.lower()))


def length_limits(max_length_limit, length_limit_step):
    """Generates the length limits"""
    string_len = len(str(max_length_limit))
    return [
        str(i).zfill(string_len) for i in
        xrange(
            length_limit_step,
            max_length_limit + length_limit_step - 1,
            length_limit_step
        )
    ]


def unique_names(count):
    """Generate count unique variable name"""
    return ('C{0}'.format(i) for i in xrange(0, count))


def generate_take(out_f, steps, line_prefix):
    """Generate the take function"""
    out_f.write(
        '{0}constexpr inline int take(int n_)\n'
        '{0}{{\n'
        '{0}  return {1} 0 {2};\n'
        '{0}}}\n'
        '\n'.format(
            line_prefix,
            ''.join('n_ >= {0} ? {0} : ('.format(s) for s in steps),
            ')' * len(steps)
        )
    )


def generate_make_string(out_f, max_step):
    """Generate the make_string template"""
    steps = [2 ** n for n in xrange(int(math.log(max_step, 2)), -1, -1)]

    with Namespace(
        out_f,
        ['boost', 'metaparse', 'v{0}'.format(VERSION), 'impl']
    ) as nsp:
        generate_take(out_f, steps, nsp.prefix())

        out_f.write(
            '{0}template <int LenNow, int LenRemaining, char... Cs>\n'
            '{0}struct make_string;\n'
            '\n'
            '{0}template <char... Cs>'
            ' struct make_string<0, 0, Cs...> : string<> {{}};\n'
            .format(nsp.prefix())
        )

        disable_sun = False
        for i in reversed(steps):
            if i > 64 and not disable_sun:
                out_f.write('#ifndef __SUNPRO_CC\n')
                disable_sun = True
            out_f.write(
                '{0}template <int LenRemaining,{1}char... Cs>'
                ' struct make_string<{2},LenRemaining,{3}Cs...> :'
                ' concat<string<{4}>,'
                ' typename make_string<take(LenRemaining),'
                'LenRemaining-take(LenRemaining),Cs...>::type> {{}};\n'
                .format(
                    nsp.prefix(),
                    ''.join('char {0},'.format(n) for n in unique_names(i)),
                    i,
                    ''.join('{0},'.format(n) for n in unique_names(i)),
                    ','.join(unique_names(i))
                )
            )
        if disable_sun:
            out_f.write('#endif\n')


def generate_string(out_dir, limits):
    """Generate string.hpp"""
    max_limit = max((int(v) for v in limits))

    with open(filename(out_dir, 'string'), 'wb') as out_f:
        with IncludeGuard(out_f):
            out_f.write(
                '\n'
                '#include <boost/metaparse/v{0}/cpp11/impl/concat.hpp>\n'
                '#include <boost/preprocessor/cat.hpp>\n'
                .format(VERSION)
            )

            generate_make_string(out_f, 512)

            out_f.write(
                '\n'
                '#ifndef BOOST_METAPARSE_LIMIT_STRING_SIZE\n'
                '#  error BOOST_METAPARSE_LIMIT_STRING_SIZE not defined\n'
                '#endif\n'
                '\n'
                '#if BOOST_METAPARSE_LIMIT_STRING_SIZE > {0}\n'
                '#  error BOOST_METAPARSE_LIMIT_STRING_SIZE is greater than'
                ' {0}. To increase the limit run tools/string_headers.py of'
                ' Boost.Metaparse against your Boost headers.\n'
                '#endif\n'
                '\n'
                .format(max_limit)
            )

            define_macro(out_f, (
                'STRING',
                ['s'],
                '{0}::make_string< '
                '{0}::take(sizeof(s)-1), sizeof(s)-1-{0}::take(sizeof(s)-1),'
                'BOOST_PP_CAT({1}, BOOST_METAPARSE_LIMIT_STRING_SIZE)(s)'
                '>::type'
                .format(
                    '::boost::metaparse::v{0}::impl'.format(VERSION),
                    macro_name('I')
                )
            ))

            out_f.write('\n')
            for limit in xrange(0, max_limit + 1):
                out_f.write(
                    '#define {0} {1}\n'
                    .format(
                        macro_name('I{0}'.format(limit)),
                        macro_name('INDEX_STR{0}'.format(
                            min(int(l) for l in limits if int(l) >= limit)
                        ))
                    )
                )
            out_f.write('\n')

            prev_macro = None
            prev_limit = 0
            for length_limit in (int(l) for l in limits):
                this_macro = macro_name('INDEX_STR{0}'.format(length_limit))
                out_f.write(
                    '#define {0}(s) {1}{2}\n'
                    .format(
                        this_macro,
                        '{0}(s),'.format(prev_macro) if prev_macro else '',
                        ','.join(
                            '{0}((s), {1})'
                            .format(macro_name('STRING_AT'), i)
                            for i in xrange(prev_limit, length_limit)
                        )
                    )
                )
                prev_macro = this_macro
                prev_limit = length_limit


def positive_integer(value):
    """Throws when the argument is not a positive integer"""
    val = int(value)
    if val > 0:
        return val
    else:
        raise argparse.ArgumentTypeError("A positive number is expected")


def existing_path(value):
    """Throws when the path does not exist"""
    if os.path.exists(value):
        return value
    else:
        raise argparse.ArgumentTypeError("Path {0} not found".format(value))


def main():
    """The main function of the script"""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        '--boost_dir',
        required=False,
        type=existing_path,
        help='The path to the include/boost directory of Metaparse'
    )
    parser.add_argument(
        '--max_length_limit',
        required=False,
        default=2048,
        type=positive_integer,
        help='The maximum supported length limit'
    )
    parser.add_argument(
        '--length_limit_step',
        required=False,
        default=128,
        type=positive_integer,
        help='The longest step at which headers are generated'
    )
    args = parser.parse_args()

    if args.boost_dir is None:
        tools_path = os.path.dirname(os.path.abspath(__file__))
        boost_dir = os.path.join(
            os.path.dirname(tools_path),
            'include',
            'boost'
        )
    else:
        boost_dir = args.boost_dir

    if args.max_length_limit < 1:
        sys.stderr.write('Invalid maximum length limit')
        sys.exit(-1)

    generate_string(
        os.path.join(
            boost_dir,
            'metaparse',
            'v{0}'.format(VERSION),
            'cpp11',
            'impl'
        ),
        length_limits(args.max_length_limit, args.length_limit_step)
    )


if __name__ == '__main__':
    main()
