#
# Sirael - in-silico patient model
# Copyright (c) since 2020 Tomas Koutny.
#
# Contact:
# rawos@rawos.com
# Tomas Koutny
#
#
# Purpose of this software:
# This software is a personal, research project of Tomas Koutny. It is strictly
# prohibited to use this software for diagnosis or treatment of any medical condition,
# without proving a safety for the intended use.
#
# By using this software, you are responsible for:
#  a) implementing all safety requirements,
#  b) proving the safety for a particular purpose,
#  c) obtaining all regulatory approvals,
#  d) any arising harm.
#
# Especially, a diabetic patient is warned that improper use of this software
# may result into severe injure, including death.
#
#
#
# Licensing terms:
# This software is granted under the Apache 2.0 license for a non-commercial use.
# This software can be granted for a commercial use under a written agreement only.
# Always, you must include and display this header-comment when redistributing the software in any form.
#
# When referring to this work, use the following citation:
# Koutny, Tomas, Siraela - In-Silico Metabolic Simulator with Application for Diabetes (May 18, 2023).
# Available at
#  SSRN: https://ssrn.com/abstract=4452634
#  or http://dx.doi.org/10.2139/ssrn.4452634
#  or git: https://gitlab.com/sirael_metabolic_simulator/
#

#
# Parts of the string-conversion methods of the GUID class were adopted from stringutils.cpp 
# of the Apache-v2.0-licensed SmartCGMS SDK:
#    Tomas Koutny and Martin Ubl, "SmartCGMS as a Testbed for a Blood-Glucose Level Prediction and/or 
#    Control Challenge with (an FDA-Accepted) Diabetic Patient Simulation", Procedia Computer Science,  
#    Volume 177, pp. 354-362, 2020
#


import signal
import sys
import array
import ctypes
import matplotlib.pyplot as plt
import scipy
import numpy as np


class GUID(ctypes.Structure):

    _fields_ = [("Data", ctypes.c_byte * 16)]

    guid_overlay_indexes = [ 3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15 ]	#UUID would start with 0, 1, 2, 3
    guid_parsing_indexes = [ 1, 3, 5, 7, 10, 12, 15, 17, 20, 22, 25, 27, 29, 31, 33, 35 ]

    def __init__(self, str):
        if len(str) != 38:
            raise ValueError("invalid GUID string length")
        if (str[0] != '{') or (str[37] != '}') or (str[9] != '-') or (str[14] != '-') or (str[19] != '-') or (str[24] != '-'):
             raise ValueError("malformed GUID string")

        im = (ctypes.c_byte * 16)(0)

        for i in range(0, 16, 1):
            idx = self.guid_parsing_indexes[i]
            im[self.guid_overlay_indexes[i]] = int(str[idx:idx+2], 16)

        setattr(self, "Data", im)

    def __str__(self):
        result = list("{00000000-0000-0000-0000-000000000000}")
        to_x = "0123456789ABCDEF"

        im = getattr(self, "Data")
        for i in range(0, 16, 1):
            val = im[self.guid_overlay_indexes[i]]
            lo = val & 0xf
            hi = val // 0x10

            idx = self.guid_parsing_indexes[i]
            result[idx] = to_x[hi]
            result[idx+1] = to_x[lo]

        return ''.join(result)

    def __eq__(self, other):
        return bytes(self) == bytes(other)


