#! /usr/bin/env python3

'''
Use Swig to build wrappers for gsapi.

Example usage:

    Note that we use mupdf's scripts/jlib.py, and assume that there is a mupdf
    checkout in the parent directory of the ghostpdl checkout - see 'import
    jlib' below.

    ./toolbin/gsapiwrap.py --python -l -0 -1 -t
        Build python wrapper for gsapi and run simple test.

    ./toolbin/gsapiwrap.py --csharp -l -0 -1 -t
        Build C# wrapper for gsapi and run simple test.

Args:

    -c:
        Clean language-specific out-dir.

    -l:
        Build libgs.so (by running make).

    -0:
        Run swig to generate language-specific files.

    -1:
        Generate language wrappers by compiling/linking the files generated by
        -0.

    --csharp:
        Generate C# wrappers (requires Mono on Linux). Should usually be first
        param.

    --python
        Generate Python wrappers. Should usually be first param.

    --swig <swig>
        Set location of swig binary.

    -t
        Run simple test of language wrappers generated by -1.

Status:
    As of 2020-05-22:
        Some python wrappers seem to work ok.

        C# wrappers are not implemented for gsapi_set_poll() and
        gsapi_set_stdio().
'''

import os
import re
import sys
import textwrap

import jlib


def devpython_info():
    '''
    Use python3-config to find libpython.so and python-dev include path etc.
    '''
    python_configdir = jlib.system( 'python3-config --configdir', out='return')
    libpython_so = os.path.join(
            python_configdir.strip(),
            f'libpython{sys.version_info[0]}.{sys.version_info[1]}.so',
            )
    assert os.path.isfile( libpython_so), f'cannot find libpython_so={libpython_so}'

    python_includes = jlib.system( 'python3-config --includes', out='return')
    python_includes = python_includes.strip()
    return python_includes, libpython_so

def swig_version( swig='swig'):
    t = jlib.system( f'{swig} -version', out='return')
    m = re.search( 'SWIG Version ([0-9]+)[.]([0-9]+)[.]([0-9]+)', t)
    assert m
    swig_major = int( m.group(1))
    return swig_major


dir_ghostpdl = os.path.abspath( f'{__file__}/../../') + '/'


def out_dir( language):
    if language == 'python':
        return 'gsapiwrap/python/'
    if language == 'csharp':
        return 'gsapiwrap/csharp/'
    assert 0

def out_so( language):
    '''
    Returns name of .so that implements language-specific wrapper. I think
    these names have to match what the language runtime requires.

    For python, Swig generates a module foo.py which does 'import _foo'.

    Similarly C# assumes a file called 'libfoo.so'.
    '''
    if language == 'python':
        return f'{out_dir(language)}_gsapi.so'
    if language == 'csharp':
        return f'{out_dir(language)}libgsapi.so'
    assert 0

def lib_gs_info():
    return f'{dir_ghostpdl}sodebugbin/libgs.so', 'make sodebug'
    return f'{dir_ghostpdl}sobin/libgs.so', 'make so'

def lib_gs():
        '''
        Returns name of the gs shared-library.
        '''
        return lib_gs_info()[0]


