#!/usr/bin/env python
# -*- coding: utf-8 -*-

def process_with_timeout(target, timeout):
    import multiprocessing
    import time
    import copy

    manager = multiprocessing.Manager()
    return_dict = manager.dict()
    p = multiprocessing.Process(target=target, args=(0, return_dict))
    p.daemon = True
    start = time.time()
    p.start()
    if (timeout is not None) and (timeout > 0):
        p.join(timeout)
    else:
        p.join()

    cpu_time = time.time() - start

    error_msg = None
    if p.is_alive():
        error_msg = "Process timeout"
        p.terminate()
    elif p.exitcode != 0:
        error_msg = "Process terminated by signal {}".format(-p.exitcode)
    if error_msg is not None:
        raise RuntimeError(error_msg)

    ret = copy.copy(return_dict[0])
    ret.update({'cpu_time': cpu_time})
    return ret


def _translate(proc_num, return_dict):
    import os
    from pymodelica import compile_fmu
    from inspect import signature

    generate_html_diagnostics = {{generate_html_diagnostics}}

    try:
        # OCT r28242 ignores MODELICAPATH and instead needs to have it set through
        # a function argument.
        compilation_log = "{{ model }}".replace('.', '_') + "_compile.log"
        version = "2.0"
        compiler_log_level = f"warning:{compilation_log}"
        compiler_options = {"generate_html_diagnostics": generate_html_diagnostics,
                            "nle_solver_tol_factor": 1e-2}  # 1e-2 is the default

        # If an FMU name ends with (), then it overides the start and stop time declared
        # in the .mo file. Hence we make sure not to add brackets if there is no
        # model modifier.
        model_name_modifier = "{{ model }}({{ model_modifier }})".replace("()", "")

        sig = signature(compile_fmu)
        if "modelicapath" in str(sig):
            # This is the new API that uses modelicapath as a function argument.

            removed_modelica_path = False
            if 'MODELICAPATH' in os.environ:
                modelicapath = os.environ['MODELICAPATH']
                del os.environ['MODELICAPATH']
                removed_modelica_path = True
            else:
                modelicapath = os.path.abspath('.')

            fmu_name = compile_fmu(model_name_modifier,
                                   modelicapath=modelicapath,
                                   version=version,
                                   compiler_log_level=compiler_log_level,
                                   compiler_options=compiler_options)
        else:
            fmu_name = compile_fmu(model_name_modifier,
                                   version=version,
                                   compiler_log_level=compiler_log_level,
                                   compiler_options=compiler_options)

        if removed_modelica_path:
            os.environ['MODELICAPATH'] = modelicapath

        # Copy style sheets.
        # This is a hack to get the css and js files to render the html diagnostics.
        htm_dir = os.path.splitext(os.path.basename(fmu_name))[0] + "_html_diagnostics"
        if generate_html_diagnostics and os.path.exists(htm_dir):
            for fil in ["scripts.js", "style.css", "html-diagnostics.css", "zepto.min.js"]:
                src = os.path.join(".jmodelica_html", fil)
                if os.path.exists(src):
                    des = os.path.join(htm_dir, fil)
                    shutil.copyfile(src, des)

        # Read log file
        out = None
        if os.path.isfile(compilation_log):
            with open(compilation_log, 'r') as f:
                out = f.readlines()
        else:
            out = f"Error: Log file {compilation_log} does not exist."

        # The standard output is returned as a list, with each line being an element
        return_dict[proc_num] = {'success': True, 'fmu_name': str(fmu_name), 'stdout': out}

    except Exception as e:
        return_dict[proc_num] = {'success': False,
                                 'exception': '{}: {}'.format(type(e).__name__, e)}
    return


