# coding=utf-8
# ======================================
# File:     space.py
# Author:   Jackie PENG
# Contact:  jackie.pengzhao@gmail.com
# Created:  2020-09-30
# Desc:
#   parameter Space Class and ResultPool
#   Class for strategy optimization.
# ======================================

import numpy as np

from typing import Union, Iterable, Optional
from itertools import islice, product, zip_longest

from qteasy.utilfuncs import (
    input_to_list,
)
from qteasy.parameter import Parameter


class Space:
    """定义一个参数空间，一个参数空间包含一个或多个Axis对象，存储在axes列表中。当Axis对象等同于交易策略的Parameter
    参数的时候，Space对象就可以表示为一个交易策略的参数空间。优化器可以从参数空间中提取出一系列的参数点。

    参数空间对象支持不同类型的参数轴，可以是整数轴、浮点轴、枚举轴以及数组轴。
    参数空间对象提供了一系列的方法用于从空间中提取参数点、以及判断某个参数点是否在空间中等基本操作，除此以外，还提供了
    高级操作例如计算两个点之间的距离、根据速度向量从某个点出发生成一个子空间、或者生成一个点的邻域空间或子空间等操作。

    上面这些操作将被用于策略的优化过程，例如遗传算法、粒子群算法等优化算法中。

    Properties
    ----------
    dim: int
        参数空间的维度，即轴的数量
    axis: list
        参数空间的轴列表，每个轴都是一个Axis对象
    size: int
        参数空间的大小，即参数空间中参数点的数量
    shape: tuple
        参数空间的形状，即每个轴的长度，用于生成网格坐标
    volume: float
        参数空间的体积，即参数空间中所有参数点的坐标的乘积，用于计算参数空间的密度
    count: int
        参数空间中所有参数点的数量，如果存在连续轴，数量为inf

    Methods
    -------
    extract(qty, how='interval'):
        从参数空间中提取参数点，返回一个参数点的迭代器

    Examples
    --------
    >>> space = Space([(1, 5), (3., 10.), (5, 6, 7, 8, 9)])
    <(1, 5),(3.0, 10.0),(5,...,9)>
    >>> space.dim
    3
    >>> space.axis
    [<Int Parameter: (1, 5)>, <Float Parameter: (3.0, 10.0)>, <Enum Parameter: (5, 6, 7, 8, 9)>]
    >>> space.size
    100
    >>> space.volume
    400.0
    >>> space.shape
    (5, 7, 5)
    >>> space.count
    inf
    >>> space.types
    ['int', 'float', 'enum']
    >>> list(space.extract(4, how='interval')[0])
    [(1, 3.0, 5),
     (1, 3.0, 9),
     (1, 7.0, 5),
     (1, 7.0, 9),
     (5, 3.0, 5),
     (5, 3.0, 9),
     (5, 7.0, 5),
     (5, 7.0, 9)]
    >>> list(space.extract(8, how='rand')[0])
    [(1, 7.928856883024961, 9),
     (3, 9.764777688087385, 8),
     (4, 8.445573333306598, 5),
     (1, 7.7484413238800105, 9),
     (1, 8.943201541252197, 9),
     (4, 3.315940540291259, 7),
     (1, 3.3702544943635155, 7),
     (1, 7.595081100116288, 8)]
    """

    def __init__(self,
                 *par_ranges: Union[Iterable, Parameter, list[Parameter]],
                 par_types: Union[list[str], str] = None
                 ):
        """参数空间对象初始化，根据输入的参数生成一个空间

        Parameters
        ----------
        *par_ranges: tuple, list of tuple, Parameter, list of Parameter
            参数空间中包括的参数，可以输入参数对象，参数对象的列表或者参数的范围列表
            参数范围的列表和参数对象不能混合输入。
            如果输入参数对象，则生成一个包含这些参数对象的参数空间，此时不需要输入par_types参数
            如果输入参数范围列表，则根据par_types参数生成不同类型的参数对象，如果不输入par_types参数，
            则根据参数范围自动判断参数类型
        par_types: list of str or str, optional
            生成的空间每个轴的类型，如果不给出types，会根据输入的pars自动判断，如果给出types，会根据types中
            的类型字符串创建不同的轴，types中的类型字符串分别如下：
            'int'/'discr':      生成整数型轴
            'float'/'conti':    生成浮点数值轴
            'enum':             生成枚举轴
            ‘array’:            生成数组轴，数组轴的元素为一个数组

        Returns
        -------
        Space object

        Examples
        --------
        # 直接输入Parameter对象
        >>> par1 = Parameter((1, 5), 'int')
        >>> par2 = Parameter((3., 10.), 'float')
        >>> par3 = Parameter((5, 6, 7, 8, 9), 'enum')
        >>> space = Space(par1, par2, par3)
        <(1, 5),(3.0, 10.0),(5,...,9)>
        # 输入Parameter对象列表
        >>> space = Space((1, 5), (3., 10.), (5, 6, 7, 8, 9))
        <(1, 5),(3.0, 10.0),(5,...,9)>
        # 输入参数范围列表，并指定每个参数的类型
        >>> space = Space((1, 5), (3., 10.), (5, 6, 7, 8, 9), par_types=['float', 'int', 'int'])
        <(1.0, 5.0),(3, 10),(5, 6)>
        """
        self._axis = []
        # 处理输入，如果pars参数为Parameter对象，则直接使用
        if any(isinstance(par, Parameter) for par in par_ranges):
            if not all(isinstance(par, Parameter) for par in par_ranges):
                raise TypeError('All elements in pars should be Parameter objects if pars contains Parameter objects')
            self._axis = list(par_ranges)
            return

        # 处理输入，将输入处理为列表，并补齐与dim不足的部分
        par_ranges = list(par_ranges)
        par_dim = len(par_ranges)
        if par_types is None:
            par_types = []
        if isinstance(par_types, str):
            par_types = [par_types]
        if not isinstance(par_types, list):
            raise TypeError(f'par_types should be a list of strings, got {type(par_types)} instead')
        if not all(isinstance(pt, (str, type(None))) for pt in par_types):
            raise TypeError('All elements in par_types should be strings or None')

        par_types = input_to_list(par_types, par_dim, None)
        # 预处理par_types
        for i in range(len(par_types)):
            if par_types[i] is None:
                continue
            elif par_types[i].lower() in ['int', 'discr']:
                par_types[i] = 'int'
            elif par_types[i].lower() in ['float', 'conti']:
                par_types[i] = 'float'
            elif par_types[i].lower() in ['enum', 'enumerate']:
                par_types[i] = 'enum'
            else:
                continue
                # raise KeyError(f'Invalid parameter type: {par_types[i]}')
        # 逐一生成Axis对象并放入axes列表中
        self._axis = [Parameter(par, par_type=par_type) for par, par_type in zip(par_ranges, par_types)]

    @property
    def dim(self):  # 空间的维度
        """返回参数空间的维度，即轴的数量

        Returns
        -------
        int
            参数空间的维度
        """
        return len(self._axis)

    @property
    def axis(self):
        """返回参数空间的轴列表，每个轴都是一个Axis对象"""
        return self._axis

    @property
    def types(self):
        """List of types of axis of the space"""
        if self.dim > 0:
            types = [ax.par_type for ax in self.axis]
            return types
        else:
            return None

    @property
    def boes(self):
        """List of bounds of axis of the space"""
        if self.dim > 0:
            boes = [ax.par_range for ax in self.axis]
            return boes
        else:
            return None

    @property
    def shape(self):
        """输出空间的维度大小，输出形式为元组，每个元素代表对应维度的元素个数"""
        s = [ax.count for ax in self._axis]
        return tuple(s)

    @property
    def size(self):
        """输出空间的尺度，输出每个维度的跨度之乘积"""
        s = [ax.size for ax in self._axis]
        return tuple(s)

    @property
    def volume(self):
        """输出空间的尺度，输出每个维度的跨度之乘积"""
        s = [ax.size for ax in self._axis]
        # return np.product(s)  # np.product() will be deprecated in the future
        return np.prod(s)

    @property
    def count(self):
        s = [ax.count for ax in self._axis]
        # return np.product(s)  # np.product() will be deprecated in the future
        return np.prod(s)

    def __repr__(self):
        output = []
        for item in self.boes:
            if len(item) <= 3:
                output.append(str(item))
            else:
                output.append(f'({item[0]},...,{item[-1]})')
        return '<' + ','.join(output) + '>'

    def info(self):
        """打印空间的各项信息"""
        if self.dim > 0:
            print(type(self))
            print('dimension:', self.dim)
            print('types:', self.types)
            print('the bounds or enums of space', self.boes)
            print('shape of space:', self.shape)
            print('size of space:', self.size)
        else:
            print('Space is empty!')

    def extract(self, quantities: Union[int, list[int]] = 1, how: str = 'interval'):
        """从空间中提取出一系列的点，并且把所有的点以迭代器对象的形式返回供迭代

        Parameters
        ----------
        quantities: int or list of int, default 1
            从空间中每个轴上需要提取数据的点的数量
        how: str, default 'interval', {'interval', 'intv', 'step', 'rand', 'random'}
            合法参数：
            interval/intv/step,以间隔步长的方式提取坐标，这时候interval_or_qty代表步长
            rand/random, 以随机方式提取坐标，这时候interval_or_qty代表提取数量

        Returns
        -------
        tuple (iter, total)
            iter，迭代器数据，打包好的所有需要被提取的点的集合
            total，int，迭代器输出的点的数量

        Examples
        --------
        >>> space = Space((1, 10), (3, 10), (5, 6, 7, 8, 9))
        >>> space
        <(1, 10),(3, 10),(5,...,9)>
        >>> list(space.extract([2, 2, 2], how='interval')[0])
        [(1, 3.0, 5),
         (1, 3.0, 9),
         (1, 10.0, 5),
         (1, 10.0, 9),
         (10, 3.0, 5),
         (10, 3.0, 9),
         (10, 10.0, 5),
         (10, 10.0, 9)]
        >>> list(space.extract(8, how='rand')[0])
        [(1, 7.928856883024961, 9),
         (3, 9.764777688087385, 8),
         (4, 8.445573333306598, 5),
         (1, 7.7484413238800105, 9),
         (1, 8.943201541252197, 9),
         (4, 3.315940540291259, 7),
         (1, 3.3702544943635155, 7),
         (1, 7.595081100116288, 8)]
        """
        interval_or_qty_list = input_to_list(pars=quantities,
                                             dim=self.dim,
                                             padder=[1])
        if how in ['interval', 'intv', 'step']:
            # interval模式下，根据需要提取的点总数计算每一个轴的步长，为了确保总数正确，采用开方的方式计算每个轴的点数
            interval_or_qty_list = [int(np.ceil(intv ** (1 / self.dim))) for intv in interval_or_qty_list]

        axis_ranges = [ax.gen_values(qty, how) for ax, qty in zip(self.axis, interval_or_qty_list)]
        total = np.array(list(map(len, axis_ranges))).prod()

        if self.types == ['enum'] and isinstance(self.boes[0], tuple):
            # in this case, space is an enum of tuple parameters, no formation of tuple is needed
            return axis_ranges[0], len(axis_ranges[0])
        if how in ['interval', 'intv', 'step']:
            # 当how为interval时，由于每个axis都需要生成点，总的点数可能大于quantities，此时需要用islice切片
            # 同时为了尽量均匀切片，切片的step为total//quantities
            if total <= quantities:
                return product(*axis_ranges), total
            else:
                slice_step = total // quantities
                slice_ubound = slice_step * quantities
                return islice(product(*axis_ranges), 0, slice_ubound, slice_step), quantities  # 使用迭代器工具将所有的坐标乘积打包为点集
        elif how in ['rand', 'random']:
            return zip_longest(*axis_ranges), quantities  # 使用迭代器工具将所有点组合打包为点集
        else:
            raise KeyError(f'Invalid extraction method: {how}\n'
                           f'Valid methods are: "interval" or "rand"')

    def __contains__(self, item: Union[list, tuple, object]):
        """ 判断item是否在Space对象中, 返回True如果item在Space中，否则返回False

        Parameters
        ----------
        item: list or tuple of coordinates or a Space
            要判断的对象，必须是一个坐标点或者一个子空间

        Returns
        -------
        bool

        Examples
        --------
        >>> s = Space(('x', 'y'), (0, 10), par_types=['enum','int'])
        <('x', 'y'),(0, 10)>
        >>> ('x', 6) in s
        True
        >>> ('z', 6) in s
        False
        >>> ('x', 15) in s
        False

        """
        assert isinstance(item, (list, tuple, Space)), \
            f'TypeError, the item must be a point (tuple or list of coordinates) or a subspace, got {type(item)}'
        if isinstance(item, (tuple, list)):  # if item is a point, all parameter should be in its range
            if len(item) != self.dim:
                return False
            return all(cordi in ax for cordi, ax in zip(item, self.axis))
        else:  # if item is a space, check if all boes are within self boes
            if item.dim != self.dim:  # different dimensions
                return False
            if any(item_type != self_type for item_type, self_type in zip (item.types, self.types)):
                # different par types
                return False
            if any(item_type in ['float_array', 'int_array'] for item_type in item.types):
                # check array shapes
                item_array_axis = [item for item, t in zip(item.axis, item.types) if t in ['float_array', 'int_array']]
                self_array_axis = [item for item, t in zip(self.axis, self.types) if t in ['float_array', 'int_array']]
                if any(it.shape != st.shape for it, st in zip(item_array_axis, self_array_axis)):
                    return False
            for it_boe, s_boe, s_type in zip(item.boes, self.boes, self.types):
                if s_type == 'enum':  # in case of enum, check if all items are in self boe
                    if any(it not in s_it for it, s_it in zip(it_boe, s_boe)):
                        return False
                else:  # in other cases just to check both bounds
                    if not s_boe[0] <= it_boe[0] < it_boe[1] <= s_boe[1]:
                        return False
        return True

    def from_point(self, point, distance: Union[int, float, list, tuple], ignore_enums=True):
        """在已知空间中以一个点为中心点生成一个字空间

        Parameters
        ----------
        point: tuple or list of coordinates
            已知参数空间中的一个参数点
        distance: int or float，
            需要生成的新的子空间的数轴半径
        ignore_enums: bool, Default True
            忽略enum型轴，生成的子空间包含枚举型轴的全部元素，False生成的子空间包含enum轴的部分元素

        Returns
        -------
        Space，新的子空间

        Examples
        --------
        >>> p = (1, 2, 3)
        >>> s = Space([(0, 9), (0, 9), (0, 9)])
        >>> s.from_point(p, 1)
        <(0, 2),(1, 4),(2, 5)>

        """
        assert point in self, f'ValueError, point {point} is not in space!'
        assert self.dim > 0, 'original space should not be empty!'
        assert isinstance(distance, (int, float, list, tuple)), \
            f'TypeError, the distance must be a number or a list of numbers, got {type(distance)} instead'
        pars = []
        if isinstance(distance, (list, tuple)):
            assert len(distance) == self.dim, \
                f'ValueError, can not match {len(distance)} distances in {self.dim} dimensions!'
        else:
            distance = [distance] * self.dim
        for coordinate, boe, s_type, dis in zip(point, self.boes, self.types, distance):
            if s_type != 'enum':
                space_lbound = boe[0]
                space_ubound = boe[1]
                lbound = max((coordinate - dis), space_lbound)
                ubound = min((coordinate + dis), space_ubound)
                pars.append((lbound, ubound))
            else:
                if ignore_enums:
                    pars.append(boe)
                else:
                    enum_pos = boe.index(coordinate)
                    lbound = max((enum_pos - dis), 0)
                    ubound = min((enum_pos + dis), len(boe))
                    pars.append(boe[lbound:ubound])
        return Space(*pars, par_types=self.types)

    def _vector_axis_sizes(self) -> list:
        """返回编码向量中每一轴占用的元素个数，用于 point_to_vector / vector_to_point。

        Returns
        -------
        list of int
            标量轴（int/float/enum）为 1，数组轴为 np.prod(axis.shape)。
        """
        sizes = []
        for ax in self.axis:
            if ax.par_type in ['int', 'float', 'enum']:
                sizes.append(1)
            elif ax.par_type in ['int_array', 'float_array']:
                sizes.append(int(np.prod(ax.shape)))
            else:
                sizes.append(1)
        return sizes

    @property
    def vector_axis_sizes(self) -> list:
        """编码向量中每一轴占用的元素个数（只读），供优化算法做按段交叉等操作。"""
        return self._vector_axis_sizes()

    def point_to_vector(self, point: Union[tuple, list]) -> np.ndarray:
        """将参数空间中的点编码为定长数值向量，供 GA/PSO/gradient/bayesian 等算法复用。

        编码规则：int/float 维各 1 个元素；enum 维为枚举下标（0 到 len-1）；
        int_array/float_array 维按 C 顺序展平为一段标量。

        Parameters
        ----------
        point : tuple or list
            空间内的合法点，满足 point in space。

        Returns
        -------
        np.ndarray
            一维浮点数组，长度等于各轴编码长度之和。
        """
        assert point in self, f'ValueError, point {point} is not in space!'
        sizes = self._vector_axis_sizes()
        parts = []
        for d, (coord, ax, sz) in enumerate(zip(point, self.axis, sizes)):
            t = ax.par_type
            if t == 'int':
                parts.append(np.array([float(coord)], dtype=float))
            elif t == 'float':
                parts.append(np.array([float(coord)], dtype=float))
            elif t == 'enum':
                boe = self.boes[d]
                idx = list(boe).index(coord) if isinstance(boe, tuple) else boe.index(coord)
                parts.append(np.array([float(idx)], dtype=float))
            elif t in ['int_array', 'float_array']:
                arr = np.asarray(coord, dtype=float).ravel(order='C')
                parts.append(arr)
            else:
                parts.append(np.array([float(coord)], dtype=float))
        return np.concatenate(parts).astype(float)

    def vector_to_point(self, vec: Union[np.ndarray, list]) -> tuple:
        """将数值向量解码为参数空间内的合法点。

        解码时对 float 维 clip、int 维 round 后 clip、enum 维下标 clip 后取枚举值、
        int_array/float_array 维 reshape 并逐元素 clip（int_array 取整）。

        Parameters
        ----------
        vec : np.ndarray or list
            与 point_to_vector 输出同长的数值向量。

        Returns
        -------
        tuple
            合法参数点，满足 point in space。
        """
        vec = np.asarray(vec, dtype=float).ravel()
        sizes = self._vector_axis_sizes()
        assert vec.size == sum(sizes), (
            f'Vector length {vec.size} does not match space encoding size {sum(sizes)}'
        )
        out = []
        offset = 0
        for d, (ax, sz) in enumerate(zip(self.axis, sizes)):
            t = ax.par_type
            seg = vec[offset:offset + sz]
            offset += sz
            lb, ub = ax.lbound, ax.ubound
            if t == 'int':
                val = int(np.clip(np.round(seg[0]), lb, ub))
                out.append(val)
            elif t == 'float':
                val = float(np.clip(seg[0], lb, ub))
                out.append(val)
            elif t == 'enum':
                boe = self.boes[d]
                enum_list = list(boe) if isinstance(boe, tuple) else boe
                idx = int(np.clip(np.round(seg[0]), 0, len(enum_list) - 1))
                out.append(enum_list[idx])
            elif t == 'int_array':
                a = np.clip(seg, lb, ub).astype(int)
                out.append(a.reshape(ax.shape))
            elif t == 'float_array':
                a = np.clip(seg, lb, ub).astype(float)
                out.append(a.reshape(ax.shape))
            else:
                out.append(float(np.clip(seg[0], lb, ub)))
        return tuple(out)

    def neighbors(self,
                  point: Union[tuple, list],
                  axis_index: int,
                  count: Optional[int] = None,
                  step: Optional[Union[float, int]] = None) -> list:
        """生成仅在第 axis_index 维上与 point 不同的邻域候选点列表，供梯度搜索与贝叶斯候选使用。

        对 float 维：当前值 ± step（或默认步长），clip 到边界；
        对 int 维：当前值 ±1（或 ±step 若给出）；
        对 enum 维：除当前值外的若干枚举值（随机或全部）；
        对 int_array/float_array 维：逐元素 ±step 或整轴随机重采样若干次。

        Parameters
        ----------
        point : tuple or list
            空间内的点。
        axis_index : int
            要扰动的轴索引。
        count : int, optional
            对 enum/array 维生成的候选个数；对 int/float 若不指定则生成 ±step 两个候选。
        step : float or int, optional
            对 int/float/array 的扰动步长；不指定时 float 用轴 size 的 10%，int 用 1。

        Returns
        -------
        list of tuple
            仅第 axis_index 维与 point 不同的合法点列表（不含 point 自身）。
        """
        assert point in self, f'ValueError, point {point} is not in space!'
        assert 0 <= axis_index < self.dim, f'axis_index {axis_index} out of range [0, {self.dim})'
        ax = self.axis[axis_index]
        t = ax.par_type
        coord = point[axis_index]
        boe = self.boes[axis_index]
        lb, ub = (boe[0], boe[1]) if t != 'enum' else (None, None)
        candidates = []

        if t == 'float':
            step_val = step if step is not None else max(1e-9, (ub - lb) * 0.1)
            for delta in (step_val, -step_val):
                v = np.clip(float(coord) + delta, lb, ub)
                if v != coord:
                    new_point = list(point)
                    new_point[axis_index] = v
                    candidates.append(tuple(new_point))
        elif t == 'int':
            step_val = int(step) if step is not None else 1
            for delta in (step_val, -step_val):
                v = int(np.clip(int(coord) + delta, lb, ub))
                if v != coord:
                    new_point = list(point)
                    new_point[axis_index] = v
                    candidates.append(tuple(new_point))
        elif t == 'enum':
            enum_list = list(boe) if isinstance(boe, tuple) else boe
            others = [x for x in enum_list if x != coord]
            k = count if count is not None else min(2, len(others))
            if k <= 0 or not others:
                return []
            indices = np.random.choice(len(others), size=min(k, len(others)), replace=False)
            for i in indices:
                new_point = list(point)
                new_point[axis_index] = others[i]
                candidates.append(tuple(new_point))
        elif t in ['int_array', 'float_array']:
            arr = np.asarray(coord)
            k = count if count is not None else 2
            gen = list(ax.gen_values(k + 1, 'rand'))  # 多取一个避免全与 coord 相同
            for g in gen:
                new_arr = np.asarray(g)
                if not np.array_equal(new_arr, arr):
                    new_point = list(point)
                    new_point[axis_index] = new_arr
                    cand = tuple(new_point)
                    if cand in self and any(np.array_equal(cand, c) for c in candidates):
                        candidates.append(cand)
                    if len(candidates) >= k:
                        break
        else:
            return []

        return [c for c in candidates if tuple(c) in self]

    def sample_subspace(self,
                        center: Union[tuple, list],
                        radius: Union[int, float, list, tuple],
                        count: int,
                        ignore_enums: bool = True) -> list:
        """在以 center 为中心、半径为 radius 的子空间内随机采样 count 个点。

        封装“邻域子空间 + 随机采样”：对 int/float 维使用 center±radius 裁剪到原边界；
        对 enum 维若 ignore_enums 为 True 则保留全枚举，否则在中心下标邻域内；
        对 array 维使用标量半径：子空间为每元素在 [min(center)-r, max(center)+r] 内裁剪到轴边界。

        Parameters
        ----------
        center : tuple or list
            中心点，必须在空间内。
        radius : int, float, or list/tuple of length dim
            每维的半径（标量或按维）。
        count : int
            采样点数量。
        ignore_enums : bool, default True
            为 True 时 enum 维保持全枚举；为 False 时在中心枚举下标邻域内缩小。

        Returns
        -------
        list of tuple
            长度为 count 的合法点列表。
        """
        assert center in self, f'ValueError, center {center} is not in space!'
        if isinstance(radius, (list, tuple)):
            assert len(radius) == self.dim, f'radius length {len(radius)} != dim {self.dim}'
            dist_list = list(radius)
        else:
            dist_list = [radius] * self.dim

        pars = []
        for d, (coord, boe, s_type, dis) in enumerate(zip(center, self.boes, self.types, dist_list)):
            if s_type == 'enum':
                if ignore_enums:
                    pars.append(boe)
                else:
                    enum_list = list(boe) if isinstance(boe, tuple) else boe
                    pos = enum_list.index(coord)
                    lo = max(0, int(pos - dis))
                    hi = min(len(enum_list), int(pos + dis) + 1)
                    pars.append(tuple(enum_list[lo:hi]))
            elif s_type in ['int_array', 'float_array']:
                coord_arr = np.asarray(coord)
                space_lb, space_ub = boe[0], boe[1]
                lb = max(space_lb, float(np.min(coord_arr) - dis))
                ub = min(space_ub, float(np.max(coord_arr) + dis))
                pars.append((lb, ub))
            else:
                space_lb, space_ub = boe[0], boe[1]
                lb = max(space_lb, coord - dis)
                ub = min(space_ub, coord + dis)
                pars.append((lb, ub))

        # 数组轴需要带 shape 的类型串（如 float_array[2,2]），否则 Parameter 会得到 par_shape=None
        par_types_with_shape = []
        for ax in self.axis:
            if ax.par_type in ['int_array', 'float_array'] and getattr(ax, 'shape', None):
                par_types_with_shape.append(f'{ax.par_type}[{",".join(map(str, ax.shape))}]')
            else:
                par_types_with_shape.append(ax.par_type)
        subspace = Space(*pars, par_types=par_types_with_shape)
        par_iter, total = subspace.extract(count, how='rand')
        return list(par_iter)


