# This file is part of OpenModelica.
#
# Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC),
# c/o Linköpings universitet, Department of Computer and Information Science,
# SE-58183 Linköping, Sweden.
#
# All rights reserved.
#
# THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
# THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
# ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
# RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
# VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
#
# The OpenModelica software and the OSMC (Open Source Modelica Consortium)
# Public License (OSMC-PL) are obtained from OSMC, either from the above
# address, from the URLs:
# http://www.openmodelica.org or
# https://github.com/OpenModelica/ or
# http://www.ida.liu.se/projects/OpenModelica,
# and in the OpenModelica distribution.
#
# GNU AGPL version 3 is obtained from:
# https://www.gnu.org/licenses/licenses.html#GPL
#
# This program is distributed WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
# IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
#
# See the full OSMC Public License conditions for more details.

import logging
from pathlib import Path

from lxml import etree as ET
from OMSimulator import capi
from OMSimulator.cref import CRef
from OMSimulator.connector import Connector
from OMSimulator.fmu import FMU
from OMSimulator.settings import suppress_path_to_str
from OMSimulator.system import System
from OMSimulator.ssv import SSV
from OMSimulator.unit import Unit
from OMSimulator.enumeration import Enumeration
from OMSimulator.instantiated_model import InstantiatedModel
from OMSimulator import namespace, utils
from datetime import datetime

logger = logging.getLogger(__name__)


