from abc import ABC, abstractmethod
from typing import List, Dict, Type, Tuple, Union

class TimeSeries(ABC):
    """
    Base abstract class for all time series with simple sequential tagging
    """
    _time_series = {}  # Class-level dictionary to track all time series

    def __init__(self, series_type: str):
        """
        Initialize a new time series with a sequential tag
        
        Args:
            series_type (str): The type of time series (e.g., 'Constant', 'Linear')
        """
        self.tag = len(TimeSeries._time_series) + 1
        self.series_type = series_type
        
        # Register this time series in the class-level tracking dictionary
        self._time_series[self.tag] = self

    @classmethod
    def get_time_series(cls, tag: int) -> 'TimeSeries':
        """
        :no-index:
        Retrieve a specific time series by its tag.
        
        Args:
            tag (int): The tag of the time series
        
        Returns:
            TimeSeries: The time series with the specified tag
        
        Raises:
            KeyError: If no time series with the given tag exists
        """
        if tag not in cls._time_series:
            raise KeyError(f"No time series found with tag {tag}")
        return cls._time_series[tag]

    @classmethod
    def remove_time_series(cls, tag: int) -> None:
        """
        :no-index:
        Delete a time series by its tag and re-tag all remaining series sequentially.
        
        Args:
            tag (int): The tag of the time series to delete
        """
        if tag in cls._time_series:
            del cls._time_series[tag]
            # Re-tag all remaining time series sequentially
            cls._reassign_tags()

    @classmethod
    def _reassign_tags(cls) -> None:
        """
        Reassign tags to all time series sequentially starting from 1.
        """
        new_time_series = {}
        for idx, series in enumerate(sorted(cls._time_series.values(), key=lambda ts: ts.tag), start=1):
            series.tag = idx
            new_time_series[idx] = series
        cls._time_series = new_time_series

    @classmethod
    def get_all_time_series(cls) -> Dict[int, 'TimeSeries']:
        """
        :no-index:
        Retrieve all created time series.
        
        Returns:
            Dict[int, TimeSeries]: A dictionary of all time series, keyed by their tags
        """
        return cls._time_series.copy()


    @abstractmethod
    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        pass

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            Dict[str, float]: Dictionary of parameter names and explanations
        """
        pass

    @abstractmethod
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        pass

    @abstractmethod
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        pass

    @staticmethod
    @abstractmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a TimeSeries
        """
        pass