class ResultPool:
    """结果池类，用于保存一组元素以及这组元素的评价分数和额外信息。同时提供多种方法对结果池内的元素进行处理
    包括根据评价分数对结果进行排序、裁剪、筛选等等操作。

    Properties
    ----------
    items: list, ReadOnly
        所有池中参数
    perfs: list, ReadOnly
        所有池中参数的评价分
    extra: list, ReadOnly
        所有池中参数的额外信息
    capacity: int
        池中最多可以放入的结果数量
    item_count: int
        池中当前的结果数量
    is_empty: bool
        池是否为空

    Methods
    -------
    in_pool(item, perf, extra=None) Deprecated Warning
        将一个元素压入池中
    push(item, perf, extra=None)
        将一个元素压入池中
    clear()
        清除所有元素
    cut(keep_largest=True)
        修剪池中的元素，使其数量不超过池的容量
    """

    # result pool operation:
    def __init__(self, capacity):
        """ 初始化结果池

        Parameters
        ----------
        capacity: int
            池中最多可以放入的结果数量
        """
        self.__capacity = 0  # 池中最多可以放入的结果数量
        self.__pool = []  # 用于存放被结果所评价的元素，如参数等
        self.__perfs = []  # 用于存放每个中间结果的评价分数，老算法仍然使用列表对象
        self.__extra = []  # 用于存放额外的信息，与每个元素一一相关，在裁剪时会跟随元素被裁剪掉

        self._set_capacity(capacity)

    @property
    def items(self):
        return self.__pool.copy()  # 只读属性，所有池中参数

    @property
    def perfs(self):
        return self.__perfs.copy()  # 只读属性，所有池中参数的评价分

    @property
    def extra(self):
        return self.__extra.copy()  # 只读属性，所有池中参数的额外信息

    @property
    def capacity(self):
        return self.__capacity

    @capacity.setter
    def capacity(self, value):
        self._set_capacity(value)

    @property
    def item_count(self):
        return len(self.items)

    @property
    def is_empty(self):
        return len(self.items) == 0

    def _set_capacity(self, value):
        """ 检查输入数据的合法性，设置结果池的容量"""
        if not isinstance(value, int) or value <= 0:
            raise ValueError(f'Capacity should be a positive integer, got {value} instead')
        self.__capacity = value

    def clear(self):
        """ 清空整个结果池

        :return:
        """
        self.__pool = []
        self.__perfs = []
        self.__extra = []

    def get_item_perfs(self, items: Iterable):
        """获取结果池中元素及其对应评价分的字典表示

        Returns
        -------
        dict
            结果池中元素及其对应评价分的字典表示
        """
        raise NotImplementedError

    def get_item_extra(self, items: Iterable):
        """获取结果池中元素及其对应额外信息的字典表示

        Returns
        -------
        dict
            结果池中元素及其对应额外信息的字典表示
        """
        raise NotImplementedError

    def show_items(self, with_extra=False):
        """以表格的形式显示结果池中的所有元素"""
        print(f'Result Pool (capacity: {self.capacity}, current size: {self.item_count}):')
        # 打印表头
        header = f'{"Index":<10}{"Item":<35}{"Perf":<20}'
        if with_extra:
            header += f'{"Extra":<30}'
        print(header)
        print('-' * 95)
        # 打印每一行
        if with_extra:
            for i, (item, perf, extra) in enumerate(zip(self.__pool, self.__perfs, self.__extra)):
                print(f'{i:<10}{str(item):<35}{perf:<20.3f}{str(extra):<30}')
        else:
            for i, (item, perf) in enumerate(zip(self.__pool, self.__perfs)):
                print(f'{i:<10}{str(item):<35}{perf:<20.3f}')

    def push(self, item, perf, extra=None):
        """将新的结果压入池中

        Parameters
        ----------
        item: object
            需要放入结果池的参数对象
        perf: float
            放入结果池的参数评价分数
        extra: object， optional
            需要放入结果池的参数对象的额外信息

        Returns
        -------
        None
        """
        # perf必须是数值类型
        if not isinstance(perf, (int, float)):
            raise TypeError(f'Performance score must be a number, got {type(perf)} instead')
        self.__pool.append(item)  # 新元素入池
        self.__perfs.append(perf)  # 新元素评价分记录
        self.__extra.append(extra)

    def __add__(self, other):
        """ 将另一个pool中的内容合并到self中

        Parameters
        ----------
        other: ResultPool
            另一个结果池对象

        Returns
        -------
        self
        """
        self.__pool.extend(other.items)
        self.__perfs.extend(other.perfs)
        self.__extra.extend(other.extra)
        return self

    def cut(self, keep_largest=True):
        """将pool内的结果排序并剪切到capacity要求的大小

        直接对self对象进行操作，排序并删除不需要的结果

        Parameters
        ----------
        keep_largest: bool, Default True
            True保留评价分数最高的结果，False保留评价分数最低的结果

        Returns
        -------
        None
        """
        poo = self.__pool  # 所有池中元素
        per = self.__perfs  # 所有池中元素的评价分
        ext = self.__extra  # 池中元素的额外信息
        cap = self.__capacity
        if keep_largest:
            arr = np.array(per).argsort()[-cap:]
        else:
            arr = np.array(per).argsort()[:cap]
        poo2 = [poo[i] for i in arr]
        per2 = [per[i] for i in arr]
        ext2 = [ext[i] for i in arr]
        self.__pool = poo2
        self.__perfs = per2
        self.__extra = ext2


def space_around_centre(space, centre, radius, ignore_enums=True):
    """在给定的参数空间中指定一个参数点，并且创建一个以该点为中心且包含于给定参数空间的子空间

    如果参数空间中包含枚举类型维度，可以予以忽略或其他操作
    """
    return space.from_point(point=centre, distance=radius, ignore_enums=ignore_enums)
