from numbers import Number
from typing import Literal, Union

import numpy as np
from scipy.stats.qmc import LatinHypercube

from vpso.typing import Array1d, Array1i, Array2d, Array3d


def _asarray(val, nvec, dtype, ndim: Literal[1, 3]):
    if isinstance(val, Number):
        val = np.full(nvec, val, dtype=dtype)
    shape = (nvec, 1, 1) if ndim == 3 else (nvec,)
    return np.reshape(val, shape).astype(dtype, copy=False)


def adjust_dimensions(
    lb: Array2d,
    ub: Array2d,
    max_velocity_rate: Union[float, Array1d],
    w: Union[float, Array1d],
    c1: Union[float, Array1d],
    c2: Union[float, Array1d],
    ftol: Union[float, Array1d],
    xtol: Union[float, Array1d],
    patience: Union[int, Array1i],
) -> tuple[
    Array3d,
    Array3d,
    int,
    int,
    Array3d,
    Array3d,
    Array3d,
    Array3d,
    Array1d,
    Array1d,
    Array1i,
]:
    """Adjusts the dimensions of the input arrays to be compatible with the vectorized
    algorithm, i.e., adds dimensions when necessary or converts to array.

    lb : 2d array
        Lower bound of the search space. An array of shape `(N, d)`.
    ub : 2d array
        Upper bound of the search space. An array of shape `(N, d)`.
    max_velocity_rate : float or array, optional
        Maximum velocity rate used to initialize the particles. By default, `0.2`. Can
        also be an 1d array_like of shape `(N,)` to specify a different value for each
        of the `N` vectorized problems.
    w : float, optional
        Inertia weight. By default, `0.9`. Can
        also be an 1d array_like of shape `(N,)` to specify a different value for each
        of the `N` vectorized problems.
    c1 : float, optional
        Cognitive weight. By default, `2.0`. Can also be an 1d array_like of shape
        `(N,)` to specify a different value for each of the `N` vectorized problems.
    c2 : float, optional
        Social weight. By default, `2.0`. Can also be an 1d array_like of shape `(N,)`
        to specify a different value for each of the `N` vectorized problems.
    ftol : float or 1d array_like of floats, optional
        Tolerance for changes in the objective function value before terminating the
        solver. Can also be an 1d array_like of shape `(N,)` to specify a different
        value for each of the `N` vectorized problems. By default, `1e-8`.
    xtol : float or 1d array_like of floats, optional
        Tolerance for average changes in the objective minimizer before terminating the
        solver. Can also be an 1d array_like of shape `(N,)` to specify a different
        value for each of the `N` vectorized problems. By default, `1e-8`.
    patience : int or 1d array_like of ints, optional
        Number of iterations to wait before terminating the solver if no improvement is
        witnessed. Can also be an 1d array_like of shape `(N,)` to specify a different
        value for each of the `N` vectorized problems. By default, `1`.

    Returns
    -------
    tuple of 3d and 1d arrays
        Returns the `lb`, `ub`, `nvec`, `dim`, `max_velocity_rate`, `w`, `c1`, `c2`,
        `ftol`, `xtol`, and `patience`.
    """
    lb = lb[:, np.newaxis].astype(float, copy=False)  # add swarm dimension
    ub = ub[:, np.newaxis].astype(float, copy=False)  # add swarm dimension
    nvec, _, dim = lb.shape
    return (
        lb,
        ub,
        nvec,
        dim,
        _asarray(max_velocity_rate, nvec, np.float64, 3),
        _asarray(w, nvec, np.float64, 3),
        _asarray(c1, nvec, np.float64, 3),
        _asarray(c2, nvec, np.float64, 3),
        _asarray(ftol, nvec, np.float64, 1),
        _asarray(xtol, nvec, np.float64, 1),
        _asarray(patience, nvec, np.int32, 1),
    )


def initialize_particles(
    nvec: int,
    swarmsize: int,
    dim: int,
    lb: Array3d,
    ub: Array3d,
    max_velocity_rate: Array3d,
    lhs_sampler: LatinHypercube,
    np_random: np.random.Generator,
) -> tuple[Array3d, Array3d, Array3d]:
    """Initializes particle positions and velocities.

    Parameters
    ----------
    nvec : int
        Number of vectorized problems.
    swarmsize : int
        Size of the swarm for each problem.
    dim : int
        Dimensionality of the problem.
    lb : 3d array
        Lower bound of the search space. An array of shape `(N, 1, d)`.
    ub : 3d array
        Upper bound of the search space. An array of shape `(N, 1, d)`.
    max_velocity_rate : 3d array
        The maximum velocity rate (proportional to the search domain size) for each
        problem. An array of shape `(N, 1, 1)`.
    seed : int, optional
        Random seed.

    Returns
    -------
    tuple of 3d arrays
        Returns a tuple containing the particle positions, velocities, and maximum
        velocities.
    """
    domain = ub - lb

    x = lb + domain * lhs_sampler.random(swarmsize).reshape(
        swarmsize, nvec, dim
    ).transpose((1, 0, 2))

    v_max = max_velocity_rate * domain
    v = np_random.uniform(0, v_max, (nvec, swarmsize, dim))
    return x, v, v_max
