#
# 券商客户端自动化测试库
# Copyright (C) 2023 谁的谁（41715399@qq.com） All rights reserved.
#
# 模块功能：各种自定义控件
# 建立日期：2023.07.20
# 联系方式：谁的谁（41715399@qq.com）
#
# 开源软件声明：
# 本软件遵守“MIT License”开源协议开源，仅供学习和参考。您可以自由使用或修改源代码或二进制文件，但必须保留上述版权声明。
# 该软件旨在深度学习和挖掘python pywinauto库的功能和潜力，由于环境的不确定性和该软件的不可靠性，请不要将该软件应用于
# 实盘交易。如您确需量化交易实盘功能，请使用券商提供的量化交易平台，否则由于您使用该软件实盘交易所造成的账户损失或政策风
# 险，开源软件提供者或插件提供者均不承担任何责任。同时，无论是直接的、间接的、偶然的、潜在的因使用该软件所造成的账号安全
# 损失、数据安全损失、账户资产损失或其他任何责任事故，开源软件提供者或插件提供者均不承担任何责任。请不要将该软件应用于商
# 业活动，否则由于把该软件应用于商业活动所造成的一切损失或法律责任，开源软件提供者或插件提供者均不承担任何责任。
#
# 修改日志：
#   2022-07-20  第一次编写
#   2024-08-09  解决有些客户端弹窗控件handle为None时的问题
#

from os import remove
from csv import DictReader
from decimal import Decimal
from tempfile import NamedTemporaryFile
from os.path import exists

from .baseuiawrapper import BaseUIAWrapper, wait_until, Timings
from ..error import RecordNotFoundError, RecordAmbiguousError, ItemKeyError, TimeoutError


class PromptWrapper(BaseUIAWrapper):

    _control_types = ['Prompt']

    def __init__(self, elem):
        super(PromptWrapper, self).__init__(elem)

    def __wait_prompt_close(self):
        try:
            timeout = Timings.window_find_timeout
            wait_until(timeout, Timings.window_find_retry, lambda: bool(self.element_info.process_id), False)
        except TimeoutError:
            # 超时因存在关闭确认框或其他已知的原因
            pass

    @property
    def title(self):
        title_spec = self.child(self._client.PROMPT_TITLE_ID)
        return title_spec.window_text() if title_spec.exists() else ''

    def content(self):
        text_spec = self.child(self._client.PROMPT_CONTENT_ID)
        return text_spec.window_text() if text_spec.exists() else ''

    def ok(self):
        ok_btn = self.child({
            'title_re': self._client.PROMPT_OKBUTTON_TITLE,
            'control_type': 'Button'
        })
        if ok_btn.exists():
            ok_btn.click()
            self.__wait_prompt_close()

    def cancel(self):
        cancel_btn = self.child({
            'title_re': self._client.PROMPT_CANCELBUTTON_TITLE,
            'control_type': 'Button'
        })
        if cancel_btn.exists():
            cancel_btn.click()
            self.__wait_prompt_close()

    def close(self):
        # FIXME 有弹框关闭时会弹出确认对话框
        criterias = list(self._client.PROMPT_CLOSE_BUTTON)
        criterias.extend([
            {'title_re': self._client.PROMPT_CANCELBUTTON_TITLE, 'control_type': 'Button'},
            {'title_re': self._client.PROMPT_OKBUTTON_TITLE, 'control_type': 'Button'}
        ])

        for criteria in criterias:
            cls_btn = self.child(criteria)
            if cls_btn.exists():  # 非捕捉模式
                cls_btn.click()
                self.__wait_prompt_close()
                break


class GridItem(object):
    """表格中的项，非控件"""

    def __init__(self, grid, data):
        self.__grid = grid
        self.__data = data

        config = self.__grid.config
        self.__headHeight = 24 if config('headHeight') is None else config('headHeight')
        self.__lineHeight = 24 if config('lineHeight') is None else config('lineHeight')
        self.__offset = 6 if config('offset') is None else config('offset')

    def __getitem__(self, item):
        try:
            return self.__data[item]
        except KeyError:
            raise ItemKeyError(f'表格中没有<{item}>字段')

    def __iter__(self):
        return iter(self.__data)

    def __len__(self):
        return len(self.__data)

    def __str__(self):
        return str(self.__data)

    def __repr__(self):
        return str(self.__data)

    def __getattribute__(self, attr):
        return object.__getattribute__(self, attr)

    def __getattr__(self, item):
        if item not in ['pop', 'popitem', 'update', 'setdefault', 'clear', 'fromkeys']:
            return getattr(self.__data, item)
        else:
            raise AttributeError(f'GridItem对象没有{item}属性')

    def click(self, x=None, double=False):
        self.__grid.click_input(
            coords=(x, self.__headHeight + int(self.__lineHeight >> 1) + (self.__lineHeight * self.__data['index'])),
            double=double
        )

    def double_click(self):
        self.click(double=True)

    def select(self):
        self.click(x=self.__offset)


