from pathlib import Path
from . import Lightpath, LightPathDiagramWrapper, LightPathExportImportSupport, IDocument, DetectorResultObject
from .objects import PhysicalPropertyPy, PhysicalValue, PhysicalValueBase, PhysicalValueComplex, Document
from System.Threading import CancellationTokenSource
from typing import List

def path_check(path: str) -> None:
    """Support method to check, whether the os file exists

    Args:
        path (str): the path to the optical system (os file)

    Raises:
        FileNotFoundError: path does not exist
        FileNotFoundError: path is not a file
        ImportError: path is not an os file
    """
        
    if not Path(path).exists():
        raise FileNotFoundError('Path of the optical system does not exist!')
    if not Path(path).is_file():
        raise FileNotFoundError('Path of the optical system is not a file!')
    if path[-3:] != '.os':
        raise ImportError('Path of the optical system is not an os file!')
    
class ParameterSet():
        
    # Slots
    __slots__ = ('_lpe_index', '_param_name', '_values')
    
    # Constructor
    def __init__(self, lpe_index: int, param_name: str, values: List[float]):
        """Class for parameter definition for parameter runs / scans

        Args:
            lpe_index (int): index of the LightPathElement
            param_name (str): name (ID) of the parameter
            values (List[float]): values to vary in parameter run / scan
        """
        self.lpe_index = lpe_index
        self.param_name = param_name
        self.values = values
        
    # index getter
    @property
    def lpe_index(self) -> int:
        return self._lpe_index
    
    # index setter
    @lpe_index.setter
    def lpe_index(self, val: int) -> None:
        self._lpe_index = val
    
    # name getter
    @property
    def param_name(self) -> str:
        return self._param_name
    
    # name setter
    @param_name.setter
    def param_name(self, val: str) -> None:
        self._param_name = val
    
    # values getter
    @property
    def values(self) -> List[float]:
        return self._values
    
    # values setter
    @values.setter
    def values(self, val: List[float]) -> None:
        self._values = val 

class DetectorResultCollection():
    
    # Slots
    __slots__ = ('_description', '_data')
    
    # Constructor
    def __init__(self, detector_result_object: DetectorResultObject):
        """Class for storage of detector results

        Args:
            detector_result_object (DetectorResultObject): VL DetectorResultObject
        """
        self._description = detector_result_object.Description
        self._data = [self.__get_data_from_dro(detector_result_object)]
    
    # Overwritten str()-method
    def __str__(self) -> str:
        detector_name = f'{self.description}\n\t'
        subdetectors = '\n\t'.join([' | '.join([str(val) for val in entry]) for entry in self.data])
        return detector_name + subdetectors
    
    # name getter
    @property
    def description(self) -> str:
        return self._description
    
    # results getter
    @property
    def data(self) -> List[PhysicalValueBase]:
        return self._data
    
        
    # Methods
    
    def _add_result(self, detector_result_object: DetectorResultObject) -> None:
        """Method to add a result to the DetectorResultCollection

        Args:
            detector_result_object (DetectorResultObject): the DetectorResultObject
        """
        self._data.append(self.__get_data_from_dro(detector_result_object))

    def __get_data_from_dro(self, detector_result_object: DetectorResultObject) -> list:
        """Support method to get data from DetectorResultObject

        Args:
            detector_result_object (DetectorResultObject): the VL DetectorResultObject

        Raises:
            TypeError: wrong detector
            TypeError: unknown data type

        Returns:
            list: PhysicalValue, PhysicalValueComplex
            or Document
        """
        
        # Check if Detector is correct
        if detector_result_object.Description != self.description:
            raise TypeError(f'"{detector_result_object.Description}" is different from "{self.description}"')
        if isinstance(detector_result_object.Data, IDocument):
            return [Document(detector_result_object.Data)]
        
        return [PhysicalValue(curr_data.Value, PhysicalPropertyPy(int(curr_data.PhysicalProperty)), curr_data.Comment)
            if isinstance(curr_data.Value, float)
                else PhysicalValueComplex(curr_data.Value, PhysicalPropertyPy(curr_data.PhysicalProperty), curr_data.Comment)
                for curr_data in detector_result_object.Data]
            
class OpticalSetup():
    
    # Slots
    __slots__ = ('__path', '__simulation_lpd')
    
    # Constructor
    def __init__(self, path: str):
        """Class holding the optical setup

        Args:
            path (str): path to the optical setup
        """
        path_check(path)
        self.__path = path
        self.__simulation_lpd = Lightpath.Load(self.__path)
    
    # Overwritten str()-method     
    def __str__(self) -> str:
        pass
    
    # Change Parameter
    def change_parameter(self, lpe_index: int, param_name: str, value: float) -> None:
        """Method to change parameters of the optical setup

        Args:
            lpe_index (int): index of the LightPathElement
            param_name (str): name (ID) of the parameter
            value (float): values to be varied
        """
        LightPathExportImportSupport.ChangeParameterFromExtern(self.__simulation_lpd, lpe_index, param_name, float(value))
        
    # Perform
    def perform(self, results: List[DetectorResultCollection] = None) -> List[DetectorResultCollection]:
        lpd_wrapper = LightPathDiagramWrapper(self.__simulation_lpd)
        cts = CancellationTokenSource()
        lpd_wrapper.Perform(cts.Token)
        
        if results is None:
            results = [DetectorResultCollection(result) for result in lpd_wrapper.SimulationResults]
        else:
            for i, result in enumerate(lpd_wrapper.SimulationResults): 
                results[i]._add_result(result)        
        
        return results
        

""" Simulation Methods
"""

def parameter_scan_1D(optical_setup: OpticalSetup, params: ParameterSet) -> List[DetectorResultCollection]:
    """Function for a 1D Parameter Scan

    Args:
        optical_setup (OpticalSetup): the optical setup to scan
        params (ParameterSet): the parameter set (information)

    Returns:
        List[DetectorResultCollection]: detector results
    """
    results = None
    
    for value in params.values:
        optical_setup.change_parameter(params.lpe_index, params.param_name, value)
        results = optical_setup.perform(results)
    
    return results
    

def parameter_scan_2D(optical_setup: OpticalSetup, params_1: ParameterSet, params_2: ParameterSet) -> List[DetectorResultCollection]:
    """Function for a 2D Parameter Scan

    Args:
        optical_setup (OpticalSetup): the optical setup to scan
        params_1 (ParameterSet): the first parameter set (information)
        params_2 (ParameterSet): the second parameter set (information)

    Returns:
        List[DetectorResultCollection]: detector results
    """
    results = None
    
    for value_1 in params_1.values:
        optical_setup.change_parameter(params_1.lpe_index, params_1.param_name, value_1)
        for value_2 in params_2.values:
            optical_setup.change_parameter(params_2.lpe_index, params_2.param_name, value_2)
            results = optical_setup.perform(results)
    
    return results
