"""
Identifier:     csst-l1/msc/csst_msc_instrument/csst_msc_instrument/config.py
Name:           config.py
Description:    Configuration classes.
Author:         Li Shao (shaoli@nao.cas.cn)
Created:        2023-11-16
Modified-History:
    2023-11-16, Li Shao, created
    2023-11-23, Li Shao, change the way to get CCDArrayConfig
    2023-12-21, Li Shao, add CameraConfig
    2024-02-06, Li Shao, move to csst_msc_instrument
    2024-03-05, Li Shao, update flag and CCDArrayConfig
"""

import numpy as np
from astropy.table import Table

__all__ = ["FlagConfig", "CameraConfig", "CCDArrayConfig",
           "TestCCD", "SimCCD", "GM1201APrototype", "get_array_config"]


class FlagConfig:
    """
    Defines the universal bit flag value.

    It can return the name and the value of each flag definition, giving a safe way to handle flags.

    Attributes
    ----------
    dtype : type
        The data type of the flag.
    data : tuple[list]
        Data including the bit values, names and comments.

    Methods
    -------
    get_value(name)
        Get the flag value.
    get_comment(name)
        Get the comment string of the flag.
    get_name(bit)
        Get the name of the flag of the given bit.
    make_mask(names)
        Make a bitmask from a name list of masked bits.

    Examples
    --------
    flagconf = CCDFlagConfig()
    name = flagconf.get_name(0)  # "no_data"
    value = flagconf('no_data')  # 1
    value = flagconf.get_value('no_data')  # 1
    comment = flagconf.get_comment('dead_pixel') # "permanently damaged pixel"
    mask = flagconf.make_mask('no_data', 'dead_pixel')  # 3 = 0b11
    """

    def __init__(self):
        self._version = 'sim_202312'
        self._comment = ''

        self.dtype = np.uint32

        self.data = ([0, 'no_data', 'no data'],
                     [1, 'dead_pixel', 'permanently damaged pixel'],
                     [2, 'no_gain', 'no gain value is available'],
                     [3, 'poor_gain', 'gain value is unreliable'],
                     [4, 'no_bias', 'no bias value is available'],
                     [5, 'poor_bias', 'bias value is unreliable'],
                     [6, 'noisy_readout', 'readout noise is exceptionally large'],
                     [7, 'no_dark', 'no dark current value is available'],
                     [8, 'poor_dark', 'dark current value is unreliable'],
                     [9, 'hot_pixel', 'pixel with exceptionally large dark current'],
                     [10, 'warm_pixel', 'pixel with moderately large dark current'],
                     [11, 'no_flat', 'no flat value is available'],
                     [12, 'poor_flat', 'flat value is unreliable'],
                     [13, 'dark_pixel', 'pixel with relatively low QE'],
                     [14, 'poor_nonlinearity', 'pixel with large nonlinearity'],
                     [15, 'saturated', 'saturated pixel'],
                     [16, 'saturated_crosstalk', 'pixel affected by crosstalk of saturated pixel'],
                     [17, 'sink_pixel', 'pixel with exceptionaly high trap density'],
                     [18, 'poor_cti_corr', 'pixel affected by poor CTI correction'],
                     [19, 'poor_shutter_shade', 'pixel affected by poor shutter shade correction'],
                     [20, 'cosmic_ray', 'identified cosmic ray'],
                     [21, 'poor_cosmic_ray', 'pixel affected by poor cosmic ray correction'],
                     [22, 'poor_fringe', 'pixel affected by poor fringe correction'],
                     [23, 'ghost', 'optics ghost'],
                     [24, 'satellite_trail', 'trail caused by passby satellite'],
                     [25, 'stray_light', 'other serious stray light feature'],
                     [26, 'open_pixel', 'pixel with poor electron bounding capability'],
                     [27, 'adj_open_pixel', 'neighboring pixel of any open pixel'],
                     [28, 'undefined', 'not defined'],
                     [29, 'phot_detection', 'source detection region (0 order source if on spectroscopic image)'],
                     [30, 'spec_detection', 'spectra detection region'],
                     [31, 'spec_contamination', 'pixel contaminated by other higher/lower order spectra'],
                     )

    def __call__(self,
                 name: str,
                 ) -> np.unsignedinteger:
        return self.get_value(name)

    def get_value(self,
                  name: str,
                  ) -> np.unsignedinteger:
        """
        Return the value of the flag.

        The input flag name should be in lower case. The value will be returned in format of self.dtype.
        If no name is matched, then it will raise an exception.

        Parameters
        ----------
        name : str
            The name of the flag.

        Returns
        -------
        numpy.unsignedinteger
            The value of the flag.
        """

        value = None
        for entry in self.data:
            if entry[1] == name:
                value = self.dtype(2 ** entry[0])
        if value is None:
            raise KeyError('Invalid flag name: {}.'.format(name))

        return value

    def get_comment(self,
                    name: str,
                    ) -> str:
        """
        Return the comment string of the flag.

        The input flag name should be in lower case.
        If no name is matched, then it will raise an exception.

        Parameters
        ----------
        name : str
            The name of the flag.

        Returns
        -------
        str
            The value of the flag.
        """

        comment = None
        for entry in self.data:
            if entry[1] == name:
                comment = entry[2]
        if comment is None:
            raise KeyError('Invalid flag name: {}.'.format(name))

        return comment

    def get_name(self,
                 bit: int,
                 ) -> str | None:
        """
        Return the flag name of the given bit.

        The function searches all defined bit. If the bit is not defined, then None will be returned.

        Parameters
        ----------
        bit : int
            The bit value.

        Returns
        -------
        str or None
            The name of the flag. If the bit is undefined, return None.
        """

        name = None
        for entry in self.data:
            if entry[0] == bit:
                name = entry[1]

        return name

    def print(self) -> None:
        """
        Print the bitmask definition table.
        """

        print('Bitmask definition:')
        for i in range(np.iinfo(self.dtype).bits):
            name = self.get_name(i)
            if name is not None:
                value = self(name)
                comment = self.get_comment(name)
                print("  {:2d}\t{:15d}\t\t{:20s}\t\t{}".format(i, value, name, comment))

    def make_mask(self,
                  *names: str,
                  ) -> np.unsignedinteger:
        """
        Make a bitmask from a name list of masked bits.

        All bits correponding to the given names will be flagged. Used for combination of several flags.

        Parameters
        ----------
        *names : str
            Names of bit masks to be masked. Variable length.

        Returns
        -------
        numpy.unsignedinteger
            The mask (in unsigned integer format).
        """

        outmask = self.dtype(0)
        for name in set(names):
            value = self.get_value(name)
            outmask += value

        return outmask


