"""
Generator for C/C++.
"""

# Serge Guelton: The licensing terms are not set in the source package, but
# pypi[1] says the software is under the MIT license, so I reproduce it here
# [1] http://pypi.python.org/pypi/cgen
#
# Copyright (C) 2008 Andreas Kloeckner
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

from textwrap import dedent
from pythran.tables import pythran_ward
from pythran.spec import signatures_to_string

__copyright__ = "Copyright (C) 2008 Andreas Kloeckner"


class Nop(object):

    def generate(self, with_semicolon=True):
        yield ''


class Declarator(object):
    def generate(self, with_semicolon=True):
        tp_lines, tp_decl = self.get_decl_pair()
        tp_lines = list(tp_lines)
        for line in tp_lines[:-1]:
            yield line
        sc = ";" if with_semicolon else ""
        if tp_decl is None:
            yield "%s%s" % (tp_lines[-1], sc)
        else:
            yield "%s %s%s" % (tp_lines[-1], tp_decl, sc)

    def get_decl_pair(self):
        """Return a tuple ``(type_lines, rhs)``.

        *type_lines* is a non-empty list of lines (most often just a
        single one) describing the type of this declarator. *rhs* is the right-
        hand side that actually contains the function/array/constness notation
        making up the bulk of the declarator syntax.
        """

    def inline(self):
        """Return the declarator as a single line."""
        tp_lines, tp_decl = self.get_decl_pair()
        tp_lines = " ".join(tp_lines)
        if tp_decl is None:
            return tp_lines
        else:
            return "%s %s" % (tp_lines, tp_decl)


class Value(Declarator):
    """A simple declarator: *typename* and *name* are given as strings."""

    def __init__(self, typename, name):
        self.typename = typename
        self.name = name

    def get_decl_pair(self):
        return [self.typename], self.name


class NestedDeclarator(Declarator):
    def __init__(self, subdecl):
        self.subdecl = subdecl

    @property
    def name(self):
        return self.subdecl.name

    def get_decl_pair(self):
        return self.subdecl.get_decl_pair()


class DeclSpecifier(NestedDeclarator):
    def __init__(self, subdecl, spec, sep=' '):
        NestedDeclarator.__init__(self, subdecl)
        self.spec = spec
        self.sep = sep

    def get_decl_pair(self):
        def add_spec(sub_it):
            it = iter(sub_it)
            try:
                yield "%s%s%s" % (self.spec, self.sep, next(it))
            except StopIteration:
                pass

            for line in it:
                yield line

        sub_tp, sub_decl = self.subdecl.get_decl_pair()
        return add_spec(sub_tp), sub_decl


class Typedef(DeclSpecifier):
    def __init__(self, subdecl):
        DeclSpecifier.__init__(self, subdecl, "typedef")


class FunctionDeclaration(NestedDeclarator):
    def __init__(self, subdecl, arg_decls, *attributes):
        NestedDeclarator.__init__(self, subdecl)
        self.inline = True
        self.arg_decls = arg_decls
        self.attributes = attributes

    def get_decl_pair(self):
        sub_tp, sub_decl = self.subdecl.get_decl_pair()
        if self.inline:
            sub_tp = ['inline'] + sub_tp
        return sub_tp, ("%s(%s) %s" % (
            sub_decl,
            ", ".join(ad.inline() for ad in self.arg_decls),
            " ".join(self.attributes)))


class Struct(Declarator):

    """
    A structure declarator.

    Attributes
    ----------
    tpname : str
        Name of the structure. (None for unnamed struct)
    fields : [Declarator]
        Content of the structure.
    inherit : str
        Parent class of current structure.
    """

    def __init__(self, tpname, fields, inherit=None):
        """Initialize the structure declarator.  """
        self.tpname = tpname
        self.fields = fields
        self.inherit = inherit

    def get_decl_pair(self):
        """ See Declarator.get_decl_pair."""
        def get_tp():
            """ Iterator generating lines for struct definition. """
            decl = "struct "
            if self.tpname is not None:
                decl += self.tpname
                if self.inherit is not None:
                    decl += " : " + self.inherit
            yield decl
            yield "{"
            for f in self.fields:
                for f_line in f.generate():
                    yield "  " + f_line
            yield "} "
        return get_tp(), ""


