#!/usr/bin/env python3
#
# Main CPython entry-point, called directly from RiskScape.
#
# This code serves as a long-running IPC 'bridge' between RiskScape (Java) and
# the user's CPython function. It takes serialized arguments from a RiskScape
# function call, deserializes the data into Python types, and then passes the
# arguments to the user's Cpython function.
#
# This script gets passed the CPython function's details as text args when it
# first gets spawned by RiskScape, e.g.
#    rs2CPyAdaptor my_function.py Text Floating
#
import os
import sys
import traceback
import serializer
from serializer import Boolean, Integer, RiskscapePyException, Text

import adapter

# first arg is the filepath containing the user's python function
filepath = sys.argv[1]
# second is the root project path -
project_path = sys.argv[2]
# third arg is the function's return-type
return_type = sys.argv[3]
# last comes the types that the function takes as args
arg_types = sys.argv[4:]

WRONG_NUMBER_ARGS = lambda actual, actual_str, expected : ("Your python function takes {0} arguments, "
    " but your INI file specifies {1} 'argument-types' for this function.  "
    "Please ensure the argument-types in the INI file match your Python code").format(actual_str, expected)

logger = adapter.logger
adapter.stop_sdterr_blocking()

(input, output) = adapter.initialize_io()

try:
    sys.path.append(project_path)
    function = adapter.extract_callable(filepath, len(arg_types), WRONG_NUMBER_ARGS)
    adapter.ready(output)

    while True:
        # read the input args that the main Java process passed to us via stdin
        args = []
        for type_name in arg_types:
            data = serializer.read(type_name, input)
            logger.debug("Received {0} arg: {1}".format(type_name, data))
            args.append(data)


        result = None
        try:
            result = function(*args)
        except Exception as e:
            adapter.handle_function_call_failure(output, e)
            # wait for the next method call, maybe it'll work this time?
            continue

        logger.debug("Returning result: {0}".format(result))

        # NB we flush stderr after getting control back from the user's function so we can get any
        # debugging messages back to the user ASAP.  If we did this after flushing stdout, there's
        # the possibility java will see the response and then dispose of the cpython process before
        # stderr ever gets flushed.
        sys.stderr.flush()

        # Write the return value back to the RiskScape Java process,
        # NB we write true for success so that java will expect to deserialize the serialized result
        # and not an exception
        Integer.toBytes(output, 1, None)
        output.flush()
        serializer.write(return_type, output, result)

        # flush streams between invocations - we want java to see whole responses as soon as they
        # are available
        output.flush()

except RiskscapePyException as rse:
    # we hit an error in the serialization code. The traceback is probably
    # completely meaningless to the user, so just display the error message
    print('Error: %s' % rse, file=sys.stderr)
    exit(1)
except Exception as e:
    # we hit an error outside of the user's code that wasn't a serialization failure.
    # This is very likely to be an internal error that only a dev will be able to solve.
    # They will probably see the last thing that was printed to stderr. They can then
    # put that in a bug report - result!
    traceback.print_tb(e.__traceback__.tb_next, file=sys.stderr)
    # Now print out the exception without any stack trace.
    # Note Python works the exception here from the current 'except' case.
    # We could use other traceback APIs here, but they've changed a bit
    # between 3.5 and 3.10, so might cause compatability problems
    traceback.print_exc(limit=0, file=sys.stderr)
    exit(1)
finally:
    output.close
