import copy
import os.path
from typing import Union, List, Optional

from PIL import Image
from UniTAP.utils import function_scheduler
from UniTAP.libs.lib_tsi.tsi import *
from UniTAP.libs.lib_tsi.tsi_io import PortIO
from UniTAP.dev.modules import MemoryManager
from UniTAP.dev.ports.modules.internal_utils.math import aligned
from UniTAP.common import VideoMode, Timing, ColorInfo, VideoFrame, VideoFrameDSC
from UniTAP.dev.ports.modules.internal_utils import uicl_image_from_vm_and_data, uicl_image_convert_to_vm
from .types import VideoPattern, PGVideoMode, PGAdaptiveSyncPatternType, PGColorimetry, \
    PGColorInfo, PGColorDepth, PGStandard, PGDynamicRange, PGAspectRatio, ConstantASParams, SquareASParams, \
    ZigzagASParams, FixedASParams, ASParams
from .private_types import get_as_params_value, PgCaps, CustomTimingFlags, PGPatternID, get_pattern_params_value, \
    PGPatternType
from .pg_utils import pg_timingflags_from_vm, pg_pixel_format_from_vm, pg_colorformat_to_ci_colorformat, \
    pg_dynamicrange_to_ci_dynamicrange, pg_colorimetry_to_ci_colorimetry, pg_bpc_to_ci_bpc
from .timing_manager import TimingManager
from .pg_pattern_params import *
from ..panel_replay import *

# Override MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None


def _read_predefined_timings(io: PortIO) -> List[Timing]:
    count = io.get(TSI_R_PG_PREDEF_TIMING_COUNT)[1]

    predefined_list = []

    for i in range(count):
        io.set(TSI_W_PG_PREDEF_TIMING_SELECT, i)

        timing = Timing()
        timing.htotal = io.get(TSI_PG_CUSTOM_TIMING_HTOTAL)[1]
        timing.vtotal = io.get(TSI_PG_CUSTOM_TIMING_VTOTAL)[1]
        timing.hactive = io.get(TSI_PG_CUSTOM_TIMING_HACTIVE)[1]
        timing.vactive = io.get(TSI_PG_CUSTOM_TIMING_VACTIVE)[1]
        timing.hstart = io.get(TSI_PG_CUSTOM_TIMING_HSTART)[1]
        timing.vstart = io.get(TSI_PG_CUSTOM_TIMING_VSTART)[1]
        timing.hswidth = io.get(TSI_PG_CUSTOM_TIMING_HSYNCW)[1]
        timing.vswidth = io.get(TSI_PG_CUSTOM_TIMING_VSYNCW)[1]
        timing.id = io.get(TSI_PG_CUSTOM_TIMING_ID)[1]
        # TODO: add conversion from pg AR to Timing AR
        timing.aspect_ratio = io.get(TSI_PG_CUSTOM_TIMING_ASPECT_RATIO)[1]
        timing.frame_rate = io.get(TSI_PG_CUSTOM_TIMING_FIELD_RATE)[1]
        timing.standard = Timing.Standard(io.get(TSI_PG_CUSTOM_TIMING_STANDARD)[1])

        timing_flags = io.get(TSI_PG_CUSTOM_TIMING_FLAGS)[1]
        timing.hswidth *= (-1 if timing_flags >> 9 & 1 else 1)
        timing.vswidth *= (-1 if timing_flags >> 10 & 1 else 1)
        timing.reduce_blanking = Timing.ReduceBlanking((timing_flags >> 24) & 0x3)

        predefined_list.append(timing)

    return predefined_list