class GridWrapper(BaseUIAWrapper):

    _control_types = ['GridCSV']

    def __init__(self, elem):
        super(GridWrapper, self).__init__(elem)

    def __getitem__(self, item):
        return self.__data[item]

    def __getattribute__(self, attr):
        return object.__getattribute__(self, attr)

    def __getattr__(self, item):
        if item in ['count', 'index', 'copy']:
            return getattr(self.__data, item)
        else:
            raise AttributeError(f'GridWrapper对象没有{item}属性')

    def __iter__(self):
        return iter(self.__data)

    def __len__(self):
        return len(self.__data)

    def __repr__(self):
        rtn = '['
        for item in self.__data:
            rtn += '\n\t' + str(item) + ','
        return rtn + '\n]'

    def __str__(self):
        rtn = '['
        for item in self.__data:
            rtn += '\n\t' + str(item) + ','
        return rtn + '\n]'

    def __saveto(self, file):
        # 关闭可能存在的弹窗
        self._prompt.close()
        self.set_focus().type_keys('^s')

        saveto = self._get_control(self.config('saveto'))
        # saveto.child(self.config('savetofile')).set_text(file)
        savetofile = saveto.child(self.config('savetofile'))
        # 将鼠标移动到输入框，否则微软UIA的接口会找不到主窗口，不知何故
        savetofile.click_input()
        savetofile.set_text(file)
        saveto.ok()

    def __save_csv_and_parse(self):
        """使用另存为方式保存数据"""
        with NamedTemporaryFile(mode='w+', prefix='WYH_', suffix='.csv', newline='', delete=True) as f:
            file = f.name

        self.__saveto(file)
        while not exists(file):  # 等待保存完成
            pass

        with open(file, newline='') as csvfile:
            reader = DictReader(csvfile)
            self.__data = [GridItem(self, dict(index=reader.line_num-2, **row)) for row in reader]  # row为何是str？

        if exists(file):
            remove(file)

    def items(self, **kwargs):
        """
        依据给定的条件过滤列表，返回过滤后的列表（行，即GridItem对象）

        kwargs关键字可以是表格标头的任何一个字段，value是一个字符串或由字符串组成的元组，
        即使像成交价格、成交数量等在GridWrapper中仍然以字符串格式保存，这样做的好处是
        便于使用Decimal类进行浮点数运算，而不会因计算机浮点数危机使价格计算错误。

        items()方法是GridWrapper对象的核心方法，使用场景可能如下：

        1、获得全部委托单
        grid.items()
        2、使用一个关键字参数过滤列表
        grid.items(证券名称='农业银行')  # 所有证券名称为‘农业银行’的委托单
        3、使用多个关键字参数过滤列表
        grid.items(证券名称='农业银行', 操作='买入')  # 将农业银行的买入单过滤出来
        4、使用一个关键字参数，多值过滤列表
        grid.items(证券名称=('农业银行', '平安银行'))  # 所有证券名称为‘农业银行’和‘平安银行’的委托单
        grid.items(合同编号=('123456', '654321'))  # 合同编号为‘123456’和‘654321’的委托单
        5、使用多关键字参数，多值过滤列表
        grid.items(证券名称=('农业银行', '平安银行'), 操作='买入')  # 农业银行和平安银行的买入单
        """
        table = self.__data.copy()
        for key, value in kwargs.items():
            values = (str(value),) if isinstance(value, (str, int, float, Decimal)) else value
            table = [row for row in table if row[key] in values]
        return table

    def item(self, **kwargs):
        """依据给定的条件，返回一个匹配的项目"""
        table = self.items(**kwargs)

        if not table:
            raise RecordNotFoundError(kwargs)

        if len(table) > 1:
            exception = RecordAmbiguousError('有{0}条记录, 在此条件下{1}'.format(len(table), str(kwargs),))
            exception.table = table
            raise exception

        return table[0]

    def refresh(self):
        self.type_keys('{F5}')
        # FIXME 等待刷新完成？self是否还有效？
        self.__save_csv_and_parse()
        return self


