import ctypes
import os
from enum import Enum


class Status(Enum):
  '''Enumeration for status codes (oms_status_enu_t).'''
  ok = 0
  warning = 1
  discard = 2
  error = 3
  fatal = 4
  pending = 5

## C structure for connection geometry to properly pass the data from Python to C API
class ssd_connection_geometry_t(ctypes.Structure):
  _fields_ = [
    ("pointsX", ctypes.POINTER(ctypes.c_double)),
    ("pointsY", ctypes.POINTER(ctypes.c_double)),
    ("n", ctypes.c_uint)
  ]
## C structure for connector geometry to properly pass the data from Python to C API
class ssd_connector_geometry_t(ctypes.Structure):
  _fields_ = [
    ("x", ctypes.c_double),
    ("y", ctypes.c_double)
  ]
## C structure for element geometry to properly pass the data from Python to C API
class ssd_element_geometry_t(ctypes.Structure):
  _fields_ = [
    ("x1", ctypes.c_double),
    ("y1", ctypes.c_double),
    ("x2", ctypes.c_double),
    ("y2", ctypes.c_double),
    ("rotation", ctypes.c_double),
    ("iconSource", ctypes.c_char_p),
    ("iconRotation", ctypes.c_double),
    ("iconFlip", ctypes.c_bool),
    ("iconFixedAspectRatio", ctypes.c_bool),
  ]