class PGStatus:
    """

    Class `PGStatus` describes possible states of `PatternGenerator`.

    """

    class PGError(IntEnum):
        """

        Class `PGError` contains codes of errors with the possibility of string representation.

        """
        NotReady = -1
        OK = 0
        HWFault = 1
        PixelClock = 2
        MemoryError = 3
        DscFileZero = 4
        DscPixelClockExceeds = 5
        DscSourceNotSupport = 6
        DscSinkNotSupport = 7
        DscFailReadDpcd = 8
        DscFailWriteDpcd = 9
        WrongColorFormat = 10
        WrongColorimetry = 11
        WrongBitsPerComponent = 12
        WrongDynamicRange = 13
        NotEnoughMemoryForPattern = 14

        def __str__(self):
            if self.value == self.OK:
                return "OK"
            if self.value == self.HWFault:
                return "HW Fault."
            if self.value == self.PixelClock:
                return "Pixel clock exceed HW Caps."
            if self.value == self.MemoryError:
                return "Not enough memory for custom pattern."
            if self.value == self.DscFileZero:
                return "DSC isn’t enabled. DSC file has zero size."
            if self.value == self.DscPixelClockExceeds:
                return "DSC isn’t enabled. Video pixel clock exceeds hardware capabilities."
            if self.value == self.DscSourceNotSupport:
                return "DSC isn’t enabled. Source does not support DSC."
            if self.value == self.DscSinkNotSupport:
                return "DSC isn’t enabled. Sink does not support DSC."
            if self.value == self.DscFailReadDpcd:
                return "DSC isn’t enabled. Fail to read DSC DPCD 0x160."
            if self.value == self.DscFailWriteDpcd:
                return "DSC isn’t enabled. Fail to write DSC DPCD 0x160."
            if self.value == self.WrongColorFormat:
                return "Selected Color Format does not match the pattern."
            if self.value == self.WrongColorimetry:
                return "Selected Colorimetry does not match the pattern."
            if self.value == self.WrongBitsPerComponent:
                return "Selected BPC does not match the port capabilities."
            if self.value == self.WrongDynamicRange:
                    return "Selected Dynamic Range does not match the pattern."
            else:
                return f"Unknown error: {self.value}"

    def __init__(self, value: int):
        self._error = self.PGError(value & 0xFF)
        self._is_video_produced = (value >> 30) & 1 != 0
        self._non_applied_changes = (value >> 31) & 1 != 0

    @property
    def error(self) -> PGError:
        """

        Returns pg error.

        Returns:
            object of `PGError`
        """
        return self._error

    @property
    def is_video_produced(self) -> bool:
        """

        Returns state of video produced.

        Returns:
            object of bool - is video produced or not
        """
        return self._is_video_produced

    @property
    def non_applied_changes(self) -> bool:
        """

        Returns state of applied changes.

        Returns:
            object of bool - were there any changes or not
        """
        return self._non_applied_changes