class SSD:
  def __init__(self, name: str):
    '''Initialize an SSD object.'''
    from OMSimulator.ssp import SSP

    self._name = name
    self._filename = None
    ## TODO change the System name to "root" or "default" or "main" or "top" or something
    self.system = System(name)
    self.startTime = 0.0
    self.stopTime = 1.0
    self.resultFile = self._name + "_res.mat"
    self.loggingInterval = 0.0
    self.bufferSize = 10
    ## default solver settings
    self.initialStepSize = 1e-6
    self.minimumStepSize = 1e-12
    self.maximumStepSize = 1e-3
    self.tolerance = 1e-4
    self.unitDefinitions = list()
    self.enumerationDefinitions = list()

  @staticmethod
  def importFromFile(filename: Path, resources = None):
    '''Imports an SSD file and parses its contents.'''
    try:
      # Determine input type
      tree = ET.parse(filename)
      root = tree.getroot()
      filename = Path(filename).resolve()
      return SSD.importFromNode(root, filename, resources)
    except ET.ParseError as e:
      logger.error(f"Error parsing SSD file '{filename}': {e}")
      return None

  @staticmethod
  def importFromString(xml_code: str, resources = None):
    '''Imports an SSD from a string.'''
    try:
      root = ET.fromstring(xml_code.encode("utf-8"))
      return SSD.importFromNode(root, None, resources)
    except ET.ParseError as e:
      logger.error(f"Error parsing SSD string: {e}")
      return None

  @staticmethod
  def importFromNode(root, filename, resources = None):
    '''Imports an SSD from an XML node.'''
    utils.validateSSP(root, filename, "SystemStructureDescription.xsd")
    variant_name = root.get("name")
    ssd = SSD(variant_name)
    ssd._filename = filename
    system = root.find("ssd:System", namespaces=namespace.ns)
    if system is None:
      raise ValueError(f"Invalid SSD file: Missing <ssd:System> in {filename}")
    ssd.system = System.importFromNode(system, ssd, resources)
    utils.parseDefaultExperiment(root, ssd)
    Unit.importFromNode(root, ssd)
    Enumeration.importFromNode(root, ssd)
    logger.debug(f"SSD '{variant_name}' successfully imported from {filename}")
    return ssd

  @property
  def name(self):
    return self._name

  @name.setter
  def name(self, name: str):
    self._name = name

  @property
  def dirty(self):
    '''Returns True if the SSD has been modified since the last export.'''
    return True

  def _validateCref(self, cref: CRef):
    if self.system is None:
      raise ValueError("Variant doesn’t contain a system")

    first = cref.first()
    if str(first) != self.system.name:
        raise ValueError(f"System '{first}' not found in active variant")
    return cref.pop_first()

  def duplicate(self, variant_name: str | None = None):
    '''Duplicates the SSD and returns a new SSD object.'''
    xml_code = self.export()
    ssd = SSD.importFromString(xml_code)
    if variant_name:
      ssd.name = variant_name
    return ssd

  def addComponent(self, cref: CRef, resource: str, inst : FMU | None = None):
    subcref = self._validateCref(cref)
    return self.system.addComponent(subcref, resource, inst)

  def replaceComponent(self, cref: CRef, resource: str, inst : FMU | None = None, dryRun: bool = False):
    subcref = self._validateCref(cref)
    return self.system.replaceComponent(subcref, resource, inst, dryRun)

  def delete(self, cref: CRef):
    subcref = self._validateCref(cref)
    if subcref is None:
      del self.system # Delete the entire system
      self.system = None
      return
    return self.system.delete(subcref)

  def rename(self, cref: CRef, new_name: CRef):
    subcref = self._validateCref(cref)
    if subcref is None:
      # Renaming the root system itself.
      self.system.name = str(new_name)
      return
    self.system.rename(subcref, new_name)

  def addSSVReference(self, cref: CRef, resource1: str, resource2: str | None = None):
    subcref = self._validateCref(cref)
    self.system.addSSVReference(subcref, resource1, resource2)

  def addMetaDataReference(self, cref: CRef, resource: str, kind: str, type: str):
    subcref = self._validateCref(cref)
    self.system.addMetaDataReference(subcref, resource, kind, type)

  def exportSSVTemplate(self, cref: CRef, node):
    subcref = self._validateCref(cref)

    if subcref is None:
      return self.system.exportSSVTemplateHelper(node)

    self.system.exportSSVTemplate(subcref, node)

  def exportSSMTemplate(self, cref: CRef, node):
    subcref = self._validateCref(cref)

    if subcref is None:
      return self.system.exportSSMTemplateHelper(node)

    self.system.exportSSMTemplate(subcref, node)

  def swapSSVReference(self, cref: CRef, resource1, resource2):
    subcref = self._validateCref(cref)
    self.system.swapSSVReference(subcref, resource1, resource2)

  def listSSVReference(self, cref: CRef):
    subcref = self._validateCref(cref)
    return self.system.listSSVReference(subcref)

  def deleteResource(self, resource: str):
    '''Removes a resource from the SSP.'''
    self.system.deleteResource(resource)

  def removeSSVReference(self, cref: CRef, resource):
    subcref = self._validateCref(cref)
    self.system.removeSSVReference(subcref, resource)

  def addConnection(self, cref1: CRef, cref2: CRef):
    subcref1 = self._validateCref(cref1)
    subcref2 = self._validateCref(cref2)
    #logger.debug(f"Adding connection from {subcref1} to {subcref2}")
    self.system._addConnection(subcref1, subcref2)

  def deleteConnection(self, cref1: CRef, cref2: CRef):
    subcref1 = self._validateCref(cref1)
    subcref2 = self._validateCref(cref2)
    #logger.debug(f"Deleting connection from {subcref1} to {subcref2}")
    self.system._deleteConnection(subcref1, subcref2)

  def newSolver(self, options: dict):
    self.system.solvers.append(options)

  def setSolver(self, cref: CRef, name: str):
    subcref = self._validateCref(cref)
    self.system.setSolver(subcref, name)

  def _getComponentResourcePath(self, cref: CRef):
    subcref = self._validateCref(cref)
    return self.system._getComponentResourcePath(subcref)

  def setValue(self, cref: CRef, value, unit = None, description = None):
    subcref = self._validateCref(cref)
    self.system.setValue(subcref, value, unit, description)

  def getValue(self, cref: CRef):
    subcref = self._validateCref(cref)
    return self.system.getValue(subcref)

  def getElement(self, cref: CRef):
    subcref = self._validateCref(cref)
    return self.system.getElement(subcref)

  def mapParameter(self, cref: CRef, source: str, target: str):
    '''Maps a parameter from source to target in the system.'''
    subcref = self._validateCref(cref)
    self.system.mapParameter(subcref, source, target)

  def addSystem(self, cref: CRef):
    if self.system is None:
      raise ValueError("Variant doesn't contain a system")

    self.system.addSystem(cref.pop_first(first=self._name))

  def addConnector(self, cref: CRef, connector: Connector):
    subcref = self._validateCref(cref)
    self.system.addConnectorHelper(subcref, connector)

  def getConnector(self, cref: CRef):
    subcref = self._validateCref(cref)
    return self.system.getConnector(subcref)

  def getConnection(self, crefA: CRef, crefB: CRef):
    subcrefA = self._validateCref(crefA)
    subcrefB = self._validateCref(crefB)
    return self.system.getConnection(subcrefA, subcrefB)

  def instantiate(self, resources: dict | None = None, tempdir: str | None = None) -> InstantiatedModel:
    if self.system is None:
      raise ValueError("Variant doesn't contain a system")
    simulation_info = {"startTime": self.startTime, "stopTime": self.stopTime, "resultFile": self.resultFile, "loggingInterval": self.loggingInterval, "bufferSize": self.bufferSize}
    json_desc = self.system.generateJson(resources, tempdir, simulation_info)
    return InstantiatedModel(json_desc, self.system, resources)

  def list(self, prefix=""):
    '''Prints the SSD contents.'''
    print(f"{prefix} {type(self)}")
    print(f'{prefix} Variant "{self._name}": {suppress_path_to_str(self._filename)}')

    if self.system:
      self.system.list(prefix=prefix + " |--")

      self.system.getEnumerationDefinitions(self.enumerationDefinitions)
      if self.enumerationDefinitions:
        print(f"{prefix} EnumerationDefinitions:")
        for enumeration in self.enumerationDefinitions:
          enumeration.list(prefix=prefix + " |--")

      self.system.getUnitDefinitions(self.unitDefinitions)
      if self.unitDefinitions:
        print(f"{prefix} UnitDefinitions:")
        for unit in self.unitDefinitions:
          unit.list(prefix=prefix + " |--")


    print(f"{prefix} DefaultExperiment")
    print(f"{prefix} |-- startTime: {self.startTime}")
    print(f"{prefix} |-- stopTime: {self.stopTime}")

  def export(self, filename: str | None = None):
    '''Exports the SSD as an XML file.'''
    root = ET.Element(
      namespace.tag("ssd", "SystemStructureDescription"),
      nsmap=namespace.ns,
      name=self._name,
      version="2.0",
      generationTool= capi.capi().getVersion(),
      generationDateAndTime=datetime.now().isoformat()
    )

    if self.system is not None:
      self.system.export(root)
      self._exportEnumerationDefinitions(root)
      self._exportUnitDefinitions(root)

    self._exportDefaultExperiment(root)

    xml_content = ET.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True).decode("utf-8")

    if filename is None:
      return xml_content
    else:
      with open(filename, "w", encoding="utf-8") as file:
        xml_content = ET.tostring(
          root, encoding="utf-8", xml_declaration=True, pretty_print=True
        ).decode("utf-8")
        file.write(xml_content)

      logger.info(f"SSD '{self._name}' exported to {suppress_path_to_str(filename)}")

  def _exportUnitDefinitions(self, node):
    '''Exports unit definitions to the given XML node.'''
    self.system.getUnitDefinitions(self.unitDefinitions)
    if self.unitDefinitions:
      unit_definitions_node = ET.SubElement(node, namespace.tag("ssd", "Units"))
      for unit in self.unitDefinitions:
        unit.exportToSSD(unit_definitions_node)

  def _exportEnumerationDefinitions(self, node):
    ''' Export enumeration defintions to the given xml node.'''
    self.system.getEnumerationDefinitions(self.enumerationDefinitions)
    if self.enumerationDefinitions:
      enumeration_definitions_node = ET.SubElement(node, namespace.tag("ssd", "Enumerations"))
      for enumeration in self.enumerationDefinitions:
        enumeration.exportToSSD(enumeration_definitions_node)

  def _exportDefaultExperiment(self, node):
    '''Exports default experiment settings.'''
    default_experiment = ET.SubElement(node, namespace.tag("ssd", "DefaultExperiment"))
    default_experiment.set("startTime", str(self.startTime))
    default_experiment.set("stopTime", str(self.stopTime))
    simulation_info = { "resultFile": self.resultFile, "loggingInterval": str(self.loggingInterval), "bufferSize": str(self.bufferSize)}
    utils.exportSimulationInformation(default_experiment, [simulation_info])