# template --------------------------------------------------------------------
class Template(NestedDeclarator):
    def __init__(self, template_spec, subdecl):
        super(Template, self).__init__(subdecl)
        self.template_spec = template_spec

    def generate(self, with_semicolon=False):
        yield "template <%s>" % ", ".join(self.template_spec)
        for i in self.subdecl.generate(with_semicolon):
            yield i
        if not isinstance(self.subdecl, (Template, FunctionDeclaration)):
            yield ";"


# control flow/statement stuff ------------------------------------------------
class ExceptHandler(object):
    def __init__(self, name, body, alias=None):
        self.name = name
        self.body = body
        self.alias = alias

    def generate(self):
        if self.name is None:
            yield "catch(...)"
        else:
            yield "catch (pythonic::types::%s const& %s)" % (self.name,
                                                             self.alias or '')
        for line in self.body.generate():
            yield line


class TryExcept(object):
    def __init__(self, try_, except_):
        self.try_ = try_
        self.except_ = except_

    def generate(self):
        yield "try"

        for line in self.try_.generate():
            yield line

        for exception in self.except_:
            for line in exception.generate():
                yield "  " + line


class If(object):
    def __init__(self, condition, then_, else_=None):
        self.condition = condition
        self.then_ = then_
        self.else_ = else_

    def generate(self):
        yield "if (%s)" % self.condition

        for line in self.then_.generate():
            yield line

        if self.else_ is not None:
            yield "else"
            for line in self.else_.generate():
                yield line


class Loop(object):
    def __init__(self, body):
        self.body = body

    def generate(self):
        yield self.intro_line()
        for line in self.body.generate():
            yield line


class While(Loop):
    def __init__(self, condition, body):
        super(While, self).__init__(body)
        self.condition = condition

    def intro_line(self):
        return "while (%s)" % self.condition


class For(Loop):
    def __init__(self, start, condition, update, body):
        super(For, self).__init__(body)
        self.start = start
        self.condition = condition
        self.update = update

    def intro_line(self):
        return "for (%s; %s; %s)" % (self.start, self.condition, self.update)


class AutoFor(Loop):
    def __init__(self, target, iter_, body):
        super(AutoFor, self).__init__(body)
        self.target = target
        self.iter = iter_

    def intro_line(self):
        if self.target == '_':
            return "for (PYTHRAN_UNUSED auto&& {0}: {1})".format(self.target, self.iter)
        else:
            return "for (auto&& {0}: {1})".format(self.target, self.iter)


# simple statements -----------------------------------------------------------
class Define(object):
    def __init__(self, symbol, value):
        self.symbol = symbol
        self.value = value

    def generate(self):
        yield "#define %s %s" % (self.symbol, self.value)


class Include(object):
    def __init__(self, filename, system=True):
        self.filename = filename
        self.system = system

    def generate(self):
        if self.system:
            yield "#include <%s>" % self.filename
        else:
            yield "#include \"%s\"" % self.filename


class Label(object):
    def __init__(self, label):
        self.label = label

    def generate(self):
        yield self.label + ':;'


class Statement(object):
    def __init__(self, text):
        self.text = text

    def generate(self):
        yield self.text + ";"


class AnnotatedStatement(object):
    def __init__(self, stmt, annotations):
        self.stmt = stmt
        self.annotations = annotations

    def generate(self):
        for directive in self.annotations:
            pragma = "#pragma " + directive.s
            yield pragma.format(*directive.deps)
        for s in self.stmt.generate():
            yield s


class ReturnStatement(Statement):
    def generate(self):
        yield "return " + self.text + ";"


class EmptyStatement(Statement):
    def __init__(self):
        Statement.__init__(self, "")


class Assign(object):
    def __init__(self, lvalue, rvalue):
        self.lvalue = lvalue
        self.rvalue = rvalue

    def generate(self):
        yield "%s = %s;" % (self.lvalue, self.rvalue)


class Line(object):
    def __init__(self, text=""):
        self.text = text

    def generate(self):
        yield self.text


# initializers ----------------------------------------------------------------
class FunctionBody(object):
    def __init__(self, fdecl, body):
        """Initialize a function definition. *fdecl* is expected to be
        a :class:`FunctionDeclaration` instance, while *body* is a
        :class:`Block`.
        """
        self.fdecl = fdecl
        self.body = body

    def generate(self):
        for f_line in self.fdecl.generate(with_semicolon=False):
            yield f_line
        for b_line in self.body.generate():
            yield b_line


# block -----------------------------------------------------------------------
class Block(object):
    def __init__(self, contents=None):
        if contents is None:
            contents = []
        self.contents = contents

    def generate(self):
        yield "{"
        for item in self.contents:
            for item_line in item.generate():
                yield "  " + item_line
        yield "}"