class PatternGenerator:
    """

    Main class `PatternGenerator` allows working with PG functionality on the device: set different types of pattern
    `set_pattern`, set different video modes `set_vm`, set additional parameters for some patterns `set_pattern_params`,
    get information about current video mode on stream `get_stream_video_mode`, `apply` all transferred setting,
    `reset` settings and read pattern generator `status`.

    """

    __available_patterns = '\n'.join([e.name for e in VideoPattern])
    __hardware_generated_patterns = ''.join([(e.name + '\n') if i <= 7 else '' for i, e in enumerate(VideoPattern)])

    _MAP_PATTERN_PARAMETER = {PGPatternID.SolidColor: SolidColorParams,
                               PGPatternID.WhiteVStrips: WhiteVStripsParams,
                               PGPatternID.GradientRGBStripes: GradientStripsParams,
                               PGPatternID.Motion_Pattern: MotionParams,
                               PGPatternID.SquareWindow: SquareWindowParams}

    def __init__(self, port_io: PortIO, memory_manager: MemoryManager, stream: int):
        self.__io = port_io
        self.__memory_manager = memory_manager
        self.__stream = stream
        self.__caps = self.__read_caps()
        self._pattern = None
        self._pattern_id = PGPatternID.Disabled
        self._pattern_params = None
        self._vm = None
        self.__panel_replay = PanelReplay(port_io, self.__caps)

    def __read_caps(self):
        self._become_active()
        return self.__io.get(TSI_PG_CAPS_R, PgCaps)[1]

    def _caps(self) -> PgCaps:
        return self.__caps

    def __data_from_image_file(self, target_video_mode: VideoMode, path: str) -> bytearray:
        if target_video_mode is None:
            target_video_mode = self.get_stream_video_mode()
        current_video_mode = copy.deepcopy(target_video_mode)
        target_size = (target_video_mode.timing.hactive, target_video_mode.timing.vactive)

        image = Image.open(path)
        image = image.resize(target_size)
        image = image.convert('RGB')
        data = bytearray([x for sets in list(image.getdata()) for x in sets])

        current_video_mode.color_info.color_format = ColorInfo.ColorFormat.CF_RGB
        current_video_mode.color_info.bpc = 8
        current_video_mode.color_info.colorimetry = ColorInfo.Colorimetry.CM_sRGB

        if current_video_mode != target_video_mode:
            image = uicl_image_from_vm_and_data(current_video_mode, data)
            data = uicl_image_convert_to_vm(image, target_video_mode)

        return data

    @staticmethod
    def __data_from_dsc_file(target_video_mode: VideoMode, path: str) -> bytearray:
        with open(path, "rb") as dsc_file:
            data = bytearray(dsc_file.read())
            assert data[:4] == b"DSCF", 'DSC File missing \'DSCF\' header.'

        return data

    def __load_custom_image(self, video_mode: VideoMode, content_data: Union[str, bytearray, VideoFrame]):
        if video_mode is None:
            video_mode = self.get_stream_video_mode()

        if isinstance(content_data, str):
            data = self.__data_from_image_file(video_mode, str(content_data))
        elif isinstance(content_data, VideoFrame):
            data = content_data.data
        else:
            image = uicl_image_from_vm_and_data(video_mode, content_data)
            data = uicl_image_convert_to_vm(image, video_mode)

        block_sizes = PatternGenerator.__calculate_frame_size(video_mode.timing.hactive,
                                                              video_mode.timing.vactive)
        self.__memory_manager.set_memory_layout([block_sizes])
        self.__memory_manager.set_memory_block_index(MemoryManager.MemoryOwner.MO_PatternGenerator)

        self.__io.set(TSI_PG_CUSTOM_PATTERN_MEMORY_BLOCK_INDEX, 0)
        self.__io.set(TSI_PG_CUSTOM_PATTERN_WIDTH, video_mode.timing.hactive)
        self.__io.set(TSI_PG_CUSTOM_PATTERN_HEIGHT, video_mode.timing.vactive)
        self.__io.set(TSI_PG_CUSTOM_PATTERN_PIXEL_FORMAT, pg_pixel_format_from_vm(video_mode))
        self.__io.set(TSI_PG_CUSTOM_PATTERN_DATA, data, c_uint8, data_count=len(data))

    def __load_dsc_image(self, video_mode: VideoMode, content_data: Union[str, bytearray, VideoFrameDSC]):
        if isinstance(content_data, str):
            data = self.__data_from_dsc_file(video_mode, str(content_data))
        elif isinstance(content_data, VideoFrameDSC):
            data = content_data.data
        else:
            data = content_data

        block_sizes = len(data)
        self.__memory_manager.set_memory_layout([block_sizes])
        self.__memory_manager.set_memory_block_index(MemoryManager.MemoryOwner.MO_PatternGenerator)
        self.__io.set(TSI_PG_CUSTOM_PATTERN_MEMORY_BLOCK_INDEX, 0)
        self.__io.set(TSI_PG_CUSTOM_PATTERN_DATA, data, c_uint8, data_count=len(data))

    @staticmethod
    def __calculate_frame_size(width: int, height: int) -> int:
        line_stride_bytes = width * 4
        line_stride_aligned_bytes = aligned(line_stride_bytes, 1024)

        line_stride_bytes2 = width * 2
        line_stride_aligned_bytes2 = aligned(line_stride_bytes2, 1024)

        return int((line_stride_aligned_bytes + line_stride_aligned_bytes2) * height)

    def set_pattern(self, pattern: Union[VideoPattern, str, bytearray, VideoFrame, VideoFrameDSC]):
        """

        Allows setting video pattern on current stream.
        Possible variants:
        - type `VideoPattern` - value from enum `VideoPattern` (one of th e possible predefined patterns).
        - type str - path to image (bmp, png, jpeg, dsc and so on).
        - type bytearray - raw image data, which will be loaded to device memory.
        - type `VideoFrame` - object of class that contains the image data.
        - type `VideoFrameDSC` - object of class that contains dsc image data.

        Args:
            pattern (Union[`VideoPattern`, str, bytearray, `VideoFrame`, `VideoFrameDSC`])

        """
        self._pattern = pattern

    def _setup_pattern(self):
        if self._pattern is not None:
            if isinstance(self._pattern, VideoPattern):
                self._pattern_id = PGPatternID(self._pattern.value)
                if not self.__caps.flags.custom_vp and self._pattern_id in [PGPatternID.WhiteVStrips,
                                                                            PGPatternID.GradientRGBStripes,
                                                                            PGPatternID.ColorRamp,
                                                                            PGPatternID.ColorSquares,
                                                                            PGPatternID.Motion_Pattern]:
                    raise ValueError(f'Current PG (stream {self.__stream}) does not support pattern '
                                     f'{self._pattern.name}. Please select another supported pattern from '
                                     f'"VideoPattern":\n{self.__hardware_generated_patterns}')
            elif isinstance(self._pattern, str) and os.path.exists(self._pattern):
                filename, file_extension = os.path.splitext(self._pattern)
                if file_extension.lower() == '.dsc' and self.__support_dsc():
                    self.__load_dsc_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.DscImage
                else:
                    self.__load_custom_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.ImageFile
            elif isinstance(self._pattern, VideoFrame):
                if self._pattern.color_info.color_format == ColorInfo.ColorFormat.CF_DSC and self.__support_dsc():
                    self.__load_dsc_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.DscImage
                else:
                    self.__load_custom_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.ImageFile
            elif isinstance(self._pattern, VideoFrameDSC):
                if self._pattern.data[:4] == b'DSCF' and self.__support_dsc():
                    self.__load_dsc_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.DscImage
            elif isinstance(self._pattern, bytearray):
                if self._pattern[:4] == b'DSCF' and self.__support_dsc():
                    self.__load_dsc_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.DscImage
                else:
                    self.__load_custom_image(self._vm, self._pattern)
                    self._pattern_id = PGPatternID.ImageFile
            else:
                raise TypeError(f"Incorrect input value: {self._pattern}.\n"
                                f"If you want to select pattern, see following list of available patterns:\n"
                                f"{self.__available_patterns}")
            self.__io.set(TSI_PG_PREDEF_PATTERN_SELECT, self._pattern_id.value)
            return True
        return False

    def __support_dsc(self) -> bool:
        if not self.__caps.flags.dsc_supported:
            warnings.warn("Display Stream Compression is not supported.")
            return False
        return True

    def set_vm(self, vm: VideoMode):
        """

        Allows setting `VideoMode` on current stream.

        Args:
            vm (VideoMode)
        """
        self._vm = vm

    def _setup_vm(self):
        if self._vm is None:
            self.__read_pg_settings()
            return False
        if not self._vm.is_valid():
            raise ValueError(f"Incorrect Video Mode")
        if self.__caps.max_h_active >= self._vm.timing.hactive and \
                self.__caps.max_v_active >= self._vm.timing.vactive and \
                self.__caps.max_h_total >= self._vm.timing.htotal and \
                self.__caps.max_v_total >= self._vm.timing.vtotal and \
                self.__caps.max_frame >= int(self._vm.timing.frame_rate / 1000):
            self.__io.set(TSI_PG_CUSTOM_TIMING_FLAGS, pg_timingflags_from_vm(self._vm))
            self.__io.set(TSI_PG_CUSTOM_TIMING_HTOTAL, self._vm.timing.htotal, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_VTOTAL, self._vm.timing.vtotal, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_HACTIVE, self._vm.timing.hactive, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_VACTIVE, self._vm.timing.vactive, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_HSTART, self._vm.timing.hstart, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_VSTART, self._vm.timing.vstart, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_HSYNCW, abs(self._vm.timing.hswidth), c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_VSYNCW, abs(self._vm.timing.vswidth), c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_FIELD_RATE, self._vm.timing.frame_rate, c_uint)
            self.__io.set(TSI_PG_CUSTOM_TIMING_STANDARD, self._vm.timing.standard.value, c_uint)
            return True
        else:
            raise ValueError(f"Incorrect Video Mode (incorrect resolution and(or) frame rate")

    def set_pattern_params(self, pattern_params: PGPatternParams):
        """

        Allows setting additional parameters for some patters on current stream.
        See available `PGPatternParams` types: `SolidColorParams`, `WhiteVStripsParams`, `GradientStripsParams`,
        `MotionParams`,`SquareWindowParams` (see in pg pattern params).

        Args:
            pattern_params (PGPatternParams)
        """
        self._pattern_params = pattern_params

    def _setup_pattern_params(self):
        if self._pattern_params is not None and isinstance(self._pattern, VideoPattern):
            if self._MAP_PATTERN_PARAMETER.get(PGPatternID(self._pattern_id.value)) is not None:
                if isinstance(self._pattern_params,
                              self._MAP_PATTERN_PARAMETER.get(PGPatternID(self._pattern_id.value))):
                    self.__io.set(TSI_PG_PREDEF_PATTERN_PARAMS, get_pattern_params_value(self._pattern_params,
                                                                                         self._vm.color_info.bpc),
                                  data_count=len(get_pattern_params_value(self._pattern_params)))
                    return True
                else:
                    warnings.warn(f"Incorrect pattern name {self._pattern} and "
                                  f"pattern params {type(self._pattern_params)}.\n"
                                  f"For {self._pattern} you need to use "
                                  f"{self._MAP_PATTERN_PARAMETER.get(self._pattern_id)}")
        return False

    def __get_custom_timing_flags_to_video_mode(self) -> (ColorInfo, bool, bool):
        timing_flags = self.__io.get(TSI_PG_CUSTOM_TIMING_FLAGS, CustomTimingFlags)[1]

        vm_color_format = pg_colorformat_to_ci_colorformat(PGColorInfo(timing_flags.color_space))

        vm_colorimetry = pg_colorimetry_to_ci_colorimetry(PGColorInfo(timing_flags.color_space),
                                                          PGColorimetry(timing_flags.colorimetry))

        vm_dynamic_range = pg_dynamicrange_to_ci_dynamicrange(PGDynamicRange(timing_flags.dynamic_range))

        vm_h_sync_polarity = bool(timing_flags.h_sync_polarity)
        vm_v_sync_polarity = bool(timing_flags.v_sync_polarity)

        vm_bpc = pg_bpc_to_ci_bpc(timing_flags.color_depth)

        color_info = ColorInfo()
        color_info.color_format = vm_color_format
        color_info.colorimetry = vm_colorimetry
        color_info.dynamic_range = vm_dynamic_range
        color_info.bpc = vm_bpc

        return color_info, vm_h_sync_polarity, vm_v_sync_polarity

    def get_stream_video_mode(self) -> VideoMode:
        """

        Returns `VideoMode` information about current stream.

        Returns:
            object of VideoMode type
        """
        self.__read_pg_settings()

        video_mode = VideoMode()

        video_mode.timing.hactive = self.__io.get(TSI_PG_CUSTOM_TIMING_HACTIVE, c_uint32)[1]
        video_mode.timing.vactive = self.__io.get(TSI_PG_CUSTOM_TIMING_VACTIVE, c_uint32)[1]
        video_mode.timing.htotal = self.__io.get(TSI_PG_CUSTOM_TIMING_HTOTAL, c_uint32)[1]
        video_mode.timing.vtotal = self.__io.get(TSI_PG_CUSTOM_TIMING_VTOTAL, c_uint32)[1]
        video_mode.timing.hstart = self.__io.get(TSI_PG_CUSTOM_TIMING_HSTART, c_uint32)[1]
        video_mode.timing.vstart = self.__io.get(TSI_PG_CUSTOM_TIMING_VSTART, c_uint32)[1]

        video_mode.color_info, h_sync_polarity, v_sync_polarity = self.__get_custom_timing_flags_to_video_mode()

        video_mode.timing.hswidth = self.__io.get(TSI_PG_CUSTOM_TIMING_HSYNCW, c_uint32)[1] * (
            -1 if h_sync_polarity else 1)
        video_mode.timing.vswidth = self.__io.get(TSI_PG_CUSTOM_TIMING_VSYNCW, c_uint32)[1] * (
            -1 if v_sync_polarity else 1)
        video_mode.timing.frame_rate = self.__io.get(TSI_PG_CUSTOM_TIMING_FIELD_RATE, c_uint32)[1]

        return video_mode

    def _become_active(self):
        self.__io.set(TSI_PG_STREAM_SELECT, self.__stream)

    def __read_pg_settings(self):
        self.__io.set(TSI_PG_COMMAND_W, 2)

    def apply(self) -> bool:
        """

        Apply all settings on current stream.

        Returns:
            object of bool type - settings were set successfully or not
        """
        self.__io.set(TSI_PG_COMMAND_W, 3)

        def is_apply_pg_success(pg: PatternGenerator):
            return pg.status().error == PGStatus.PGError.OK

        return function_scheduler(is_apply_pg_success, self, interval=1, timeout=10)

    def status(self) -> PGStatus:
        """

        Returns `PGStatus` of current stream.

        Returns:
            object of PGStatus type.
        """
        self._become_active()
        return PGStatus(self.__io.get(TSI_PG_STATUS_R, c_uint32)[1])

    def reset(self):
        """

        Reset all setting on current stream.

        """
        self._vm = VideoMode()
        self._pattern = None
        self._pattern_id = PGPatternID.Disabled
        self._pattern_params = None

    def get_pixel_rate(self) -> int:
        """

        Returns current pixel rate.

        Returns:
            object of int type
        """
        vm = self.get_stream_video_mode()
        return vm.timing.htotal * vm.timing.vtotal * int(vm.timing.frame_rate / 1000)

    def panel_replay(self) -> Optional[PanelReplay]:
        """

        Returns object of PanelReplay if device supports this feature.

        Returns:
            object of 'PanelReplay' type or None
        """
        if self.__caps.flags.panel_replay != 0 or self.__caps.flags.psr != 0:
            return self.__panel_replay
        return None