class Sirael_Terminal(object):

    siraela_lib = 0
    load_metabolism = 0
    free_metabolism = 0
    state_count = 0
    select_state = 0
    step=0
    try_consume = 0
    sampled_level_count=0
    sample_levels=0

    error_buffer_len = 4096
    error_buffer = ctypes.create_string_buffer(error_buffer_len)


    def __init__(self):
        self.siraela_lib = ctypes.cdll.LoadLibrary("siraela_lib.dll")
        

        self.load_metabolism = self.siraela_lib.Load_Metabolism_From_File
        self.load_metabolism.restype = ctypes.c_void_p
        self.load_metabolism.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t]
          
        self.free_metabolism = self.siraela_lib.Free_Metabolism
        self.free_metabolism.argtypes = [ctypes.c_char_p]

        self.state_count = self.siraela_lib.Get_State_Count
        self.state_count.restype = ctypes.c_uint64
        self.state_count.argtypes = [ctypes.c_char_p]

        self.select_state = self.siraela_lib.Select_Metabolism_State
        self.state_count.restype = ctypes.c_uint64
        self.state_count.argtypes = [ctypes.c_char_p, ctypes.c_uint64, ctypes.c_char_p, ctypes.c_size_t]

        self.step = self.siraela_lib.Step
        self.step.restype = ctypes.c_uint64
        self.step.argtypes = [ctypes.c_char_p]
        
        self.try_consume = self.siraela_lib.Try_Consume
        self.try_consume.restype = ctypes.c_uint64
        self.try_consume.argtypes = [ctypes.c_char_p, ctypes.POINTER(GUID), ctypes.c_double]
        

        self.sampled_level_count=self.siraela_lib.Sampled_Level_Count
        self.sampled_level_count.restype = ctypes.c_uint64
        self.sampled_level_count.argtypes = [ctypes.c_char_p]

        self.sample_levels=self.siraela_lib.Sample_Levels	    
        self.sample_levels.restype = ctypes.c_uint64
        self.sample_levels.argtypes = [ctypes.c_char_p, ctypes.POINTER(GUID), ctypes.POINTER(ctypes.c_double)]


    def run(self):
        heartbeat = GUID("{6DFCFD02-C48C-4CE0-BD82-2D941E767A99}")
        bolus = GUID("{09B16B4A-54C2-4C6A-948A-3DEF8533059B}")
        basal = GUID("{B5897BBD-1E32-408A-A0D5-C5BFECF447D9}")
        cho = GUID("{37AA6AC1-6984-4A06-92CC-A660110D0DC7}")
        simulated_cgm_sensor = GUID("{B44F2278-F592-491E-BE20-6DF6999AC2D8}")
        simulated_glycemia = GUID("{1A03867F-2F09-4EB6-8499-AC1C21025A2C}")

        sbr_warned=False

        sir_path ="path to the Siraela IR file"
        scenario_path = "path to the scenario file"

        siraela_handle = self.siraela_lib.Load_Metabolism_From_File(ctypes.c_char_p(sir_path.encode()), self.error_buffer, self.error_buffer_len)
        print(self.error_buffer.value.decode())
        if siraela_handle == None:
            return
        
        signal_count = self.sampled_level_count(ctypes.c_char_p(siraela_handle))
        if signal_count == 0:
            print("No levels to be sampled! It makes no sense => exiting...")
            return

        sampled_levels_vals = (ctypes.c_double * signal_count)()
        sampled_levels_ids = (GUID * signal_count)()

        commands = open(scenario_path, "r")
        for line in commands:
            x = line.split()
            command = x[0]
            if command.startswith("reset") == True:
                #select new state
                sel_state = self.select_state(ctypes.c_char_p(siraela_handle), int(x[1]), self.error_buffer, self.error_buffer_len)
                #print("Selected state: " + str(sel_state))
                print(self.error_buffer.value.decode())
            elif command.startswith("cho") == True:
                #consume carbohydrates
                self.try_consume(ctypes.c_char_p(siraela_handle), cho, float(x[1]))                
            elif command.startswith("bolus") == True:
                #inject insulin bolus
                self.try_consume(ctypes.c_char_p(siraela_handle), bolus, float(x[1]))
            elif command.startswith("basal") == True:
                #set new insulin basal rate - do not use this in serious research!
                self.try_consume(ctypes.c_char_p(siraela_handle), basal, float(x[1]))
                if sbr_warned==False:
                    sbr_warned==True
                    print("Warning: Real pump does not deliver insulin continuously, but discretely as a sequence of small boluses.\n"
									 "This terminal will provide least-effort simulation of a continuous delivery for initial experimentation.\n" 
									 "For a serious research, please, provide a real-pump simulator, which would deliver boluses only.")
            elif command.startswith("heartbeat") == True:
                #set current heartbeat
                self.try_consume(ctypes.c_char_p(siraela_handle), heartbeat, float(x[1]))
            elif command.startswith("advance") == True:
                #do a step
                self.step(ctypes.c_char_p(siraela_handle), int(x[1]))
            elif command.startswith("emit") == True:
                #emit and process the signals
                signals_sampled = self.sample_levels(ctypes.c_char_p(siraela_handle), sampled_levels_ids, sampled_levels_vals)
                if signals_sampled != signal_count:
                    print("Got unexpected number of sampled signals: " + str(signals_sampled))
                    print("Did not you forget to reset the virtual machine to a particular state?")
                for i in range(0, signals_sampled, 1):
                    id = sampled_levels_ids[i]
                    
                    if id==simulated_cgm_sensor:                        
                        print("Simulated CGM sensor glucose level: " + str(sampled_levels_vals[i]))
                    elif id==simulated_glycemia:
                        print("Simulated blood glucose level: " + str(sampled_levels_vals[i]))
                    else:
                        print(str(id) + " : " + str(sampled_levels_vals[i]))
            elif command.startswith("echo") == True:        
                print(line[5:])
            else:
                print("Unknown command! "+line)

sirael = Sirael_Terminal()
sirael.run()