def swig_i( swig, language):
    '''
    Returns text for a swig .i file for psi/iapi.h.
    '''
    swig_major = swig_version( swig)


    # We need to redeclare or wrap some functions, e.g. to add OUTPUT
    # annotations. We use #define, %ignore and #undef to hide the original
    # declarations in the .h file.
    #
    fns_redeclare = (
            'gsapi_run_file',
            'gsapi_run_string',
            'gsapi_run_string_begin',
            'gsapi_run_string_continue',
            'gsapi_run_string_end',
            'gsapi_run_string_with_length',
            'gsapi_set_poll',
            'gsapi_set_poll_with_handle',
            'gsapi_set_stdio',
            'gsapi_set_stdio_with_handle',
            'gsapi_new_instance',
            )


    swig_i_text = textwrap.dedent(f'''
            %module(directors="1") gsapi

            %include cpointer.i
            %pointer_functions(int, pint);

            // This seems to be necessary to make csharp handle OUTPUT args.
            //
            %include typemaps.i

            // For gsapi_init_with_args().
            %include argcargv.i

            %include cstring.i

            // Include type information in python doc strings. If we have
            // swig-4, we can propogate comments from the C api instead, which
            // is preferred.
            //
            {'%feature("autodoc", "3");' if swig_major < 4 else ''}

            %{{
                #include "psi/iapi.h"
                //#include "base/gserrors.h"

                // Define wrapper functions that present a modified API that
                // swig can cope with.
                //

                // Swig cannot handle void** out-param.
                //
                static void* new_instance( void* caller_handle, int* out)
                {{
                    void*   ret = NULL;
                    *out = gsapi_new_instance( &ret, caller_handle);
                    printf( "gsapi_new_instance() returned *out=%i ret=%p\\n", *out, ret);
                    fflush( stdout);
                    return ret;
                }}

                // Swig cannot handle (const char* str, int strlen) args.
                //
                static int run_string_continue(void *instance, const char *str, int user_errors, int *pexit_code) {{

                    return gsapi_run_string_continue( instance, str, strlen(str), user_errors, pexit_code);
                }}
            %}}

            // Strip gsapi_ prefix from all generated names.
            //
            %rename("%(strip:[gsapi_])s") "";

            // Tell Swig about gsapi_get_default_device_list()'s out-params, so
            // it adds them to the returned object.
            //
            // I think the '(void) *$1' will ensure that swig code doesn't
            // attempt to free() the returned string.
            //
            {'%cstring_output_allocate_size(char **list, int *listlen, (void) *$1);' if language == 'python' else ''}

            // Tell swig about the (argc,argv) args in gsapi_init_with_args().
            //
            %apply (int ARGC, char **ARGV) {{ (int argc, char **argv) }}

            // Support for wrapping various functions that take function
            // pointer args. For each, we define a wrapper function that,
            // instead of having function pointer args, takes a class with
            // virtual methods. This allows swig to wrap things - python/c# etc
            // can create a derived class that implements these virtual methods
            // in the python/c# world.
            //

            // Wrap gsapi_set_stdio_with_handle().
            //
            %feature("director") set_stdio_class;

            %inline {{
                struct set_stdio_class {{

                    virtual int stdin_fn( char* buf, int len) = 0;
                    virtual int stdout_fn( const char* buf, int len) = 0;
                    virtual int stderr_fn( const char* buf, int len) = 0;

                    static int stdin_fn_wrap( void *caller_handle, char *buf, int len) {{
                        return ((set_stdio_class*) caller_handle)->stdin_fn(buf, len);
                    }}
                    static int stdout_fn_wrap( void *caller_handle, const char *buf, int len) {{
                        return ((set_stdio_class*) caller_handle)->stdout_fn(buf, len);
                    }}
                    static int stderr_fn_wrap( void *caller_handle, const char *buf, int len) {{
                        return ((set_stdio_class*) caller_handle)->stderr_fn(buf, len);
                    }}

                    virtual ~set_stdio_class() {{}}
                }};

                int set_stdio_with_class( void *instance, set_stdio_class* class_) {{
                    return gsapi_set_stdio_with_handle(
                            instance,
                            set_stdio_class::stdin_fn_wrap,
                            set_stdio_class::stdout_fn_wrap,
                            set_stdio_class::stderr_fn_wrap,
                            (void*) class_
                            );
                }}


            }}

            // Wrap gsapi_set_poll().
            //
            %feature("director") set_poll_class;

            %inline {{
                struct set_poll_class {{
                    virtual int poll_fn() = 0;

                    static int poll_fn_wrap( void* caller_handle) {{
                        return ((set_poll_class*) caller_handle)->poll_fn();
                    }}

                    virtual ~set_poll_class() {{}}
                }};

                int set_poll_with_class( void* instance, set_poll_class* class_) {{
                    return gsapi_set_poll_with_handle(
                            instance,
                            set_poll_class::poll_fn_wrap,
                            (void*) class_
                            );
                }}

            }}

            // For functions that we re-declare (typically to specify OUTPUT on
            // one or more args), use a macro to rename the declaration in the
            // header file and tell swig to ignore these renamed declarations.
            //
            ''')

    for fn in fns_redeclare:
        swig_i_text += f'#define {fn} {fn}0\n'

    for fn in fns_redeclare:
        swig_i_text += f'%ignore {fn}0;\n'

    swig_i_text += textwrap.dedent(f'''
            #include "psi/iapi.h"
            //#include "base/gserrors.h"
            ''')

    for fn in fns_redeclare:
        swig_i_text += f'#undef {fn}\n'


    swig_i_text += textwrap.dedent(f'''
            // Tell swig about our wrappers and altered declarations.
            //

            // Use swig's OUTPUT annotation for out-parameters.
            //
            int gsapi_run_file(void *instance, const char *file_name, int user_errors, int *OUTPUT);
            int gsapi_run_string_begin(void *instance, int user_errors, int *OUTPUT);
            int gsapi_run_string_end(void *instance, int user_errors, int *OUTPUT);
            //int gsapi_run_string_with_length(void *instance, const char *str, unsigned int length, int user_errors, int *OUTPUT);
            int gsapi_run_string(void *instance, const char *str, int user_errors, int *OUTPUT);

            // Declare functions defined above that we want swig to wrap. These
            // don't have the gsapi_ prefix, so that they can internally call
            // the wrapped gsapi_*() function. [We've told swig to strip the
            // gsapi_ prefix on generated functions anyway, so this doesn't
            // afffect the generated names.]
            //
            static int run_string_continue(void *instance, const char *str, int user_errors, int *OUTPUT);
            static void* new_instance(void* caller_handle, int* OUTPUT);
            ''')

    if language == 'python':
        swig_i_text += textwrap.dedent(f'''

                // Define python code that is needed to handle functions with
                // function-pointer args.
                //
                %pythoncode %{{

                    set_stdio_g = None
                    def set_stdio( instance, stdin, stdout, stderr):
                        class derived( set_stdio_class):
                            def stdin_fn( self):
                                return stdin()
                            def stdout_fn( self, text, len):
                                return stdout( text, len)
                            def stderr_fn( self, text, len):
                                return stderr( text)

                        global set_stdio_g
                        set_stdio_g = derived()
                        return set_stdio_with_class( instance, set_stdio_g)

                    set_poll_g = None
                    def set_poll( instance, fn):
                        class derived( set_poll_class):
                            def poll_fn( self):
                                return fn()
                        global set_poll_g
                        set_poll_g = derived()
                        return set_poll_with_class( instance, set_poll_g)
                %}}
                ''')

    return swig_i_text



