import numpy as np
import pandas as pd

def make_grid(values, num_bin, values_to_add=None, binning_method="quantile"):
    """
    Generates a grid of values used for binning continuous data. This grid can be created using either
    quantile-based binning or fixed-width binning.

    Args:
        values (array-like): The data values from which the grid is to be created.
        num_bin (int): The number of bins to use for the grid.
        values_to_add (list, optional): Additional specific values to include in the grid.
        binning_method (str, optional): Method of binning ('quantile' or 'fixed'). Defaults to 'quantile'.

    Returns:
        list: A list of grid values that mark the boundaries of bins.

    Note:
        - 'quantile' binning divides the data into bins such that each bin has approximately the same number of values.
        - 'fixed' binning divides the range of data into equally spaced segments based on the minimum and maximum values.
    """
    unique_values = list(set(values))
    unique_values.sort()
    if len(unique_values) <= num_bin:
        return unique_values

    if binning_method == "quantile":
        qs = np.linspace(0, 1, num=num_bin + 1)
        grid_values = set(np.quantile(values, qs, method='inverted_cdf'))
        if values_to_add is not None:
            grid_values = grid_values.union(set(values_to_add))
        grid_values = list(grid_values)
        grid_values.sort()
    elif binning_method == "fixed":
        grid_values = np.linspace(min(values), max(values), num=num_bin + 1)
    return grid_values

def match_grid_value(values, grid, return_index=False, all_inside=False):
    """
    Maps values to the nearest grid points or their indices. This function is useful for discretizing continuous values
    or for aligning values with predefined bins.

    Args:
        values (array-like): Values to be matched to the grid.
        grid (list): A grid of values, such as those generated by `make_grid`.
        return_index (bool, optional): If True, returns the indices in the grid for each value instead of the grid values. Defaults to False.
        all_inside (bool, optional): If True, adjusts the indices to ensure all values are within the grid limits. Defaults to False.

    Returns:
        list: Depending on `return_index`, either a list of grid values matching each input value or a list of indices indicating the position in the grid.
    """
    bin_index = np.searchsorted(grid, values, side='right')
    if all_inside:
        bin_index[bin_index == len(grid)] = len(grid) - 1
    bin_index = [i if i == 0 else i-1 for i in bin_index]
    
    if not return_index:
        values_discrete = [grid[i] for i in bin_index]
        return values_discrete
    else:
        return bin_index