class CameraConfig:
    """
    Define camera configurations: chip layout, LED, etc.

    It inclues the filter, the pixel array configuration, LED, etc.

    Attributes
    ----------
    chip : astropy.table.Table
        The table containing the chip information.
    led : astropy.table.Table
        The table containing the LED information.
    """

    def __init__(self):
        self._version = 'sim_202312'
        self._comment = ''

        self.chip = Table()
        self.chip['index'] = np.arange(30, dtype=int)
        self.chip['chipid'] = ['01', '02', '03', '04', '05',
                               '06', '07', '08', '09', '10',
                               '11', '12', '13', '14', '15',
                               '16', '17', '18', '19', '20',
                               '21', '22', '23', '24', '25',
                               '26', '27', '28', '29', '30']
        self.chip['row'] = [0, 0, 0, 0, 0,
                            1, 1, 1, 1, 1,
                            2, 2, 2, 2, 2,
                            3, 3, 3, 3, 3,
                            4, 4, 4, 4, 4,
                            5, 5, 5, 5, 5]
        self.chip['column'] = [0, 1, 2, 3, 4,
                               0, 1, 2, 3, 4,
                               0, 1, 2, 3, 4,
                               0, 1, 2, 3, 4,
                               0, 1, 2, 3, 4,
                               0, 1, 2, 3, 4]
        self.chip['filter'] = ['GI', 'GV', 'GU', 'GU', 'GV',
                               'y', 'i', 'g', 'r', 'GI',
                               'z', 'NUV', 'NUV', 'u', 'y',
                               'y', 'u', 'NUV', 'NUV', 'z',
                               'GI', 'r', 'g', 'i', 'y',
                               'GV', 'GU', 'GU', 'GV', 'GI']
        self.chip['chiplab'] = ['GI-1', 'GV-1', 'GU-1', 'GU-2', 'GV-2',
                                'y-1', 'i-1', 'g-1', 'r-1', 'GI-2',
                                'z-1', 'NUV-1', 'NUV-2', 'u-1', 'y-2',
                                'y-3', 'u-2', 'NUV-3', 'NUV-4', 'z-2',
                                'GI-3', 'r-2', 'g-2', 'i-2', 'y-4',
                                'GV-3', 'GU-3', 'GU-4', 'GV-4', 'GI-4']
        self.chip['array_config'] = ['CCDArrayConfig', ] * 30

        self.led = Table()
        self.led['index'] = np.arange(28)
        self.led['dev'] = np.append(['main'] * 14, ['back'] * 14)
        self.led['id'] = [i + 1 for i in range(14)] * 2
        self.led['name'] = ['LED{:02d}'.format(i + 1) for i in range(14)] * 2
        self.led['wave'] = np.array([275, 310, 430, 505, 545, 590, 670,
                                     760, 880, 940, 1050, 1550, 340, 365] * 2).astype(float)
        self.led['filter_phot'] = ['NUV', 'NUV', 'g', 'g', 'g', 'r', 'r',
                                   'i', 'z', 'y', 'J', 'H', 'u', 'u'] * 2
        self.led['filter_spec'] = ['GU', 'GU', 'GV', 'GV', 'GV', 'GV', 'GI',
                                   'GI', 'GI', 'GI', 'J', 'H', 'GU', 'GU'] * 2