def run_swig( swig, language):
    '''
    Runs swig using a generated .i file.

    The .i file modifies the gsapi API in places to allow specification of
    out-parameters that swig understands - e.g. void** OUTPUT doesn't work.
    '''
    os.makedirs( out_dir(language), exist_ok=True)
    swig_major = swig_version( swig)

    swig_i_text = swig_i( swig, language)
    swig_i_filename = f'{out_dir(language)}iapi.i'
    jlib.update_file( swig_i_text, swig_i_filename)

    out_cpp = f'{out_dir(language)}gsapi.cpp'

    if language == 'python':
        out_lang = f'{out_dir(language)}gsapi.py'
    elif language == 'csharp':
        out_lang = f'{out_dir(language)}gsapi.cs'
    else:
        assert 0

    out_files = (out_cpp, out_lang)

    doxygen_arg = ''
    if swig_major >= 4 and language == 'python':
        doxygen_arg = '-doxygen'

    extra = ''
    if language == 'csharp':
        # Tell swig to put all generated csharp code into a single file.
        extra = f'-outfile gsapi.cs'

    command = (textwrap.dedent(f'''
            {swig}
                -Wall
                -c++
                -{language}
                {doxygen_arg}
                -module gsapi
                -outdir {out_dir(language)}
                -o {out_cpp}
                {extra}
                -includeall
                -I{dir_ghostpdl}
                -ignoremissing
                {swig_i_filename}
            ''').strip().replace( '\n', ' \\\n')
            )

    jlib.build(
            (swig_i_filename,),
            out_files,
            command,
            prefix='    ',
            )


