import numpy as np
import os

class utils:

    @staticmethod
    def smoothstep_up(x, c=1.5):
        return 1 / (1 + np.exp(-c * x))

    @staticmethod
    def smoothstep_down(x, c=1.5):
        return 1 - 1 / (1 + np.exp(-c * x))

    @staticmethod
    def sig(x1, c=100.0):
        """Sigmoidal function for 0 to 1, used to smoothen some hard transitions.

        Args:
            x1: x-position, transition at 0.
            c: size of the transition [100]

        Output: A value between 0 and 1.

        """
        return 1 / (1 + np.exp(-c * x1))

    @staticmethod
    def check_dict_keys_types(d, required):
        """
        Checks that a dictionary `d` contains all required keys
        and that the values have the correct types.

        Parameters
        ----------
        d : dict
            The dictionary to check.
        required : dict
            A dict where each key is the required key, and the value is the expected type.

        Returns
        -------
        bool
            True if all checks pass, False otherwise.
        list
            List of errors found (missing keys or wrong types).
        """
        errors = []
        for key, expected_type in required.items():
            if key not in d:
                errors.append(f"Missing key: '{key}'")
            elif not isinstance(d[key], expected_type):
                errors.append(f"Key '{key}' expected type not {expected_type}, got {type(d[key]).__name__}")

        print(errors)
        return len(errors) == 0, errors
    
    @staticmethod
    def densify_with_endpoint_gaussian(t, total_points=300, sigma=0.6, power=0.1):
        t = np.array(t)
        lengths = np.diff(t)
        # Weighting by 1/(length^power)
        weights = 1.0 / (lengths ** power)
        weights /= weights.sum()

        num_points_per_segment = np.maximum((weights * total_points).astype(int), 1)

        new_times = []
        for i, (n_points, seg_len) in enumerate(zip(num_points_per_segment, lengths)):
            segment_start = t[i]
            segment_end = t[i + 1]

            # Base uniform [0,1] positions for this segment
            x = np.linspace(0, 1, n_points + 2)[1:-1]

            # Gaussian density centered at both ends
            density = np.exp(-((x - 0) ** 2 / (2 * sigma ** 2))) + np.exp(-((x - 1) ** 2 / (2 * sigma ** 2)))
            density /= density.sum()

            # Get cumulative density to redistribute points
            cdf = np.cumsum(density)
            cdf /= cdf[-1]

            # Map normalized cdf to actual time segment
            segment_points = segment_start + cdf * (segment_end - segment_start)
            new_times.append(segment_points)

        all_times = np.concatenate([t, *new_times])
        return np.clip(np.sort(all_times), t[0], t[-1])

    @staticmethod
    def time_event_factory(targets, tol=1e-6):
        def event(t, y):
            return np.min(np.abs(t - targets)) - tol

        event.terminal = True
        event.direction = 1
        return event

    @staticmethod
    def uniquify(path_original):
        # Creates a unique foldername for the simulation
        counter = 1
        path = path_original + " - " + str(counter)

        while os.path.isdir(path):
            counter += 1
            path = path_original + " - " + str(counter)

        return path

    @staticmethod
    def output_format(nr, form='{:.5E}'):
        """
        Format a number using the specified format string with proper error handling.
        
        Args:
            nr: The number to format (int, float, or numeric string)
            form: Format string (default: '{:.5E}' for scientific notation)
            
        Returns:
            str: Formatted number string, or error message if formatting fails
            
        Raises:
            TypeError: If inputs are of wrong type after conversion attempts
            ValueError: If format string is invalid or number cannot be converted
        """
        # Input validation and type conversion
        if nr is None:
            raise ValueError("Number input cannot be None")
            
        # Try to convert nr to a numeric type if it's not already
        try:
            if isinstance(nr, str):
                # Try to convert string to float
                try:
                    nr = float(nr)
                except ValueError:
                    raise ValueError(f"Cannot convert string '{nr}' to a numeric value")
            elif not isinstance(nr, (int, float, complex)):
                # Try to convert other types to float
                try:
                    nr = float(nr)
                except (ValueError, TypeError):
                    raise TypeError(f"Input type {type(nr).__name__} cannot be converted to a numeric value")
        except (ValueError, TypeError) as e:
            raise e
            
        # Validate format string
        if not isinstance(form, str):
            raise TypeError(f"Format string must be a string, got {type(form).__name__}")
            
        if not form:
            raise ValueError("Format string cannot be empty")
            
        # Try to apply the format
        try:
            # Apply the actual format
            result = form.format(nr)
            return result
            
        except ValueError as e:
            raise ValueError(f"Invalid format string '{form}': {str(e)}")
        except TypeError as e:
            raise TypeError(f"Format string '{form}' is incompatible with input type: {str(e)}")
        except Exception as e:
            # Catch any other unexpected formatting errors
            raise RuntimeError(f"Unexpected error during formatting: {str(e)}")