class capi:
  def __init__(self):
    dirname = os.path.dirname(__file__)
    ## look for dll in the current directory for the python pip package
    omslib = os.path.join(dirname, "OMSimulator.dll")

    ## look for dll in the OpenModelica top level directory or OMSimulator stand alone directory
    if not os.path.exists(omslib):
      if os.name == 'nt': # Windows
        omslib = os.path.join(dirname, "../../bin", "OMSimulator.dll")
        self._dllDir = os.add_dll_directory(os.path.dirname(omslib))
      else:
        # attempt to fix #8163 on Linux
        omslib = os.path.join(dirname, "..", "OMSimulator.dll")

    self.obj=ctypes.CDLL(omslib)


    self.obj.oms_addConnection.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]
    self.obj.oms_addConnection.restype = ctypes.c_int
    self.obj.oms_addConnector.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
    self.obj.oms_addConnector.restype = ctypes.c_int
    self.obj.oms_addSubModel.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_addSubModel.restype = ctypes.c_int
    self.obj.oms_addSystem.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_addSystem.restype = ctypes.c_int
    self.obj.oms_addSignalsToResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_addSignalsToResults.restype = ctypes.c_int
    self.obj.oms_compareSimulationResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double]
    self.obj.oms_compareSimulationResults.restype = ctypes.c_int
    self.obj.oms_delete.argtypes = [ctypes.c_char_p]
    self.obj.oms_delete.restype = ctypes.c_int
    self.obj.oms_getVersion.argtypes = None
    self.obj.oms_getVersion.restype = ctypes.c_char_p
    self.obj.oms_getBoolean.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_bool)]
    self.obj.oms_getBoolean.restype = ctypes.c_int
    self.obj.oms_getInteger.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
    self.obj.oms_getInteger.restype = ctypes.c_int
    self.obj.oms_getReal.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
    self.obj.oms_getReal.restype = ctypes.c_int
    self.obj.oms_getString.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
    self.obj.oms_getString.restype = ctypes.c_int
    self.obj.oms_getVariableType.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
    self.obj.oms_getVariableType.restype = ctypes.c_int
    self.obj.oms_getResultFileSignals.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
    self.obj.oms_getResultFileSignals.restype = ctypes.c_int
    self.obj.oms_initialize.argtypes = [ctypes.c_char_p]
    self.obj.oms_initialize.restype = ctypes.c_int
    self.obj.oms_instantiate.argtypes = [ctypes.c_char_p]
    self.obj.oms_instantiate.restype = ctypes.c_int
    self.obj.oms_newModel.argtypes = [ctypes.c_char_p]
    self.obj.oms_newModel.restype = ctypes.c_int
    self.obj.oms_removeSignalsFromResults.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_removeSignalsFromResults.restype = ctypes.c_int
    self.obj.oms_setCommandLineOption.argtypes = [ctypes.c_char_p]
    self.obj.oms_setCommandLineOption.restype = ctypes.c_int
    self.obj.oms_setConnectionLinearTransformation.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double]
    self.obj.oms_setConnectionLinearTransformation.restype = ctypes.c_int
    self.obj.oms_setConnectionGeometry.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ssd_connection_geometry_t)]
    self.obj.oms_setConnectionGeometry.restype = ctypes.c_int
    self.obj.oms_setConnectorGeometry.argtypes = [ctypes.c_char_p, ctypes.POINTER(ssd_connector_geometry_t)]
    self.obj.oms_setConnectorGeometry.restype = ctypes.c_int
    self.obj.oms_setConnectorNumericType.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setConnectorNumericType.restype = ctypes.c_int
    self.obj.oms_setElementGeometry.argtypes = [ctypes.c_char_p, ctypes.POINTER(ssd_element_geometry_t)]
    self.obj.oms_setElementGeometry.restype = ctypes.c_int
    self.obj.oms_setTempDirectory.argtypes = [ctypes.c_char_p]
    self.obj.oms_setTempDirectory.restype = ctypes.c_int
    self.obj.oms_setExportName.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setExportName.restype = ctypes.c_int
    self.obj.oms_setAliasName.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setAliasName.restype = ctypes.c_int
    self.obj.oms_setBoolean.argtypes = [ctypes.c_char_p, ctypes.c_bool]
    self.obj.oms_setBoolean.restype = ctypes.c_int
    self.obj.oms_setInteger.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setInteger.restype = ctypes.c_int
    self.obj.oms_setReal.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setReal.restype = ctypes.c_int
    self.obj.oms_setString.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    self.obj.oms_setString.restype = ctypes.c_int
    self.obj.oms_setSolver.argtypes = [ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setSolver.restype = ctypes.c_int
    self.obj.oms_setResultFile.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
    self.obj.oms_setResultFile.restype = ctypes.c_int
    self.obj.oms_setStartTime.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setStartTime.restype = ctypes.c_int
    self.obj.oms_setStopTime.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setStopTime.restype = ctypes.c_int
    self.obj.oms_setTolerance.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setTolerance.restype = ctypes.c_int
    self.obj.oms_setFixedStepSize.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setFixedStepSize.restype = ctypes.c_int
    self.obj.oms_setVariableStepSize.argtypes = [ctypes.c_char_p, ctypes.c_double, ctypes.c_double, ctypes.c_double]
    self.obj.oms_setVariableStepSize.restype = ctypes.c_int
    self.obj.oms_setLogFile.argtypes = [ctypes.c_char_p]
    self.obj.oms_setLogFile.restype = ctypes.c_int
    self.obj.oms_setLoggingInterval.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_setLoggingInterval.restype = ctypes.c_int
    self.obj.oms_setLoggingLevel.argtypes = [ctypes.c_int]
    self.obj.oms_setLoggingLevel.restype = ctypes.c_int
    self.obj.oms_simulate.argtypes = [ctypes.c_char_p]
    self.obj.oms_simulate.restype = ctypes.c_int
    self.obj.oms_stepUntil.argtypes = [ctypes.c_char_p, ctypes.c_double]
    self.obj.oms_stepUntil.restype = ctypes.c_int
    self.obj.oms_terminate.argtypes = [ctypes.c_char_p]
    self.obj.oms_terminate.restype = ctypes.c_int

  def addConnection(self, crefA, crefB, suppressUnit=False):
    status = self.obj.oms_addConnection(crefA.encode(), crefB.encode(), suppressUnit)
    return Status(status)

  def addConnector(self, cref, causality : int, signal_type : int):
    status = self.obj.oms_addConnector(cref.encode(), causality, signal_type)
    return Status(status)

  def addSubModel(self, model_name: str, submodel_path: str) -> Status:
    '''Add a submodel to the current model.'''
    status = self.obj.oms_addSubModel(model_name.encode('utf-8'), submodel_path.encode('utf-8'))
    return Status(status)

  def addSystem(self, system_name: str, system_type: int) -> Status:
    '''Add a system to the current model.
    system_type: 0 for system, 1 for sub-system.'''
    status = self.obj.oms_addSystem(system_name.encode('utf-8'), system_type)
    return Status(status)

  def addSignalsToResults(self, cref, regex):
    status = self.obj.oms_addSignalsToResults(cref.encode(), regex.encode())
    return Status(status)

  def compareSimulationResults(self, filenameA, filenameB, varA, varB, relTol, absTol):
    result = self.obj.oms_compareSimulationResults(filenameA.encode(), filenameB.encode(), varA.encode(), varB.encode(), relTol, absTol)
    return result

  def delete(self, cref):
    status = self.obj.oms_delete(cref.encode())
    return Status(status)

  def getVersion(self):
    return self.obj.oms_getVersion().decode('utf-8')

  def getReal(self, cref):
    value = ctypes.c_double()
    status = self.obj.oms_getReal(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getInteger(self, cref):
    value = ctypes.c_int()
    status = self.obj.oms_getInteger(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getString(self, cref):
    value = ctypes.c_char_p()
    status = self.obj.oms_getString(cref.encode(), ctypes.byref(value))
    value_ = value.value.decode('utf-8') if value.value else None
    self.obj.oms_freeMemory(value)
    return [value_, Status(status)]

  def getBoolean(self, cref):
    value = ctypes.c_bool()
    status = self.obj.oms_getBoolean(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def getResultFileSignals(self, cref):
    value = ctypes.c_char_p()
    status = self.obj.oms_getResultFileSignals(cref.encode(), ctypes.byref(value))
    signals = value.value.decode('utf-8') if value.value else None
    self.obj.oms_freeMemory(value)
    return [signals, Status(status)]

  def getVariableType(self, cref):
    value = ctypes.c_int()
    status = self.obj.oms_getVariableType(cref.encode(), ctypes.byref(value))
    return [value.value, Status(status)]

  def initialize(self, cref) -> Status:
    '''Enters initialization mode.'''
    status = self.obj.oms_initialize(cref.encode())
    return Status(status)

  def instantiate(self, cref):
    status = self.obj.oms_instantiate(cref.encode())
    return Status(status)

  def newModel(self, model_name: str = "default") -> Status:
    '''Create a new model with the given name.'''
    status = self.obj.oms_newModel(model_name.encode('utf-8'))
    return Status(status)

  def setCommandLineOption(self, cmd):
    status = self.obj.oms_setCommandLineOption(cmd.encode())
    return Status(status)

  def setConnectionLinearTransformation(self, crefA, crefB, factor, offset):
    '''Set the linear transformation for a connection between two connectors.
    The linear transformation is defined as: output = factor * input + offset.'''
    status = self.obj.oms_setConnectionLinearTransformation(crefA.encode(), crefB.encode(), factor, offset)
    return Status(status)

  def setConnectionGeometry(self, crefA, crefB, pointsX, pointsY):
    '''Set the connection geometry for a connection between two connectors.
    The connection geometry is defined by a list of points (pointsX, pointsY) that define the path of the connection in the diagram.'''
    n = len(pointsX)
    if n != len(pointsY):
      raise ValueError("pointsX and pointsY must have the same length")
    geometry = ssd_connection_geometry_t(
      (ctypes.c_double * n)(*pointsX),
      (ctypes.c_double * n)(*pointsY),
      n
    )
    status = self.obj.oms_setConnectionGeometry(crefA.encode(), crefB.encode(), ctypes.byref(geometry))
    return Status(status)

  def setConnectorGeometry(self, cref, x, y):
    '''Set the connector geometry for a connector.
    The connector geometry is defined by a point (x, y) that defines the position of the connector in the diagram.'''
    geometry = ssd_connector_geometry_t(x, y)
    status = self.obj.oms_setConnectorGeometry(cref.encode(), ctypes.byref(geometry))
    return Status(status)

  def setConnectorNumericType(self, cref, numericType):
    '''Set the numeric type for a connector.
    This is used to specify the numeric type of the connector, which is important for FMI3 connectors that can have different numeric types (e.g., float32, float64, int32, int64).'''
    status = self.obj.oms_setConnectorNumericType(cref.encode(), numericType)
    return Status(status)

  def setElementGeometry(self, cref, x1, y1, x2, y2, rotation=0.0, iconSource=b"", iconRotation=0.0, iconFlip=False, iconFixedAspectRatio=False):
    '''Set the element geometry for a model or system.
    The element geometry is defined by a bounding box (x1, y1, x2, y2) that defines the position and size of the element in the diagram, as well as optional rotation and icon information.'''
    geometry = ssd_element_geometry_t(x1, y1, x2, y2, rotation, iconSource, iconRotation, iconFlip, iconFixedAspectRatio)
    status = self.obj.oms_setElementGeometry(cref.encode(), ctypes.byref(geometry))
    return Status(status)

  def setTempDirectory(self, newTempDir):
    status = self.obj.oms_setTempDirectory(newTempDir.encode())
    return Status(status)

  def setExportName(self, cref, exportName):
    '''Set the export name for a model or system.
    This is used to specify the name under which the model will be exported.'''
    status = self.obj.oms_setExportName(cref.encode(), exportName.encode())
    return Status(status)

  def setAliasName(self, cref, aliasName):
    '''Set the export name for top level system and sub-system connectors
    This is used to specify the name under which the model will be exported.'''
    status = self.obj.oms_setAliasName(cref.encode(), aliasName.encode())
    return Status(status)

  def setReal(self, cref, value):
    '''Set a real value for a model or system.'''
    status = self.obj.oms_setReal(cref.encode(), value)
    return Status(status)

  def setInteger(self, cref, value):
    '''Set a integer value for a model or system.'''
    status = self.obj.oms_setInteger(cref.encode(), value)
    return Status(status)

  def setBoolean(self, cref, value):
    '''Set a boolean value for a model or system.'''
    status = self.obj.oms_setBoolean(cref.encode(), value)
    return Status(status)

  def setString(self, cref, value):
    '''Set a string value for a model or system.'''
    status = self.obj.oms_setString(cref.encode(), value.encode())
    return Status(status)

  def setSolver(self, cref, solver):
    status = self.obj.oms_setSolver(cref.encode(), solver)
    return Status(status)

  def setResultFile(self, cref, filename, bufferSize=1) -> Status:
    '''Set the result file for a model or system.
    The bufferSize specifies how many values are buffered before writing to the file.'''
    status = self.obj.oms_setResultFile(cref.encode(), filename.encode(), bufferSize)
    return Status(status)

  def setStartTime(self, cref, startTime) -> Status:
    '''Set the start time for the simulation.'''
    status = self.obj.oms_setStartTime(cref.encode(), startTime)
    return Status(status)

  def setStopTime(self, cref, stopTime) -> Status:
    '''Set the stop time for the simulation.'''
    status = self.obj.oms_setStopTime(cref.encode(), stopTime)
    return Status(status)

  def setTolerance(self, cref, relativeTolerance) -> Status:
    '''Set the relative and absolute tolerance for the simulation.'''
    status = self.obj.oms_setTolerance(cref.encode(), relativeTolerance)
    return Status(status)

  def setFixedStepSize(self, cref, stepSize) -> Status:
    '''Set the variable step size parameters for the simulation.'''
    status = self.obj.oms_setFixedStepSize(cref.encode(), stepSize)
    return Status(status)

  def setVariableStepSize(self, cref, initialStepSize, minStepSize, maxStepSize) -> Status:
    '''Set the variable step size parameters for the simulation.'''
    status = self.obj.oms_setVariableStepSize(cref.encode(), initialStepSize, minStepSize, maxStepSize)
    return Status(status)

  def simulate(self, cref) -> Status:
    '''Exits initialization mode and runs the simulation until stopTime is reached.'''
    status = self.obj.oms_simulate(cref.encode())
    return Status(status)

  def stepUntil(self, cref, stopTime) -> Status:
    '''Step the simulation until the specified stop time.
    Note: If the model is in initialization mode, this will exit initialization mode.'''
    status = self.obj.oms_stepUntil(cref.encode(), stopTime)
    return Status(status)

  def setLogFile(self, filename):
    status = self.obj.oms_setLogFile(filename.encode())
    return Status(status)

  def setLoggingInterval(self, cref, loggingInterval):
    status = self.obj.oms_setLoggingInterval(cref.encode(), loggingInterval)
    return Status(status)

  def setLoggingLevel(self, level):
    status = self.obj.oms_setLoggingLevel(level)
    return Status(status)

  def terminate(self, cref) -> Status:
    '''Terminate the simulation and free resources.'''
    status = self.obj.oms_terminate(cref.encode())
    return Status(status)

  def removeSignalsFromResults(self, cref, regex):
    status = self.obj.oms_removeSignalsFromResults(cref.encode(), regex.encode())
    return Status(status)

Capi = capi()