def main( argv):

    swig = 'swig'
    language = 'python'

    args = jlib.Args( sys.argv[1:])
    while 1:
        try:
            arg = args.next()
        except StopIteration:
            break

        if 0:
            pass

        elif arg == '-c':
            jlib.system( f'rm {out_dir(language)}* || true', verbose=1, prefix='    ')

        elif arg == '-l':
            command = lib_gs_info()[1]
            jlib.system( command, verbose=1, prefix='    ')

        elif arg == '-0':
            run_swig( swig, language)

        elif arg == '-1':

            libs = [lib_gs()]
            includes = [dir_ghostpdl]
            file_cpp = f'{out_dir(language)}gsapi.cpp'

            if language == 'python':
                python_includes, libpython_so = devpython_info()
                libs.append( libpython_so)
                includes.append( python_includes)

            includes_text = ''
            for i in includes:
                includes_text += f' -I{i}'
            command = textwrap.dedent(f'''
                    g++
                        -g
                        -Wall -W
                        -o {out_so(language)}
                        -fPIC
                        -shared
                        {includes_text}
                        {jlib.link_l_flags(libs)}
                        {file_cpp}
                    ''').strip().replace( '\n', ' \\\n')
            jlib.build(
                    (file_cpp, lib_gs(), 'psi/iapi.h'),
                    (out_so(language),),
                    command,
                    prefix='    ',
                    )

        elif arg == '--csharp':
            language = 'csharp'

        elif arg == '--python':
            language = 'python'

        elif arg == '--swig':
            swig = args.next()

        elif arg == '-t':

            if language == 'python':
                text = textwrap.dedent('''
                        #!/usr/bin/env python3

                        import os
                        import sys

                        import gsapi

                        gsapi.gs_error_Quit = -101

                        def main():
                            minst, code = gsapi.new_instance(None)
                            print( f'minst={minst} code={code}')

                            if 1:
                                def stdin_local(len):
                                    # Not sure whether this is right.
                                    return sys.stdin.read(len)
                                def stdout_local(text, l):
                                    sys.stdout.write(text[:l])
                                    return l
                                def stderr_local(text, l):
                                    sys.stderr.write(text[:l])
                                    return l
                                gsapi.set_stdio( minst, None, stdout_local, stderr_local);

                            if 1:
                                def poll_fn():
                                    return 0
                                gsapi.set_poll(minst, poll_fn)
                            if 1:
                                s = 'display x11alpha x11 bbox'
                                gsapi.set_default_device_list( minst, s, len(s))

                            e, text = gsapi.get_default_device_list( minst)
                            print( f'gsapi.get_default_device_list() returned e={e} text={text!r}')

                            out = 'out.pdf'
                            if os.path.exists( out):
                                os.remove( out)
                            assert not os.path.exists( out)

                            gsargv = ['']
                            gsargv += f'-dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -sOutputFile={out} contrib/pcl3/ps/levels-test.ps'.split()
                            print( f'gsargv={gsargv}')
                            code = gsapi.set_arg_encoding(minst, gsapi.GS_ARG_ENCODING_UTF8)
                            if code == 0:
                                code = gsapi.init_with_args(minst, gsargv)

                            code, exit_code = gsapi.run_string_begin( minst, 0)
                            print( f'gsapi.run_string_begin() returned code={code} exit_code={exit_code}')
                            assert code == 0
                            assert exit_code == 0

                            gsapi.run_string

                            code1 = gsapi.exit(minst)
                            if (code == 0 or code == gsapi.gs_error_Quit):
                                code = code1
                            gsapi.delete_instance(minst)
                            assert os.path.isfile( out)
                            if code == 0 or code == gsapi.gs_error_Quit:
                                return 0
                            return 1

                        if __name__ == '__main__':
                            code = main()
                            assert code == 0
                            sys.exit( code)
                        ''')
                text = text[1:] # skip leading \n.
                test_py = f'{out_dir(language)}test.py'
                jlib.update_file( text, test_py)
                os.chmod( test_py, 0o744)

                jlib.system(
                        f'LD_LIBRARY_PATH={os.path.abspath( f"{lib_gs()}/..")}'
                            f' PYTHONPATH={out_dir(language)}'
                            f' {test_py}'
                            ,
                        verbose = 1,
                        prefix='    ',
                        )

            elif language == 'csharp':
                # See: https://github.com/swig/swig/blob/master/Lib/csharp/typemaps.i
                #
                text = textwrap.dedent('''
                        using System;
                        public class runme {
                            static void Main() {
                                int code;
                                SWIGTYPE_p_void instance;
                                Console.WriteLine("hello world");
                                instance = gsapi.new_instance(null, out code);
                                Console.WriteLine("code is: " + code);
                                gsapi.add_control_path(instance, 0, "hello");
                            }
                        }
                        ''')
                test_cs = f'{out_dir(language)}test.cs'
                jlib.update_file( text, test_cs)
                files_in = f'{out_dir(language)}gsapi.cs', test_cs
                file_out = f'{out_dir(language)}test.exe'
                command = f'mono-csc -debug+ -out:{file_out} {" ".join(files_in)}'
                jlib.build( files_in, (file_out,), command, prefix='    ')

                ld_library_path = f'{dir_ghostpdl}sobin'
                jlib.system( f'LD_LIBRARY_PATH={ld_library_path} {file_out}', verbose=jlib.log, prefix='    ')

        elif arg == '--tt':
            # small swig test case.
            os.makedirs( 'swig-tt', exist_ok=True)
            i = textwrap.dedent(f'''
                    %include cpointer.i
                    %include cstring.i
                    %feature("autodoc", "3");
                    %cstring_output_allocate_size(char **list, int *listlen, (void) *$1);
                    %inline {{
                    static inline int gsapi_get_default_device_list(void *instance, char **list, int *listlen)
                    {{
                        *list = (char*) "hello world";
                        *listlen = 6;
                        return 0;
                    }}
                    }}
                    ''')
            jlib.update_file(i, 'swig-tt/tt.i')
            jlib.system('swig -c++ -python -module tt -outdir swig-tt -o swig-tt/tt.cpp swig-tt/tt.i', verbose=1)
            p = textwrap.dedent(f'''
                    #!/usr/bin/env python3
                    import tt
                    print( tt.gsapi_get_default_device_list(None))
                    ''')[1:]
            jlib.update_file( p, 'swig-tt/test.py')
            python_includes, python_so = devpython_info()
            includes = f'-I {python_includes}'
            link_flags = jlib.link_l_flags( [python_so])
            jlib.system( f'g++ -shared -fPIC {includes} {link_flags} -o swig-tt/_tt.so swig-tt/tt.cpp', verbose=1)
            jlib.system( f'cd swig-tt; python3 test.py', verbose=1)

        elif arg == '-T':
            # Very simple test that we can create c# wrapper for trivial code.
            os.makedirs( 'swig-cs-test', exist_ok=True)
            example_cpp = textwrap.dedent('''
                    #include <time.h>
                    double My_variable = 3.0;

                    int fact(int n) {
                        if (n <= 1) return 1;
                        else return n*fact(n-1);
                    }

                    int my_mod(int x, int y) {
                        return (x%y);
                    }

                    char *get_time()
                    {
                        time_t ltime;
                        time(&ltime);
                        return ctime(&ltime);
                    }
                    ''')
            jlib.update_file( example_cpp, 'swig-cs-test/example.cpp')

            example_i = textwrap.dedent('''
                    %module example
                    %{
                    /* Put header files here or function declarations like below */
                    extern double My_variable;
                    extern int fact(int n);
                    extern int my_mod(int x, int y);
                    extern char *get_time();
                    %}

                    extern double My_variable;
                    extern int fact(int n);
                    extern int my_mod(int x, int y);
                    extern char *get_time();
                    ''')
            jlib.update_file( example_i, 'swig-cs-test/example.i')

            runme_cs = textwrap.dedent('''
                    using System;
                    public class runme {
                        static void Main() {
                            Console.WriteLine(example.My_variable);
                            Console.WriteLine(example.fact(5));
                            Console.WriteLine(example.get_time());
                        }
                    }
                    ''')
            jlib.update_file( runme_cs, 'swig-cs-test/runme.cs')
            jlib.system( 'g++ -g -fPIC -shared -o swig-cs-test/libfoo.so swig-cs-test/example.cpp', verbose=1)
            jlib.system( 'swig -c++ -csharp -module example -outdir swig-cs-test -o swig-cs-test/example_wrap.cpp -outfile example.cs swig-cs-test/example.i', verbose=1)
            jlib.system( 'g++ -g -fPIC -shared -L swig-cs-test -l foo swig-cs-test/example_wrap.cpp -o swig-cs-test/libexample.so', verbose=1)
            jlib.system( 'cd swig-cs-test; mono-csc -out:runme.exe example.cs runme.cs', verbose=1)
            jlib.system( 'cd swig-cs-test; LD_LIBRARY_PATH=`pwd` ./runme.exe', verbose=1)
            jlib.system( 'ls -l swig-cs-test', verbose=1)


        else:
            raise Exception( f'unrecognised arg: {arg}')

if __name__ == '__main__':
    try:
        main( sys.argv)
    except Exception as e:
        jlib.exception_info( out=sys.stdout)
        sys.exit(1)