class ConstantTimeSeries(TimeSeries):
    """
    TimeSeries object with constant load factor
    """
    def __init__(self, **kwargs):
        """
        Initialize a Constant TimeSeries
        
        Args:
            factor (float): The constant load factor value
            tag (int, optional): Specific tag to use. If None, auto-assigned.
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Constant')
        self.factor = kwargs["factor"]


    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return f"timeSeries Constant {self.tag} -factor {self.factor}"

    
    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [("factor", "The constant load factor value (optional , default: 1.0)")]
    
    @staticmethod
    def validate(**kwargs)-> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Constant TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        factor = kwargs.get("factor", 1.0)
        factor = float(factor)
        # check if factor is a number
        if not isinstance(factor, (int, float)):
            raise ValueError("factor must be a number")
        return {"factor": factor}
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {"factor": self.factor}
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.factor = kwargs["factor"]


class LinearTimeSeries(TimeSeries):
    """
    TimeSeries object with linear load factor
    """
    def __init__(self, **kwargs):
        """
        Initialize a Linear TimeSeries
        
        Args:
            factor (float): The linear load factor scale
            tag (int, optional): Specific tag to use. If None, auto-assigned.
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Linear')
        self.factor = kwargs["factor"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return f"timeSeries Linear {self.tag} -factor {self.factor}"

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [("factor", "The linear load factor scale (optional, default: 1.0)")]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Linear TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        factor = kwargs.get("factor", 1.0)
        factor = float(factor)
        if not isinstance(factor, (int, float)):
            raise ValueError("factor must be a number")
        return {"factor": factor}
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {"factor": self.factor}
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.factor = kwargs["factor"]


class TrigTimeSeries(TimeSeries):
    """
    TimeSeries object with sinusoidal load factor
    """
    def __init__(self, **kwargs):
        """
        Initialize a Trig TimeSeries
        
        Args:
            tStart (float): Start time of time series
            tEnd (float): End time of time series
            period (float): Period of sine wave
            factor (float): Load factor amplitude
            shift (float): Phase shift in radians
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Trig')
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return (f"timeSeries Trig {self.tag} "
                f"{self.tStart} {self.tEnd} {self.period} "
                f"-factor {self.factor} -shift {self.shift}")

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("tStart", "Start time of time series (optional, default: 0.0)"),
            ("tEnd", "End time of time series (optional, default: 1.0)"),
            ("period", "Period of sine wave (optional, default: 1.0)"),
            ("factor", "Load factor amplitude (optional, default: 1.0)"),
            ("shift", "Phase shift in radians (optional, default: 0.0)"),
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Trig TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        tStart = kwargs.get("tStart", 0.0)
        tEnd = kwargs.get("tEnd", 1.0)
        period = kwargs.get("period", 1.0)
        factor = kwargs.get("factor", 1.0)
        shift = kwargs.get("shift", 0.0)

        try:
            tStart = float(tStart)
        except ValueError:
            raise ValueError("tStart must be a number")
        
        try:
            tEnd = float(tEnd)
        except ValueError:
            raise ValueError("tEnd must be a number")
        
        try:
            period = float(period)
        except ValueError:
            raise ValueError("period must be a number")
        
        try:
            factor = float(factor)
        except ValueError:
            raise ValueError("factor must be a number")
        
        try:
            shift = float(shift)
        except ValueError:
            raise ValueError("shift must be a number")
        
        if tStart >= tEnd:
            raise ValueError("tStart must be less than tEnd")
        
        if period <= 0:
            raise ValueError("period must be greater than 0")
        
        return {
            "tStart": tStart,
            "tEnd": tEnd,
            "period": period,
            "factor": factor,
            "shift": shift,
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {
            "tStart": self.tStart,
            "tEnd": self.tEnd,
            "period": self.period,
            "factor": self.factor,
            "shift": self.shift,
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]

class RampTimeSeries(TimeSeries):
    """
    TimeSeries object with ramped load factor from tStart to tEnd
    """
    def __init__(self, **kwargs):
        """
        Initialize a Ramp TimeSeries
        
        Args:
            tStart (float): Start time of ramp
            tRamp (float): Length of time to perform the ramp
            smoothness (float): Smoothness parameter (optional, default: 0.0)
            offset (float): Vertical offset amount (optional, default: 0.0)
            cFactor (float): Load factor scale factor (optional, default: 1.0)
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Ramp')
        self.tStart = kwargs["tStart"]
        self.tRamp = kwargs["tRamp"]
        self.smoothness = kwargs["smoothness"]
        self.offset = kwargs["offset"]
        self.cFactor = kwargs["cFactor"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        cmd = f"timeSeries Ramp {self.tag} {self.tStart} {self.tRamp}"
        cmd += f" -smooth {self.smoothness}"
        cmd += f" -offset {self.offset}"
        cmd += f" -factor {self.cFactor}"
        return cmd

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("tStart", "Start time of ramp (optional, default: 0.0)"),
            ("tRamp", "Length of time to perform the ramp (optional, default: 1.0)"),
            ("smoothness", "Smoothness parameter (optional, default: 0.0)"),
            ("offset", "Vertical offset amount (optional, default: 0.0)"),
            ("cFactor", "Load factor scale factor (optional, default: 1.0)")
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Ramp TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        tStart = kwargs.get("tStart", 0.0)
        tRamp = kwargs.get("tRamp", 1.0)
        smoothness = kwargs.get("smoothness", 0.0)
        offset = kwargs.get("offset", 0.0)
        cFactor = kwargs.get("cFactor", 1.0)

        try :
            tStart = float(tStart)
        except ValueError:
            raise ValueError("tStart must be a number")
        
        try:
            tRamp = float(tRamp)
        except ValueError:
            raise ValueError("tRamp must be a number")
        
        try:
            smoothness = float(smoothness)
        except ValueError:
            raise ValueError("smoothness must be a number")
        
        try:
            offset = float(offset)
        except ValueError:
            raise ValueError("offset must be a number")
        
        try:
            cFactor = float(cFactor)
        except ValueError:
            raise ValueError("cFactor must be a number")
        
        
        if not (0 <= smoothness <= 1):
            raise ValueError("smoothness must be between 0 and 1")
        
        return {
            "tStart": tStart,
            "tRamp": tRamp,
            "smoothness": smoothness,
            "offset": offset,
            "cFactor": cFactor
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {
            "tStart": self.tStart,
            "tRamp": self.tRamp,
            "smoothness": self.smoothness,
            "offset": self.offset,
            "cFactor": self.cFactor
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.tStart = kwargs["tStart"]
        self.tRamp = kwargs["tRamp"]
        self.smoothness = kwargs["smoothness"]
        self.offset = kwargs["offset"]
        self.cFactor = kwargs["cFactor"]



class TriangularTimeSeries(TimeSeries):
    """
    TimeSeries object with triangular load pattern
    """
    def __init__(self, **kwargs):
        """
        Initialize a Triangular TimeSeries
        
        Args:
            tStart (float): Start time of series
            tEnd (float): End time of series
            period (float): Period of triangular wave
            factor (float): Load factor amplitude
            shift (float): Phase shift
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Triangular')
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return (f"timeSeries Triangular {self.tag} "
                f"{self.tStart} {self.tEnd} {self.period} "
                f"-factor {self.factor} -shift {self.shift}")

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("tStart", "Start time of series (optional, default: 0.0)"),
            ("tEnd", "End time of series (optional, default: 1.0)"),
            ("period", "Period of triangular wave (optional, default: 1.0)"),
            ("factor", "Load factor amplitude (optional, default: 1.0)"),
            ("shift", "Phase shift (optional, default: 0.0)")
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Triangular TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        tStart = kwargs.get("tStart", 0.0)
        tEnd = kwargs.get("tEnd", 1.0)
        period = kwargs.get("period", 1.0)
        factor = kwargs.get("factor", 1.0)
        shift = kwargs.get("shift", 0.0)

        count = 0
        try:
            tStart = float(tStart); count += 1
            tEnd = float(tEnd); count += 1
            period = float(period); count += 1
            factor = float(factor); count += 1
            shift = float(shift); count += 1
        except ValueError:
            if count == 0:
                raise ValueError("tStart must be a number")
            elif count == 1:
                raise ValueError("tEnd must be a number")
            elif count == 2:
                raise ValueError("period must be a number")
            elif count == 3:
                raise ValueError("factor must be a number")
            elif count == 4:
                raise ValueError("shift must be a number")
            
        
        if tStart >= tEnd:
            raise ValueError("tStart must be less than tEnd")
        
        if period <= 0:
            raise ValueError("period must be greater than 0")
        
        return {
            "tStart": tStart,
            "tEnd": tEnd,
            "period": period,
            "factor": factor,
            "shift": shift
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {
            "tStart": self.tStart,
            "tEnd": self.tEnd,
            "period": self.period,
            "factor": self.factor,
            "shift": self.shift
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]


class RectangularTimeSeries(TimeSeries):
    """
    TimeSeries object with rectangular (step) load pattern
    """
    def __init__(self, **kwargs):
        """
        Initialize a Rectangular TimeSeries
        
        Args:
            tStart (float): Start time of series
            tEnd (float): End time of series
            factor (float): Load factor amplitude
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Rectangular')
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.factor = kwargs["factor"]
        self.period = kwargs["period"]
        self.shift = kwargs["shift"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return (f"timeSeries Rectangular {self.tag} "
                f"{self.tStart} {self.tEnd}  {self.period} -shift {self.shift} -factor {self.factor}")

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("tStart", "Start time of series (optional, default: 0.0)"),
            ("tEnd", "End time of series (optional, default: 1.0)"),
            ("factor", "Load factor amplitude (optional, default: 1.0)"),
            ("period", "Period of rectangular wave (optional, default: 0.0)"),
            ("shift", "Phase shift (optional, default: 0.0)")
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Rectangular TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        tStart = kwargs.get("tStart", 0.0)
        tEnd = kwargs.get("tEnd", 1.0)
        factor = kwargs.get("factor", 1.0)
        period = kwargs.get("period", 0.0)
        shift = kwargs.get("shift", 0.0)

        try:
            tStart = float(tStart)
        except ValueError:
            raise ValueError("tStart must be a number")
        
        try:
            tEnd = float(tEnd)
        except ValueError:
            raise ValueError("tEnd must be a number")
        
        try:
            factor = float(factor)
        except ValueError:
            raise ValueError("factor must be a number")
        

        try:
            period = float(period)
        except ValueError:
            raise ValueError("period must be a number")
        
        try:
            shift = float(shift)
        except ValueError:
            raise ValueError("shift must be a number")

        
        if tStart >= tEnd:
            raise ValueError("tStart must be less than tEnd")
        
        if period <= 0:
            raise ValueError("period must be greater than 0")
        

        
        return {
            "tStart": tStart,
            "tEnd": tEnd,
            "factor": factor
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {
            "tStart": self.tStart,
            "tEnd": self.tEnd,
            "factor": self.factor,
            "period": self.period,
            "shift": self.shift
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.factor = kwargs["factor"]
        self.period = kwargs["period"]
        self.shift = kwargs["shift"]



class PulseTimeSeries(TimeSeries):
    """
    TimeSeries object with pulse load pattern
    """
    def __init__(self, **kwargs):
        """
        Initialize a Pulse TimeSeries
        
        Args:
            tStart (float): Start time of pulse
            tEnd (float): End time of pulse
            period (float): Period of pulse
            width (float): Width of pulse as a fraction of period
            factor (float): Load factor amplitude
            shift (float): Phase shift
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Pulse')
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.width = kwargs["width"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        return (f"timeSeries Pulse {self.tag} "
                f"{self.tStart} {self.tEnd} {self.period} -width {self.width} "
                f"-factor {self.factor} -shift {self.shift}")

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("tStart", "Start time of pulse (optional, default: 0.0)"),
            ("tEnd", "End time of pulse (optional, default: 1.0)"),
            ("period", "Period of pulse (optional, default: 1.0)"),
            ("width", "Width of pulse as a fraction of period (optional, default: 0.5)"),
            ("factor", "Load factor amplitude (optional, default: 1.0)"),
            ("shift", "Phase shift (optional, default: 0.0)"),
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int]]:
        """
        Validate the input parameters for creating a Pulse TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int]]: Dictionary of parameter names and values
        """
        tStart = kwargs.get("tStart", 0.0)
        tEnd = kwargs.get("tEnd", 1.0)
        period = kwargs.get("period", 1.0)
        width = kwargs.get("width", 0.5)
        factor = kwargs.get("factor", 1.0)
        shift = kwargs.get("shift", 0.0)


        count = 0
        try:
            tStart = float(tStart); count += 1
            tEnd = float(tEnd); count += 1
            period = float(period); count += 1
            width = float(width); count += 1
            factor = float(factor); count += 1
            shift = float(shift); count += 1
        except ValueError:
            if count == 0:
                raise ValueError("tStart must be a number")
            elif count == 1:
                raise ValueError("tEnd must be a number")
            elif count == 2:
                raise ValueError("period must be a number")
            elif count == 3:
                raise ValueError("width must be a number")
            elif count == 4:
                raise ValueError("factor must be a number")
            elif count == 5:
                raise ValueError("shift must be a number")
        
        if tStart >= tEnd:
            raise ValueError("tStart must be less than tEnd")
        
        if period <= 0:
            raise ValueError("period must be greater than 0")
        
        if width <= 0 or width >= 1:
            raise ValueError("width must be between 0 and 1")
        
        return {
            "tStart": tStart,
            "tEnd": tEnd,
            "period": period,
            "width": width,
            "factor": factor,
            "shift": shift,
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list]]:
        """
        Get the parameters defining this time series
        """
        return {
            "tStart": self.tStart,
            "tEnd": self.tEnd,
            "period": self.period,
            "width": self.width,
            "factor": self.factor,
            "shift": self.shift,
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.tStart = kwargs["tStart"]
        self.tEnd = kwargs["tEnd"]
        self.period = kwargs["period"]
        self.width = kwargs["width"]
        self.factor = kwargs["factor"]
        self.shift = kwargs["shift"]


class PathTimeSeries(TimeSeries):
    """
    TimeSeries object that interpolates between defined time and load factor points
    """
    def __init__(self, **kwargs):
        """
        Initialize a Path TimeSeries
        
        Args:
            dt (float): Time increment for path
            values (list): List of force values
            filePath (str): Path to file containing force values
            factor (float): Scale factor for force values
            useLast (bool): Use last force value beyond the last time point if true
            prependZero (bool): Prepend a zero value at the start
            startTime (float): Start time of the time series
            time (list): List of time points
            fileTime (str): Path to file containing time points
        """
        kwargs = self.validate(**kwargs)
        super().__init__('Path')
        self.dt = kwargs.get("dt")
        self.values = kwargs.get("values")
        self.filePath = kwargs.get("filePath")
        self.factor = kwargs["factor"]
        self.useLast = kwargs["useLast"]
        self.prependZero = kwargs["prependZero"]
        self.startTime = kwargs["startTime"]
        self.time = kwargs.get("time")
        self.fileTime = kwargs.get("fileTime")

    def to_tcl(self) -> str:
        """
        Convert the time series to a TCL command string for OpenSees
        
        Returns:
            str: The TCL command string
        """
        cmd = f"timeSeries Path {self.tag}"
        if self.dt is not None:
            cmd += f" -dt {self.dt}"
        if self.filePath:
            cmd += f" -filePath {self.filePath}"
        elif self.values:
            values_str = " ".join(map(str, self.values))
            cmd += f" -values {{{values_str}}}"
        if self.time:
            time_str = " ".join(map(str, self.time))
            cmd += f" -time {{{time_str}}}"
        if self.fileTime:
            cmd += f" -fileTime {self.fileTime}"
        if self.factor != 1.0:
            cmd += f" -factor {self.factor}"
        if self.useLast:
            cmd += " -useLast"
        if self.prependZero:
            cmd += " -prependZero"
        if self.startTime != 0.0:
            cmd += f" -startTime {self.startTime}"
        return cmd

    @staticmethod
    def get_Parameters() -> List[Tuple[str, str]]:
        """
        Get the parameters defining this time series
        
        Returns:
            List[Tuple[str, str]]: List of parameter names and explanations
        """
        return [
            ("dt", "Time increment for path"),
            ("time", "List of time points (optional)"),
            ("fileTime", "Path to file containing time points (optional)"),
            ("values", "List of comma separated force values (optional if using filePath)"),
            ("filePath", "Path to file containing force values (optional)"),
            ("factor", "Scale factor for force values (optional, default: 1.0)"),
            ("useLast", "Use last force value beyond the last time point if true (optional, default: False)"),
            ("prependZero", "Prepend a zero value at the start (optional, default: False)"),
            ("startTime", "Start time of the time series (optional, default: 0.0)"),
        ]
    
    @staticmethod
    def validate(**kwargs) -> Dict[str, Union[str, list, float, int, bool]]:
        """
        Validate the input parameters for creating a Path TimeSeries
        
        Args:
            **kwargs: Parameters for time series initialization
        
        Returns:
            Dict[str, Union[str, list, float, int, bool]]: Dictionary of parameter names and values
        """
        dt = kwargs.get("dt")
        factor = kwargs.get("factor", 1.0)
        useLast = kwargs.get("useLast", False)
        prependZero = kwargs.get("prependZero", False)
        startTime = kwargs.get("startTime", 0.0)
        time = kwargs.get("time")
        fileTime = kwargs.get("fileTime")
        values = kwargs.get("values")
        filePath = kwargs.get("filePath")

        if kwargs.get("values") is not None and kwargs.get("filePath") is None:
            values = kwargs.get("values")
            values = [float(v) for v in values.split(",")]
        elif kwargs.get("filePath") is not None and kwargs.get("values") is None:
            filePath = str(kwargs.get("filePath"))
        elif kwargs.get("values") is None and kwargs.get("filePath") is None:
            raise ValueError("Either values or filePath must be provided")
        else:
            raise ValueError("Only one of values or filePath should be provided")
        

        if time is not None and fileTime is not None and dt is not None:
            raise ValueError("Only one of time, fileTime or dt should be provided")
        elif time is None and fileTime is None and dt is None:
            raise ValueError("One of time, fileTime or dt should be provided")
        elif time is  None and fileTime is None and dt is not None:
            try:
                dt = float(dt)
            except ValueError:
                raise ValueError("dt must be a number")
        elif time is not None and fileTime is None and dt is None:
            try :
                time = [float(t) for t in time.split(",")]
            except ValueError:
                raise ValueError("time must be a list of comma separated numbers")
        elif time is None and fileTime is not None and dt is None:
            fileTime = str(fileTime)
        
        elif time is not None :
            if fileTime is not None or dt is not None:
                raise ValueError("Only one of time, fileTime or dt should be provided")
        elif time is None:
            if fileTime is not None and dt is not None:
                raise ValueError("Only one of time, fileTime or dt should be provided")


        if values and not isinstance(values, list):
            raise ValueError("values must be a list")
        

        try :
            factor = float(factor)
        except ValueError:
            raise ValueError("factor must be a number")
        
        
        if not isinstance(useLast, bool):
            raise ValueError("useLast must be a boolean")
        
        if not isinstance(prependZero, bool):
            raise ValueError("prependZero must be a boolean")
        
        try:
            startTime = float(startTime)
        except ValueError:
            raise ValueError("startTime must be a number")
        
        if time and not isinstance(time, list):
            raise ValueError("time must be a list")
        
        return {
            "dt": dt,
            "values": values,
            "filePath": filePath,
            "factor": factor,
            "useLast": useLast,
            "prependZero": prependZero,
            "startTime": startTime,
            "time": time,
            "fileTime": fileTime
        }
    
    def get_values(self) -> Dict[str, Union[str, int, float, list, bool]]:
        """
        Get the parameters defining this time series
        """
        return {
            "dt": self.dt,
            "values": ",".join(map(str, self.values)) if self.values else None,
            "filePath": self.filePath,
            "factor": self.factor,
            "useLast": self.useLast,
            "prependZero": self.prependZero,
            "startTime": self.startTime,
            "time": ",".join(map(str, self.time)) if self.time else None,
            "fileTime": self.fileTime
        }
    
    def update_values(self, **kwargs) -> None:
        """
        Update the values of the time series
        
        Args:
            **kwargs: Parameters for time series initialization
        """
        kwargs = self.validate(**kwargs)
        self.dt = kwargs["dt"]
        self.values = kwargs["values"]
        self.filePath = kwargs["filePath"]
        self.factor = kwargs["factor"]
        self.useLast = kwargs["useLast"]
        self.prependZero = kwargs["prependZero"]
        self.startTime = kwargs["startTime"]
        self.time = kwargs["time"]
        self.fileTime = kwargs["fileTime"]




class TimeSeriesRegistry:
    """
    A registry to manage time series types and their creation.
    """
    _time_series_types = {
        'constant': ConstantTimeSeries,
        'linear': LinearTimeSeries,
        'trig': TrigTimeSeries,
        'ramp': RampTimeSeries,
        'triangular': TriangularTimeSeries,
        'rectangular': RectangularTimeSeries,
        'pulse': PulseTimeSeries,
        'path': PathTimeSeries,
    }

    @classmethod
    def register_time_series_type(cls, name: str, series_class: Type[TimeSeries]):
        """
        Register a new time series type for easy creation.
        
        Args:
            name (str): The name of the time series type
            series_class (Type[TimeSeries]): The class of the time series
        """
        cls._time_series_types[name.lower()] = series_class

    @classmethod
    def get_time_series_types(cls):
        """
        :no-index:
        Get available time series types.
        
        Returns:
            List[str]: Available time series types
        """
        return list(cls._time_series_types.keys())

    @classmethod
    def create_time_series(cls, series_type: str, **kwargs) -> TimeSeries:
        """
        :no-index:
        Create a new time series of a specific type.
        
        Args:
            series_type (str): Type of time series to create
            **kwargs: Parameters for time series initialization
        
        Returns:
            TimeSeries: A new time series instance
        
        Raises:
            KeyError: If the time series type is not registered
        """
        if series_type.lower() not in cls._time_series_types:
            raise KeyError(f"Time series type {series_type} not registered")
        
        return cls._time_series_types[series_type.lower()](**kwargs)




class TimeSeriesManager:
    """
    Singleton class for managing time series objects in the application.
    
    This manager provides a centralized way to create, retrieve, and manage 
    time series objects. It maintains a singleton instance to ensure consistent 
    access to time series throughout the application.
    """
    _instance = None

    def __new__(cls):
        """
        Create a new instance of TimeSeriesManager if one doesn't exist.
        
        This implements the singleton pattern, ensuring only one instance 
        of TimeSeriesManager exists across the application.
        
        Returns:
            TimeSeriesManager: The singleton instance of TimeSeriesManager
        """
        if cls._instance is None:
            cls._instance = super(TimeSeriesManager, cls).__new__(cls)
        return cls._instance
    
    def __len__(self):
        """
        Get the number of time series objects managed by this instance.
        
        Returns:
            int: The number of time series objects
        """
        return len(TimeSeries._time_series)
    
    def __iter__(self):
        """
        Iterate over the time series objects managed by this instance.
        
        Returns:
            Iterator[TimeSeries]: An iterator over the time series objects
        """
        return iter(TimeSeries._time_series.values())

    def create_time_series(self, series_type: str, **kwargs) -> TimeSeries:
        """
        Create a new time series of the specified type.

        This method delegates to the TimeSeriesRegistry to create a new time
        series object with the provided parameters.

        Args:
            series_type (str): The type of time series to create (e.g., 'constant', 'linear')
            **kwargs: Parameters specific to the time series type initialization

        Returns:
            TimeSeries: A new time series instance

        Raises:
            KeyError: If the requested time series type is not registered
            ValueError: If validation of parameters fails
        """
        return TimeSeriesRegistry.create_time_series(series_type, **kwargs)

    def get_time_series(self, tag: int) -> TimeSeries:
        """
        Retrieve a specific time series by its tag.

        Args:
            tag (int): The unique identifier tag of the time series

        Returns:
            TimeSeries: The time series object with the specified tag

        Raises:
            KeyError: If no time series with the given tag exists
        """
        return TimeSeries.get_time_series(tag)

    def remove_time_series(self, tag: int) -> None:
        """
        Remove a time series by its tag.

        This method removes the time series with the given tag and
        reassigns sequential tags to all remaining time series objects.

        Args:
            tag (int): The tag of the time series to remove
        """
        TimeSeries.remove_time_series(tag)

    def get_all_time_series(self) -> Dict[int, TimeSeries]:
        """
        Retrieve all registered time series objects.

        Returns:
            Dict[int, TimeSeries]: A dictionary of all time series objects, where keys are the tags and values are the TimeSeries objects
        """
        return TimeSeries.get_all_time_series()

    def get_available_types(self) -> List[str]:
        """
        Get a list of all available time series types that can be created.

        Returns:
            List[str]: A list of strings representing available time series types

        Example:
            manager = TimeSeriesManager()
            types = manager.get_available_types()
            print("Available time series types:", types)
        """
        return TimeSeriesRegistry.get_time_series_types()
    
    def clear_all(self):
        """
        Clears all time series from the registry.

        This method clears all registered time series objects, effectively
        resetting the state of the time series management system.

        Example:
            manager = TimeSeriesManager()
            # Create some time series
            # ...
            # Clear all time series when starting a new model
            manager.clear_all()
        """
        TimeSeries._time_series.clear()