class JQKGridWrapper(BaseUIAWrapper):
    """为同花顺客户端定制的表格控件，因其保存时会弹出验证码"""

    _control_types = ['JQKGridCSV']

    def __init__(self, elem):
        super(JQKGridWrapper, self).__init__(elem)

    def __getitem__(self, item):
        return self.__data[item]

    def __getattribute__(self, attr):
        return object.__getattribute__(self, attr)

    def __getattr__(self, item):
        if item in ['count', 'index', 'copy']:
            return getattr(self.__data, item)
        else:
            raise AttributeError(f'THSGridWrapper对象没有{item}属性')

    def __iter__(self):
        return iter(self.__data)

    def __len__(self):
        return len(self.__data)

    def __repr__(self):
        rtn = '['
        for item in self.__data:
            rtn += '\n\t' + str(item) + ','
        return rtn + '\n]'

    def __str__(self):
        rtn = '['
        for item in self.__data:
            rtn += '\n\t' + str(item) + ','
        return rtn + '\n]'

    def __saveto(self, file):
        # 关闭可能存在的弹窗
        self._prompt.close()
        self.set_focus().type_keys('^s')

        saveto = self._get_control(self.config('saveto'))

        # 捕捉是否有验证码提示框
        pane = self._prompt.tooltip(text=self.config('verifycode_text'))
        if pane is not None:
            # 由于提示框tooltip的捕捉机制以及弹窗的等待关闭机制，故exists()不需要捕捉模式
            while not saveto.exists():  # 默认非捕捉模式
                pane.child(self.config('verifycode_id')).set_text(
                    # 选择是否主动刷新验证码
                    # pane.child(self.config('verifycodeimage_id').refresh().window_text()
                    pane.child(self.config('verifycodeimage_id')).window_text()
                )
                # 由于点击确认后，验证码提示框并不会主动关闭，故判断“另存为”对话框是否弹出作为验证码是否正确的判断依据
                pane.ok()
            else:
                # 关闭验证码提示框
                pane.cancel()

        savetofile = saveto.child(self.config('savetofile'))
        # 将鼠标移动到输入框，否则微软UIA的接口会找不到主窗口，不知何故
        savetofile.click_input()
        # 这里感觉“卡”一下，是因为虽然“另存为”对话框已弹出，但弹窗的“等待关闭”机制仍在等待验证码提示框关闭，但验证码提示框不关闭
        savetofile.set_text(file)
        # 保存
        saveto.ok()

    def __save_csv_and_parse(self):
        """使用另存为方式保存数据"""
        with NamedTemporaryFile(mode='w+', prefix='WYH_', suffix='.csv', newline='', delete=True) as f:
            file = f.name

        self.__saveto(file)
        while not exists(file):  # 等待保存完成
            pass

        with open(file, newline='') as csvfile:
            reader = DictReader(csvfile, dialect='excel-tab')  # 同花顺表格采用excel-tab变种
            self.__data = [GridItem(self, dict(index=reader.line_num-2, **row)) for row in reader]  # row为何是str？

        if exists(file):
            remove(file)

    def items(self, **kwargs):
        """
        依据给定的条件过滤列表，返回过滤后的列表（行，即GridItem对象）

        kwargs关键字可以是表格标头的任何一个字段，value是一个字符串或由字符串组成的元组，
        即使像成交价格、成交数量等在GridWrapper中仍然以字符串格式保存，这样做的好处是
        便于使用Decimal类进行浮点数运算，而不会因计算机浮点数危机使价格计算错误。

        items()方法是GridWrapper对象的核心方法，使用场景可能如下：

        1、获得全部委托单
        grid.items()
        2、使用一个关键字参数过滤列表
        grid.items(证券名称='农业银行')  # 所有证券名称为‘农业银行’的委托单
        3、使用多个关键字参数过滤列表
        grid.items(证券名称='农业银行', 操作='买入')  # 将农业银行的买入单过滤出来
        4、使用一个关键字参数，多值过滤列表
        grid.items(证券名称=('农业银行', '平安银行'))  # 所有证券名称为‘农业银行’和‘平安银行’的委托单
        grid.items(合同编号=('123456', '654321'))  # 合同编号为‘123456’和‘654321’的委托单
        5、使用多关键字参数，多值过滤列表
        grid.items(证券名称=('农业银行', '平安银行'), 操作='买入')  # 农业银行和平安银行的买入单
        """
        table = self.__data.copy()
        for key, value in kwargs.items():
            values = (str(value),) if isinstance(value, (str, int, float, Decimal)) else value
            table = [row for row in table if row[key] in values]
        return table

    def item(self, **kwargs):
        """依据给定的条件，返回一个匹配的项目"""
        table = self.items(**kwargs)

        if not table:
            raise RecordNotFoundError(kwargs)

        if len(table) > 1:
            exception = RecordAmbiguousError('有{0}条记录, 在此条件下{1}'.format(len(table), str(kwargs),))
            exception.table = table
            raise exception

        return table[0]

    def refresh(self):
        self.type_keys('{F5}')
        # FIXME 等待刷新完成？self是否还有效？
        self.__save_csv_and_parse()
        return self