def _simulate(proc_num, return_dict):
    from pyfmi import load_fmu

    # Import the class that grabs stdout
    import OutputGrabber as og

    if not {{simulate}}:
        return_dict[proc_num] = {'success': False,
                                 'message': 'No simulation requested.'}
        return return_dict

    # Simulate the model

    debug_solver = {{debug_solver}}
    debug_solver_interactive_mode = {{debug_solver_interactive_mode}}

    try:
        fmu_name = "{{ model }}".replace(".", "_") + ".fmu"
        mod = load_fmu(fmu_name)
        x_nominal = mod.nominal_continuous_states

        opts = mod.simulate_options()  # Retrieve the default options
        opts['logging'] = False
        opts['solver'] = '{{ solver }}'
        opts['ncp'] = {{ncp}}

        rtol = {{rtol}}

        if len(x_nominal) > 0:
            atol = rtol * x_nominal
        else:
            atol = rtol

        if opts['solver'] == 'CVode':
            # Set user-specified tolerance if it is smaller than the tolerance in the .mo file
            opts['CVode_options']['external_event_detection'] = False
            opts['CVode_options']['maxh'] = (mod.get_default_experiment_stop_time(
            ) - mod.get_default_experiment_start_time()) / float(opts['ncp'])
            opts['CVode_options']['iter'] = 'Newton'
            opts['CVode_options']['discr'] = 'BDF'
            # True is default, set to false if many events
            opts['CVode_options']['store_event_points'] = True

        if debug_solver:
            opts["logging"] = True  # <- Turn on solver debug logging
            mod.set("_log_level", 4)

        opts['{{ solver }}_options']['rtol'] = rtol
        opts['{{ solver }}_options']['atol'] = atol
        opts['filter'] = {{filter}}
        opts['result_file_name'] = "{{ result_file_name }}"

        # Grab the std output
        out = og.OutputGrabber()
        out.start()
        res = mod.simulate(
            options=opts,
            start_time={{start_time}},
            final_time={{final_time}})
        out.stop()

        ######################################################################
        # Get debugging information
        if debug_solver and debug_solver_interactive_mode:
            # Load the debug information
            from pyfmi.debug import CVodeDebugInformation
            debug = CVodeDebugInformation("{{ model }}".replace(".", "_") + "_debug.txt")

            # Below are options to plot the order, error and step-size evolution.
            # The error methods also take a threshold and a region if you want to
            # limit the plot to a certain interval.

            if opts['solver'].lower() == 'cvode':
                # Plot wall-clock time versus model time
                debug.plot_cumulative_time_elapsed()
                # Plot only the region 0.8 - 1.0 seconds and only state variables with an
                # error greater than 0.01 (for any point in that region)
                debug.plot_error(region=[0.8, 1.0], threshold=0.01)

            # Plot order evolution
            debug.plot_order()

            # Plot error evolution
            debug.plot_error()  # Note see also the arguments to the method

            # Plot the used step-size
            debug.plot_step_size()

            # See also debug?

        start_time = res['time'][0]
        final_time = res['time'][-1]
        return_dict[proc_num] = {
            'success': True,
            'start_time': start_time,
            'final_time': final_time,
            'stdout': out.capturedtext.split('\n')}

    except Exception as e:
        return_dict[proc_num] = {'success': False,
                                 'exception': '{}: {}'.format(type(e).__name__, e)}
    return return_dict


def run():
    import os
    import json
    import traceback
    import sys

    import pymodelica
    # Increase memory
    pymodelica.environ['JVM_ARGS'] = '-Xmx4096m'

    time_out = {{time_out}}
    model = "{{ model }}"
    result = {"model": model,
              "translation": {"success": False},
              "simulation": {"success": False}}

    # Compile model
    log_file = "{}_buildingspy.json".format(model.replace(".", "_"))
    try:
        os.remove(log_file)
    except OSError:
        pass

    try:
        ret_dic = process_with_timeout(target=_translate, timeout=time_out)
        result["translation"] = ret_dic

    except Exception as e:
        msg = "{}: {}".format(type(e).__name__, str(e))
        if msg == "RuntimeError: Process timeout":
            msg = msg + " after {} seconds".format(time_out)
        result["translation"]["exception"] = msg
        result["translation"]["traceback"] = traceback.format_exc()

    # Load model if translation was successful
    if result["translation"]["success"]:
        try:
            ret_dic = process_with_timeout(target=_simulate, timeout=time_out)
            result["simulation"] = ret_dic

        except Exception as e:
            msg = "{}: {}".format(type(e).__name__, str(e))
            if msg == "RuntimeError: Process timeout":
                msg = msg + " after {} seconds".format(time_out)
            result["simulation"]["exception"] = msg
            result["simulation"]["traceback"] = traceback.format_exc()

    with open(log_file, "w") as log:
        log.write("{}\n".format(json.dumps(result, indent=4, sort_keys=False)))


if __name__ == "__main__":
    run()