class CCDArrayConfig:
    """
    Defines pixel array configuration.

    Basic pixel array configuration class. A parent class for all other array configurations.
    Default values are from e2v CCD290-99.

    Parameters
    ----------
    image_format : str
        Data format:
            "raw": readout oriented raw format (default);
            "cube": data cube, all channels are stacked on z direction;
            "sky": sky oriented mosaic format.
    nx : int, optional
        The number of pixels in each channel in x (serial readout) direction.
    nx_active : int
        The number of active pixels in each channel in x (serial readout) direction.
    nx_prescan_physical : int or list or tuple or numpy.ndarray, optional
        The number of physical prescan pixels in each channel in x (serial readout) direction.
    nx_prescan : int or list or tuple or numpy.ndarray, optional
        The number of prescan pixels in each channel in x (serial readout) direction.
    ny : int, optional
        The number of pixels in each channel in y (parallel readout) direction.
    ny_active : int
        The number of active pixels in each channel in y (serial readout) direction.
    ny_prescan_physical : int or list or tuple or numpy.ndarray, optional
        The number of physical prescan pixels in each channel in y (parallel readout) direction.
    ny_prescan : int or list or tuple or numpy.ndarray, optional
        The number of prescan pixels in each channel in y (parallel readout) direction.
    version : str, optional
        The version tag.
    comment : str, optional
        Comment to this version.

    Attributes
    ----------
    nchan : int
        The number of channels.
    nx_overscan : numpy.ndarray
        A list of numbers of overscan pixels in each channel in x (serial readout) direction.
    ny_overscan : numpy.ndarray
        A list of numbers of overscan pixels in each channel in y (parallel readout) direction.

    Methods
    -------
    has_valid_overscan(minwidth, axis='x')
        To judge if there are enough overscan region for overscan correction.
    get_shape(full=True)
        Return the shape of array (numpy default definition).
    get_boundary(channel, rtype='active', full=True)
        Calculate boundary indices for active, prescan, overscan, channel and physical pixel regions.
    get_cutout(channel, image, rtype='active', flip=True)
        Get image cutout of specified region (active, prescan, overscan or channel).
    set_cutout(channel, image, cutout, rtype='active', flip=True)
        Paste the cutout into specific region of the image.

    Examples
    --------
    conf0 = CCDArrayConfig("raw")
    conf1 = CCDArrayConfig("sky")
    conf0.has_valid_overscan(100, axis='y')  # False
    conf1.get_shape(full=False)  # 1152, 4616
    conf0.get_range(1, rtype='channel')  # 0, 1250, 0, 4700
    conf1.get_range(1, rtype='active')  # 0, 1152, 0, 4616
    cutout = conf0.get_cutout(4, image, rtype='x_overscan')  # serial overscan region: numpy array (4616, 71)
    conf0.set_cutout(4, image2, cutout, rtype='x_overscan')  # paste the cutout back to some other image
    """

    def __init__(self,
                 image_format: str,
                 nx: int = 1250,
                 nx_active: int = 1152,
                 nx_prescan_physical: int | list | tuple | np.ndarray = 27,
                 nx_prescan: int | list | tuple | np.ndarray = 27,
                 ny: int = 4700,
                 ny_active: int = 4616,
                 ny_prescan_physical: int | list | tuple | np.ndarray = 0,
                 ny_prescan: int | list | tuple | np.ndarray = 0,
                 version: str = '',
                 comment: str = '',
                 ):

        self.image_format = image_format
        if self.image_format not in ['raw', 'cube', 'sky']:
            raise ValueError('Invalid image type: "{}". Must be "raw", "cube" or "sky".'.format(self.image_format))
        self.version = version
        self.comment = comment

        # physical configuration
        self.nchan = 16
        self.nx_active = nx_active
        self.ny_active = ny_active

        # prescan
        if type(nx_prescan_physical) is int:
            self.nx_prescan_physical = np.full(self.nchan, nx_prescan_physical)
        else:
            self.nx_prescan_physical = np.asarray(nx_prescan_physical).astype(int)
        if type(ny_prescan_physical) is int:
            self.ny_prescan_physical = np.full(self.nchan, ny_prescan_physical)
        else:
            self.ny_prescan_physical = np.asarray(ny_prescan_physical).astype(int)

        # single channel configuration
        # always follow the readout sequence: prescan -> active -> overscan
        if self.image_format == 'sky':
            self.nx, self.ny = self.nx_active, self.ny_active  # no prescan or overscan
            self.nx_prescan = np.full(self.nchan, 0, dtype=int)
            self.ny_prescan = np.full(self.nchan, 0, dtype=int)
        else:
            self.nx, self.ny = nx, ny  # include prescan and overscan
            if type(nx_prescan) is int:
                self.nx_prescan = np.full(self.nchan, nx_prescan)
            else:
                self.nx_prescan = np.asarray(nx_prescan).astype(int)
            if type(ny_prescan) is int:
                self.ny_prescan = np.full(self.nchan, ny_prescan)
            else:
                self.ny_prescan = np.asarray(ny_prescan).astype(int)
        self.nx_overscan = self.nx - self.nx_prescan - self.nx_active
        self.ny_overscan = self.ny - self.ny_prescan - self.ny_active
        self.shape = (self.ny, self.nx)

        # image configuration
        # xflip and yflip decide if the channel cutout should be flipped when making full image
        if self.image_format == 'raw':
            self.full_shape = (self.ny, self.nx * 16)
            self.col = np.arange(16, dtype=int)
            self.row = np.zeros(16, dtype=int)
            self.xflip = np.zeros(16, dtype=bool)
            self.yflip = np.zeros(16, dtype=bool)
        elif self.image_format == 'cube':
            self.full_shape = (16, self.ny, self.nx)
            self.col = np.zeros(16, dtype=int)
            self.row = np.zeros(16, dtype=int)
            self.xflip = np.zeros(16, dtype=bool)
            self.yflip = np.zeros(16, dtype=bool)
        else:
            self.full_shape = (self.ny * 2, self.nx * 8)
            self.col = np.asarray([0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 0])
            self.row = np.asarray([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])
            self.xflip = np.asarray([0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]).astype(bool)
            self.yflip = np.asarray([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]).astype(bool)

    def print(self):
        """
        Print configuration.
        """
        print('CLASS: {}'.format(self.__class__.__name__))
        for key in self.__dict__:
            if type(self.__dict__[key]) is str:
                print('  {} = "{}"'.format(key, self.__dict__[key]))
            else:
                print('  {} = {}'.format(key, self.__dict__[key]))

    def has_valid_overscan(self,
                           minwidth: int,
                           axis: str = 'x',
                           ) -> bool:
        """
        Judge if there are enough overscan region for overscan correction.

        Judge if there are enough overscan region for overscan correction.
        The function can be used for exception raising when overscan is used.

        Parameters
        ----------
        minwidth : int
            Minimum width of valid overscan region.
        axis : str, optional
            Overscan direction, "x" (default) or "y".

        Returns
        -------
        bool
            If True, the configuration contains valid overscan region.
        """

        if axis == 'x':
            index = self.nx_overscan < minwidth
        else:
            index = self.ny_overscan < minwidth
        if index.sum() > 0:
            flag = False
        else:
            flag = True

        return flag

    def get_shape(self,
                  full: bool = True,
                  ) -> tuple:
        """
        Return the shape of array (numpy default definition).

        This function returns the image shape, for both full image and channel image.

        Parameters
        ----------
        full : bool, optional
            If True, return the shape of full image; otherwise return the shape of single channel.

        Returns
        -------
        tuple
            The shape of image, in format of tuple (ny, nx) or (nz, ny, nx).
        """

        if full:
            return self.full_shape
        else:
            return self.shape

    def get_boundary(self,
                     channel: int,
                     rtype: str = 'active',
                     full: bool = True,
                     ) -> tuple[int, int, int, int]:
        """
        Calculate boundary indices for active, prescan, overscan, channel and physical pixel regions.

        This function can return corner indices for different kind of regions. It could be used for image cutout.
        The indices are in python format (start from 0).

        Parameters
        ----------
        channel : int
            Channel number (start from 1).
        rtype : str, optional
            Region type:
                "active": area exposed to external light (default);
                "channel": the whole channel;
                "x_prescan": serial prescan region (including artificial and physical pixels);
                "y_prescan": parallel prescan region;
                "x_overscan": serial overscan region;
                "y_overscan": parallel overscan region;
                "physical": region with physical pixels, including physical part of prescan and active region;
                "cti": region affected by charge transfer inefficiency, i.e. all pixels except artificial prescan.
        full : bool, optional
            If True (default), return the coordinates on the full image;
            otherwise return the coordinates on single channel cutout.

        Returns
        -------
        x1 : int
            X start coordinate of the desired region (python indexing).
        x2 : int
            X end coordinate of the desired region (python indexing).
        y1 : int
            Y start coordinates of the desired region (python indexing).
        y2 : int
            Y end coordinates of the desired region (python indexing).
        """

        xcoords = [0, self.nx_prescan[channel - 1], self.nx_prescan[channel - 1] + self.nx_active, self.nx]
        ycoords = [0, self.ny_prescan[channel - 1], self.ny_prescan[channel - 1] + self.ny_active, self.ny]

        # one single channel cutout: readout at (0, 0)
        if rtype == 'active':
            x1, x2 = xcoords[1], xcoords[2]
            y1, y2 = ycoords[1], ycoords[2]
        elif rtype == 'channel':
            x1, x2 = xcoords[0], xcoords[3]
            y1, y2 = ycoords[0], ycoords[3]
        elif rtype in ['x_prescan', 'x_overscan']:
            y1, y2 = ycoords[1], ycoords[2]
            if rtype == 'x_prescan':
                x1, x2 = xcoords[0], xcoords[1]
            else:
                x1, x2 = xcoords[2], xcoords[3]
        elif rtype in ['y_prescan', 'y_overscan']:
            x1, x2 = xcoords[1], xcoords[2]
            if rtype == 'y_prescan':
                y1, y2 = ycoords[0], ycoords[1]
            else:
                y1, y2 = ycoords[2], ycoords[3]
        elif rtype == 'physical':
            x1, x2 = xcoords[1] - self.nx_prescan_physical[channel - 1], xcoords[2]
            y1, y2 = ycoords[1] - self.ny_prescan_physical[channel - 1], ycoords[2]
        elif rtype == 'cti':
            x1, x2 = xcoords[1] - self.nx_prescan_physical[channel - 1], xcoords[3]
            y1, y2 = ycoords[1] - self.ny_prescan_physical[channel - 1], ycoords[3]
        else:
            raise ValueError('Invalid region type. Please use one of them: ' +
                             '"active", "x_prescan", "y_prescan", "x_overscan", "y_overscan", ' +
                             '"channel", "physical", "cti"')

        # if on full mosaic image, flip and shift the coordinates
        if full:
            if self.xflip[channel - 1]:
                x1, x2 = self.nx - x2, self.nx - x1
            if self.yflip[channel - 1]:
                y1, y2 = self.ny - y2, self.ny - y1
            x1 = self.col[channel - 1] * self.nx + x1
            x2 = self.col[channel - 1] * self.nx + x2
            y1 = self.row[channel - 1] * self.ny + y1
            y2 = self.row[channel - 1] * self.ny + y2

        return x1, x2, y1, y2

    def get_cutout(self,
                   channel: int,
                   image: np.ndarray,
                   rtype: str = 'active',
                   flip: bool = True,
                   ) -> np.ndarray:
        """
        Get image cutout of specified region (active, prescan, overscan or channel).

        A 2D image cutout will be extracted by requirement.
        The input image should be either full image or single channel image.

        Parameters
        ----------
        channel : int
            Channel number (start from 1).
        image : numpy.ndarray
            Input image.
        rtype : str, optional
            Region type. See get_boundary() for more details.
        flip : bool, optional
            If True (default), return the flipped image cutout according to xflip/yflip parameter;
            otherwise return the cutout with the same orientation as input image.
            It only works if the input image is full image.

        Returns
        -------
        numpy.ndarray
            2D image cutout of desired region.
        """

        if np.array_equal(image.shape, self.get_shape(full=True)):
            full = True
        elif np.array_equal(image.shape, self.get_shape(full=False)):
            full = False
        else:
            raise ValueError('Invalid input image shape: {}.'.format(image.shape))

        x1, x2, y1, y2 = self.get_boundary(channel, rtype=rtype, full=full)
        if x1 == x2 or y1 == y2:
            raise IndexError('No such region exists. Please check your configuration attributes.')

        if len(image.shape) == 3:
            cutout = image[channel - 1, y1:y2, x1:x2]
        else:
            cutout = image[y1:y2, x1:x2]

        if full and flip:
            if self.xflip[channel - 1]:
                cutout = cutout[:, ::-1]
            if self.yflip[channel - 1]:
                cutout = cutout[::-1, :]

        return cutout

    def set_cutout(self,
                   channel: int,
                   image: np.ndarray,
                   cutout: np.ndarray,
                   rtype: str = 'active',
                   flip: bool = True,
                   ) -> None:
        """
        Paste a 2D image cutout to a specified region of image.

        The specific region of the image will be overwritten by the cutout. Caution should be taken here.

        Parameters
        ----------
        channel : int
            Channel number (start from 1).
        image : numpy.ndarray
            Image to be updated.
        cutout : numpy.ndarray
            Input cutout. It must be a 2D array.
        rtype : str, optional
            Region type. See get_boundary() for more details.
        flip : bool, optional
            If True (default), return the flipped image cutout according to xflip/yflip parameter;
            otherwise return the cutout with the same orientation as input image.
            It only works if the input image is full image.
        """

        if np.array_equal(image.shape, self.get_shape(full=True)):
            full = True
        elif np.array_equal(image.shape, self.get_shape(full=False)):
            full = False
        else:
            raise ValueError('Invalid input image shape: {}.'.format(image.shape))

        x1, x2, y1, y2 = self.get_boundary(channel, rtype=rtype, full=full)
        if x1 == x2 or y1 == y2:
            raise IndexError('No such region exists. Please check your configuration attributes.')
        if not np.array_equal(cutout.shape, (y2 - y1, x2 - x1)):
            raise ValueError('Invalid input cutout shape: {}. It should be {}'.format(cutout.shape, (y2 - y1, x2 - x1)))

        cutout_paste = cutout.copy()
        if full and flip:
            if self.xflip[channel - 1]:
                cutout_paste = cutout_paste[:, ::-1]
            if self.yflip[channel - 1]:
                cutout_paste = cutout_paste[::-1, :]

        if len(image.shape) == 3:
            image[channel - 1, y1:y2, x1:x2] = cutout_paste
        else:
            image[y1:y2, x1:x2] = cutout_paste