class HdmiPatternGenerator(PatternGenerator):
    """
    Class `HdmiPatternGenerator` inherited from class `PatternGenerator`.
    Allows getting `timing_manager`, `max_stream_count`, `apply` PG settings.
    Also has all the `PatternGenerator` functionality.
    """

    def __init__(self, port_io: PortIO, memory_manager: MemoryManager):
        super().__init__(port_io, memory_manager, 0)
        self.__timing_manager = TimingManager(_read_predefined_timings(port_io))

    @property
    def timing_manager(self) -> TimingManager:
        """

        Should be used for working with available timings on device.

        Returns:
            object of `TimingManager` type.
        """
        return self.__timing_manager

    @property
    def max_stream_count(self) -> int:
        """

        Returns maximum count of available streams.

        Returns:
            object of int type.
        """
        return 1

    def apply(self) -> bool:
        """

        Apply all settings.

        Returns:
            object of bool type - settings were set successfully or not
        """
        self._become_active()
        res = self._setup_vm()
        res += self._setup_pattern()
        res += self._setup_pattern_params()
        if res != 0:
            return super().apply()
        else:
            return False


class DpPatternGenerator(PatternGenerator):
    """
    Class `DpPatternGenerator` inherited from class `PatternGenerator`.
    Allows getting `timing_manager`, `adaptive_sync_status`, `apply` and `reset` PG settings and set additional settings:
    `set_as_config`, `set_scrolling_params`.
    Also has all the `PatternGenerator` functionality.
    """

    def __init__(self, port_io: PortIO, memory_manager: MemoryManager, stream: int):
        super().__init__(port_io, memory_manager, stream)
        self.__scrolling_params = None
        self.__as_config = None
        self.__io = port_io
        self.__timing_manager = TimingManager(_read_predefined_timings(port_io))

    @property
    def timing_manager(self) -> TimingManager:
        """

        Should be used for working with available timings on device.

        Returns:
            object of `TimingManager` type.
        """
        return self.__timing_manager

    def set_as_config(self, as_config: ASParams):
        """

        Allows setting adaptive sync configuration.
        See available `ASParams` types: `ConstantASParams`, `SquareASParams`, `ZigzagASParams`, `FixedASParams`
        (see in types).

        Args:
            as_config (ASParams)
        """
        self.__as_config = as_config

    def set_scrolling_params(self, scrolling_params: PGScrollingParams):
        """

        Allows setting additional configuration for "Scrolling pattern". See available `PGScrollingParams` types:
        `StepsScrollingParams` (see in pg pattern params).

        Args:
            scrolling_params (PGScrollingParams)
        """
        self.__scrolling_params = scrolling_params

    def _setup_as_config(self):
        if self.__as_config is not None:
            if not self._caps().flags.adaptive_sync:
                warnings.warn("AdaptiveSync is not supported")
                self.__as_config = None
                return False

            self.__io.set(TSI_PG_ADAPTIVE_SYNC_CTRL,
                          get_as_params_value(self.__as_config),
                          data_count=len(get_as_params_value(self.__as_config)))
            return True
        return False

    def _setup_scrolling_params(self):
        if self.__scrolling_params is not None:
            if not self._caps().flags.scrolling_pattern:
                warnings.warn("Scrolling params is not supported")
                self.__scrolling_params = None
                return False
            if self._vm.color_info.color_format == ColorInfo.ColorFormat.CF_YCbCr_422 and \
                    self.__scrolling_params.horizontally % 2:
                warnings.warn(f"Wrong horizontally param. Number must be even. Will be used param - 1: "
                              f"{self.__scrolling_params.horizontally - 1}")
                self.__scrolling_params.horizontally -= 1
            elif self._vm.color_info.color_format == ColorInfo.ColorFormat.CF_YCbCr_420 and \
                    self.__scrolling_params.vertically % 2:
                warnings.warn(f"Wrong vertically param. Number must be even. Will be used param - 1: "
                              f"{self.__scrolling_params.vertically - 1}")
                self.__scrolling_params.vertically -= 1

            if not (self._caps().patterns & (1 << self._pattern_id.value)):
                warnings.warn(f"Patterns scrolling feature is not supported on pattern {self._pattern_id.name}")
                return False
            value = 1
            value |= (1 << 31)
            self.__io.set(TSI_PG_CUSTOM_PATTERN_SCROLLING_CONTROL, value, c_uint32)
            self.__io.set(TSI_PG_CUSTOM_PATTERN_SCROLLING_CONFIG, get_pattern_params_value(self.__scrolling_params),
                          data_count=len(get_pattern_params_value(self.__scrolling_params)))
            return True
        return False

    def adaptive_sync_status(self) -> bool:
        """

        Returns work status of adaptive sync.

        Returns:
            object of bool type - adaptive sync enabled or not
        """
        self._become_active()

        res = self.__io.get(TSI_PG_ADAPTIVE_SYNC_STS, c_uint32)

        if res[0] < TSI_SUCCESS:
            return False

        return res[1] & 1 != 0

    def apply(self) -> bool:
        """

        Apply all settings.

        Returns:
            object of bool type - settings were set successfully or not
        """
        self._become_active()
        res = self._setup_vm()
        res += self._setup_pattern()
        res += self._setup_pattern_params()
        res += self._setup_scrolling_params()
        if res != 0:
            res = super().apply()
        else:
            res = False
        self._setup_as_config()
        return res

    def reset(self):
        """

        Reset all setting.

        """
        self.__scrolling_params = None
        self.__as_config = None
        super().reset()


