import numpy as np

def to_numpy(x):
    """
    Convert input to a numpy array.
    Handles lists, tuples, and potentially pandas/polars Series if passed 
    (though they are usually compatible with np.asanyarray).
    """
    return np.asanyarray(x)

def check_singleton(x, var_name, type_check=None):
    """
    Check if x is a singleton and optionally of a specific type.
    """
    if np.ndim(x) != 0 and np.size(x) != 1:
         raise ValueError(f"`{var_name}` must be a singleton.")
    
    if type_check:
        if not isinstance(x, type_check):
            if type_check is bool and isinstance(x, (bool, np.bool_)):
                return
            if type_check is str and isinstance(x, (str, np.str_)):
                return
            if type_check is int and isinstance(x, (int, np.integer)):
                return
            if type_check is float and isinstance(x, (float, np.floating)):
                return
            raise TypeError(f"`{var_name}` must be of type {type_check.__name__}.")

def sandwich(x, prefix="", suffix=""):
    """
    Add prefix and suffix to strings in x.
    x is expected to be a numpy array of strings.
    """
    if prefix:
        x = np.char.add(prefix, x)
    if suffix:
        x = np.char.add(x, suffix)
        
    return x

def unique_optimization(func):
    """
    Decorator to apply the unique value optimization pattern.
    The wrapped function should accept `unique_values` and `**kwargs`.
    """
    def wrapper(x, **kwargs):
        x_arr = to_numpy(x)
        original_shape = x_arr.shape
        x_flat = x_arr.ravel()
        
        uvals, inverse = np.unique(x_flat, return_inverse=True)
        
        formatted_uvals = func(uvals, **kwargs)
        
        result_flat = formatted_uvals[inverse]
        result = result_flat.reshape(original_shape)
        
        if result.ndim == 0:
            return result.item()
            
        return result
    
    return wrapper