class TestCCD(CCDArrayConfig):
    """
    Defines pixel array configuration for testing.

    Pixel array configuration class for testing. A subclass of CCDArrayConfig.

    Parameters
    ----------
    image_format : str
        Data format. See CCDArrayConfig for more details.
    """

    def __init__(self,
                 image_format: str,
                 ):
        super().__init__(image_format,
                         nx=200,
                         ny=300,
                         nx_active=50,
                         ny_active=150,
                         nx_prescan_physical=np.full(4, 27, dtype=int),
                         nx_prescan=np.full(4, 27, dtype=int),
                         ny_prescan_physical=np.full(4, 4, dtype=int),
                         ny_prescan=np.full(4, 4, dtype=int),
                         version='test_202403',
                         comment='for function testing only')
        self.nchan = 4
        if self.image_format == 'raw':
            self.full_shape = (self.ny, self.nx * 4)
            self.col = np.arange(4, dtype=int)
            self.row = np.zeros(4, dtype=int)
            self.xflip = np.zeros(4, dtype=bool)
            self.yflip = np.zeros(4, dtype=bool)
        elif self.image_format == 'cube':
            self.full_shape = (4, self.ny, self.nx)
            self.col = np.zeros(4, dtype=int)
            self.row = np.zeros(4, dtype=int)
            self.xflip = np.zeros(4, dtype=bool)
            self.yflip = np.zeros(4, dtype=bool)
        else:
            self.full_shape = (self.ny * 2, self.nx * 2)
            self.col = np.asarray([0, 1, 1, 0])
            self.row = np.asarray([0, 0, 1, 1])
            self.xflip = np.asarray([0, 1, 1, 0]).astype(bool)
            self.yflip = np.asarray([0, 0, 1, 1]).astype(bool)