class Module(Block):
    def generate(self):
        for c in self.contents:
            for line in c.generate():
                yield line


class Namespace(Block):
    def __init__(self, name, contents=None):
        Block.__init__(self, contents)
        self.name = name

    def generate(self):
        yield "namespace " + self.name
        yield "{"
        for item in self.contents:
            for item_line in item.generate():
                yield "  " + item_line
        yield "}"


# copy-pasted from codepy.bpl, which is a real mess...
# the original code was under MIT License
# cf. http://pypi.python.org/pypi/codepy
# so I reproduce it here
#
# Copyright (C) 2008 Andreas Kloeckner
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

class PythonModule(object):
    '''
    Wraps the creation of a Pythran module wrapped a Python native Module
    '''
    def __init__(self, name, docstrings, metadata):
        '''
        Builds an empty PythonModule
        '''
        self.name = name
        self.preamble = []
        self.includes = []
        self.functions = {}
        self.global_vars = []
        self.implems = []
        self.capsules = []
        self.python_implems = []
        self.wrappers = []
        self.docstrings = docstrings

        self.metadata = metadata
        moduledoc = self.docstring(self.docstrings.get(None, ""))
        self.metadata['moduledoc'] = moduledoc

    def docstring(self, doc):
        return self.splitstring(dedent(doc).replace('"', '\\"')
                                           .replace('\n', '\\n')
                                           .replace('\r', '\\r'))

    def splitstring(self, doc):
        return '"{}"'.format('\\n""'.join(doc.split('\\n')))

    def add_to_preamble(self, *pa):
        self.preamble.extend(pa)

    def add_to_includes(self, *incl):
        self.includes.extend(incl)

    def add_pyfunction(self, func, name, types, signature):
        self.add_function_to(self.python_implems, func, name, types, signature)

    def add_capsule(self, func, ptrname, sig):
        self.capsules.append((ptrname, sig))
        self.implems.append(func)

    def add_function(self, func, name, types, signature):
        self.add_function_to(self.implems, func, name, types, signature)

    def add_function_to(self, to, func, name, ctypes, signature):
        """
        Add a function to be exposed. *func* is expected to be a
        :class:`cgen.FunctionBody`.

        Because a function can have several signatures exported,
        this method actually creates a wrapper for each specialization
        and a global wrapper that checks the argument types and
        runs the correct candidate, if any
        """
        to.append(func)

        args_unboxing = []  # turns PyObject to c++ object
        args_checks = []  # check if the above conversion is valid
        wrapper_name = pythran_ward + 'wrap_' + func.fdecl.name

        for i, t in enumerate(ctypes):
            args_unboxing.append('from_python<{}>(args_obj[{}])'.format(t, i))
            args_checks.append('is_convertible<{}>(args_obj[{}])'.format(t, i))
        arg_decls = func.fdecl.arg_decls[:len(ctypes)]
        keywords = "".join('"{}", '.format(arg.name) for arg in arg_decls)
        wrapper = dedent('''
            static PyObject *
            {wname}(PyObject *self, PyObject *args, PyObject *kw)
            {{
                PyObject* args_obj[{size}+1];
                {silent_warning}
                char const* keywords[] = {{{keywords} nullptr}};
                if(! PyArg_ParseTupleAndKeywords(args, kw, "{fmt}",
                                                 (char**)keywords {objs}))
                    return nullptr;
                if({checks})
                    return to_python({name}({args}));
                else {{
                    return nullptr;
                }}
            }}''')

        self.wrappers.append(
            wrapper.format(name=func.fdecl.name,
                           silent_warning= '' if ctypes else '(void)args_obj;',
                           size=len(ctypes),
                           fmt="O" * len(ctypes),
                           objs=''.join(', &args_obj[%d]' % i
                                        for i in range(len(ctypes))),
                           args=', '.join(args_unboxing),
                           checks=' && '.join(args_checks) or '1',
                           wname=wrapper_name,
                           keywords=keywords,
                           )
        )

        func_descriptor = wrapper_name, ctypes, signature
        self.functions.setdefault(name, []).append(func_descriptor)

    def add_global_var(self, name, init):
        self.global_vars.append(name)
        self.python_implems.append(Assign('static PyObject* ' + name,
                                   'to_python({})'.format(init)))

    def __str__(self):
        """Generate (i.e. yield) the source code of the
        module line-by-line.
        """
        themethods = []
        theextraobjects = []
        theoverloads = []
        for vname in self.global_vars:
            theextraobjects.append(
                'PyModule_AddObject(theModule, "{0}", {0});'.format(vname))

        for fname, overloads in self.functions.items():
            tryall = []
            signatures = []
            for overload, ctypes, signature in overloads:
                try_ = dedent("""
                    if(PyObject* obj = {name}(self, args, kw))
                        return obj;
                    PyErr_Clear();
                    """.format(name=overload))
                tryall.append(try_)
                signatures.append(signature)

            candidates = signatures_to_string(fname, signatures)

            wrapper_name = pythran_ward + 'wrapall_' + fname

            candidate = dedent('''
            static PyObject *
            {wname}(PyObject *self, PyObject *args, PyObject *kw)
            {{
                return pythonic::handle_python_exception([self, args, kw]()
                -> PyObject* {{
                {tryall}
                return pythonic::python::raise_invalid_argument(
                               "{name}", {candidates}, args, kw);
                }});
            }}
            '''.format(name=fname,
                       tryall="\n".join(tryall),
                       candidates=self.splitstring(
                           candidates.replace('\n', '\\n')
                       ),
                       wname=wrapper_name))

            fdoc = self.docstring(self.docstrings.get(fname, ''))
            themethod = dedent('''{{
                "{name}",
                (PyCFunction){wname},
                METH_VARARGS | METH_KEYWORDS,
                {doc}}}'''.format(name=fname,
                                  wname=wrapper_name,
                                  doc=fdoc))
            themethods.append(themethod)
            theoverloads.append(candidate)

        for ptrname, sig in self.capsules:
            capsule = '''
            PyModule_AddObject(theModule, "{ptrname}",
                               PyCapsule_New((void*)&{ptrname}, "{sig}", NULL)
            );'''.format(ptrname=ptrname,
                         sig=sig)
            theextraobjects.append(capsule)

        methods = dedent('''
            static PyMethodDef Methods[] = {{
                {methods}
                {{NULL, NULL, 0, NULL}}
            }};
            '''.format(methods="".join(m + "," for m in themethods)))

        module = dedent('''
            #if PY_MAJOR_VERSION >= 3
              static struct PyModuleDef moduledef = {{
                PyModuleDef_HEAD_INIT,
                "{name}",            /* m_name */
                {moduledoc},         /* m_doc */
                -1,                  /* m_size */
                Methods,             /* m_methods */
                NULL,                /* m_reload */
                NULL,                /* m_traverse */
                NULL,                /* m_clear */
                NULL,                /* m_free */
              }};
            #define PYTHRAN_RETURN return theModule
            #define PYTHRAN_MODULE_INIT(s) PyInit_##s
            #else
            #define PYTHRAN_RETURN return
            #define PYTHRAN_MODULE_INIT(s) init##s
            #endif
            PyMODINIT_FUNC
            PYTHRAN_MODULE_INIT({name})(void)
            #ifndef _WIN32
            __attribute__ ((visibility("default")))
            #if defined(GNUC) && !defined(__clang__)
            __attribute__ ((externally_visible))
            #endif
            #endif
            ;
            PyMODINIT_FUNC
            PYTHRAN_MODULE_INIT({name})(void) {{
                import_array()
                #if PY_MAJOR_VERSION >= 3
                PyObject* theModule = PyModule_Create(&moduledef);
                #else
                PyObject* theModule = Py_InitModule3("{name}",
                                                     Methods,
                                                     {moduledoc}
                );
                #endif
                if(! theModule)
                    PYTHRAN_RETURN;
                PyObject * theDoc = Py_BuildValue("(sss)",
                                                  "{version}",
                                                  "{date}",
                                                  "{hash}");
                if(! theDoc)
                    PYTHRAN_RETURN;
                PyModule_AddObject(theModule,
                                   "__pythran__",
                                   theDoc);

                {extraobjects}
                PYTHRAN_RETURN;
            }}
            '''.format(name=self.name,
                       extraobjects='\n'.join(theextraobjects),
                       **self.metadata))

        body = (self.preamble +
                self.includes +
                self.implems +
                [Line('#ifdef ENABLE_PYTHON_MODULE')] +
                self.python_implems +
                [Line(code) for code in self.wrappers + theoverloads] +
                [Line(methods), Line(module), Line('#endif')])

        return "\n".join(Module(body).generate())


class CompilationUnit(object):

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

    def __str__(self):
        return '\n'.join('\n'.join(s.generate()) for s in self.body)