class DpMstPatternGenerator:
    """
    Class `DpMstPatternGenerator` allows working with one of the supported streams on the device (contains list of the
    `DpPatternGenerator` objects). To access the selected stream, use an override of `[ ]`.
    Also, allows working with stream number 0 directly and applying all settings of all streams together.
    """

    def __init__(self, port_io: PortIO, memory_manager: MemoryManager, max_stream_count: int):
        self.__pg_list = []
        self.__io = port_io
        self.__MAX_STREAM_COUNT = max_stream_count
        for i in range(self.max_stream_count):
            self.__pg_list.append(DpPatternGenerator(port_io, memory_manager, i))
        self.__timing_manager = self.__pg_list[0].timing_manager

    def __getitem__(self, index: int):
        """

        Allows selecting one of the supported streams.
        Example:
        `'object of device role'.pg[index]`

        """
        if not (0 <= index < self.max_stream_count):
            raise ValueError(f"Index of PG must be in range [0..{self.max_stream_count}].")

        return self.__pg_list[index]

    @property
    def timing_manager(self) -> TimingManager:
        """

        Should be used for working with available timings on device.

        Returns:
            object of `TimingManager` type.
        """
        return self.__timing_manager

    @property
    def max_stream_count(self) -> int:
        """

        Returns maximum count of available streams.

        Returns:
            object of int type.
        """
        return self.__MAX_STREAM_COUNT

    def set_pattern(self, pattern: Union[VideoPattern, str, bytearray, VideoFrame, VideoFrameDSC]):
        """

        Allows setting video pattern on stream number 0 of pattern generator.
        Possible variants:
        - type `VideoPattern` - value from enum `VideoPattern` (one of th e possible predefined patterns).
        - type str - path to image (bmp, png, jpeg, dsc and so on).
        - type bytearray - raw image data, which will be loaded to device memory.
        - type `VideoFrame` - object of class that contains the image data.
        - type `VideoFrameDSC` - object of class that contains the dsc image data.

        Args:
            pattern (Union[`VideoPattern`, str, bytearray, `VideoFrame`, `VideoFrameDSC`])

        """
        self.__pg_list[0].set_pattern(pattern)

    def set_vm(self, vm: VideoMode):
        """

        Allows setting `VideoMode` on stream number 0 of pattern generator.

        Args:
            vm (VideoMode)
        """
        self.__pg_list[0].set_vm(vm)

    def set_pattern_params(self, pattern_params: PGPatternParams):
        """

        Allows setting additional parameters for some patters on stream number 0 of pattern generator.
        See available `PGPatternParams` types: `SolidColorParams`, `WhiteVStripsParams`, `GradientStripsParams`,
        `MotionParams`,`SquareWindowParams` (see in pg pattern params).

        Args:
            pattern_params (PGPatternParams)
        """
        self.__pg_list[0].set_pattern_params(pattern_params)

    def set_as_config(self, as_config: ASParams):
        """

        Allows setting adaptive sync configuration on stream number 0 of pattern generator.
        See available `ASParams` types: `ConstantASParams`, `SquareASParams`, `ZigzagASParams`, `FixedASParams`.
        (see in types).

        Args:
            as_config (ASParams)
        """
        self.__pg_list[0].set_as_config(as_config)

    def set_scrolling_params(self, scrolling_params: PGScrollingParams):
        """

        Allows setting additional configuration for "Scrolling pattern" on stream number 0 of pattern generator.
        See available `PGScrollingParams` types: `StepsScrollingParams` (see in pg pattern params).

        Args:
            scrolling_params (PGScrollingParams)
        """
        self.__pg_list[0].set_scrolling_params(scrolling_params)

    def get_stream_video_mode(self) -> VideoMode:
        """

        Returns `VideoMode` information about current configuration of PG on stream 0.

        Returns:
            object of VideoMode type
        """
        return self.__pg_list[0].get_stream_video_mode()

    def apply(self) -> bool:
        """

        Apply all setting on stream number 0 of pattern generator.

        Returns:
            object of bool type - settings were set successfully or not
        """
        return self.__pg_list[0].apply()

    def status(self) -> PGStatus:
        """

        Returns `PGStatus` on stream number 0 of pattern generator.

        Returns:
            object of PGStatus type.
        """
        return self.__pg_list[0].status()

    def reset(self):
        """

        Reset all setting on stream number 0 of pattern generator.

        """
        self.__pg_list[0].reset()

    def apply_all(self):
        """

        Apply all setting on all supported streams of pattern generator.

        """
        for i in range(self.max_stream_count):
            self.__pg_list[i].apply()

    def get_pixel_rate(self, stream: int = 0) -> int:
        """

        Returns current pixel rate for selected stream.

        Returns:
            object of int type
        """
        return self.__pg_list[stream].get_pixel_rate()

    @property
    def panel_replay(self) -> PanelReplay:
        """

        Returns object of PanelReplay if device supports this feature (first stream).

        Returns:
            object of 'PanelReplay' type or None
        """
        return self.__pg_list[0].panel_replay()

    def __read_max_stream_count(self) -> int:
        result = self.__io.get(TSI_PG_MAX_STREAM_COUNT_R, c_uint32)
        return result[1]