class SimCCD(CCDArrayConfig):
    """
    Defines pixel array configuration for CSST simulation.

    Pixel array configuration class for CSST simulation. A subclass of CCDArrayConfig.

    Parameters
    ----------
    image_format : str
        Data format. See CCDArrayConfig for more details.
    """

    def __init__(self,
                 image_format: str,
                 ):
        super().__init__(image_format,
                         version='sim_202312',
                         comment='readout similar to e2v CCD290-99 NAOC laboratory test')


class GM1201APrototype(CCDArrayConfig):
    """
    Defines pixel array configuration for CSST simulation.

    Pixel array configuration class for CSST simulation. A subclass of CCDArrayConfig.

    Parameters
    ----------
    image_format : str
        Data format. See CCDArrayConfig for more details.
    """

    def __init__(self,
                 image_format: str,
                 ):
        x_prescan_physical = np.full(16, 27, dtype=int)
        x_prescan_physical[[0, 7, 8, 15]] = 29
        x_prescan = np.full(16, 29, dtype=int)
        super().__init__(image_format,
                         nx_prescan_physical=x_prescan_physical,
                         nx_prescan=x_prescan,
                         ny_prescan_physical=4,
                         ny_prescan=4,
                         version='gm1201a_202208',
                         comment='GM1201A user manual V2.7 and NAOC laboratory test')


def get_array_config(name: str,
                     ) -> type[CCDArrayConfig]:
    """
    Get the pixel array configuration class by name.

    Return the type of class by giving its class name. Need to be called to make a new instance.

    Parameters
    ----------
    name : str
        Name of the class.

    Returns
    -------
    generic
        Class of the specific name.

    Examples
    --------
    thisclass = get_array_config('CCDArrayConfig')  # CCDArrayConfig
    conf = thisclass()  # CCDArrayConfig()
    """

    if name not in __all__ or name in ['FlagConfig', 'CameraConfig', 'get_array_config']:
        raise ValueError('Invalid class name.')

    return eval(name)
