#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on 2020/9/7
@author:Irony，侯展意
@email:
@file: pythoneditor
@description: Code Editor

输入 ### 开头的行，可以触发运行一个模块的功能。
输入if __name__=='__main__':可以触发运行全部代码的功能。
按照PyMiner控件协议，输入# mkval后面接pmgwidgets控件表达式，可以触发控件编辑按钮。
"""

import ast
import logging
import os
import re
from typing import List, Tuple, Union, Optional
import json

from qtpy.QtCore import QDir, Qt, QTimer
from qtpy.QtGui import QCloseEvent, QKeySequence, QTextDocument
from qtpy.QtWidgets import QMessageBox, QMenu, QAction, QShortcut
from yapf.yapflib import py3compat, yapf_api

# from .baseeditor import PMBaseEditor, PMAPI
from qtpyeditor import PMGPythonEditor, PythonHighlighter

with open(os.path.join(os.path.dirname(__file__), 'flake8_trans.json'), 'rb') as f:  # 打开flake8提示信息的文件翻译。
    flake8_translations = json.load(f)
show_as_errors = {'E999'}
show_as_warnings = set()
show_as_infos = {'F841', 'F401', 'F403', 'F405', 'E303'}  # 分别是变量导入未使用、定义未使用、使用*导入以及无法推断可能由*导入的类型。
logger = logging.getLogger(__name__)
from pmgwidgets import in_unit_test, is_standard_widget_name, PMGPanelDialog, parse_simplified_pmgjson, \
    run_python_file_in_terminal


class PMPythonEditor(PMGPythonEditor):
    """
    自定义编辑器控件
    """

    def __init__(self, parent=None):
        super(PMPythonEditor, self).__init__(parent)  # , comment_string='# ')
        self.browser_id = None
        self._extension_names.append('.py')
        # self._init_editor()
        # self._init_lexer(QsciLexerPython(self.text_edit))
        # self._init_apis()
        # 编辑器主题
        # self.slot_set_theme(self._theme)
        self.extension_lib = None
        self.last_hint = ''

        self.prepare_actions()
        # self.marker_update_timer = QTimer()
        # self.marker_update_timer.start(1000)
        # self.marker_update_timer.timeout.connect(self.set_marker_for_run)

        # self.completer = PMPythonAutocompleter()
        # self.completer.signal_autocomp_parsed.connect(self.set_api)

    def prepare_actions(self):
        self._init_actions()
        self._init_signals()

    def set_lib(self, lib):
        self.extension_lib = lib

    def _init_actions(self) -> None:
        """
        初始化事件

        :return:
        """
        super(PMPythonEditor, self)._init_actions()
        self._action_help = QAction(self.tr('Function Help'), self.text_edit)
        self._shortcut_help = QShortcut(QKeySequence('F1'), self.text_edit)
        self._action_help.setShortcut(QKeySequence('F1'))

        self._action_help_in_console = QAction(self.tr('Help In Console'), self.text_edit)
        self._shortcut_help_in_console = QShortcut(QKeySequence('F2'), self.text_edit)
        self._action_help_in_console.setShortcut(QKeySequence('F2'))

        self._action_goto_definition = QAction(self.tr('Go to Definition'), self.text_edit)
        self._shortcut_goto_definition = QShortcut(QKeySequence('Ctrl+B'), self.text_edit)
        self._action_goto_definition.setShortcut(QKeySequence('Ctrl+B'))

    def _init_signals(self) -> None:
        """
        初始化信号绑定
        无需super，因为基类已经初始化过了。
        :return: None
        """
        super(PMPythonEditor, self)._init_signals()
        self._shortcut_help.activated.connect(self.get_help)
        self._action_help.triggered.connect(self.get_help)

        self._shortcut_help_in_console.activated.connect(self.get_help_in_console)
        self._action_help_in_console.triggered.connect(self.get_help_in_console)

        self._shortcut_goto_definition.activated.connect(self.slot_goto_definition)
        self._action_goto_definition.triggered.connect(self.slot_goto_definition)

    def set_indicators(self, msgs: List[Tuple[int, int, str, str]], clear=True):
        """
        设置 error warning info 指示器

        :param msgs: 消息数组
        :param clear: 是否清空之前的标记
        :type msgs: Options[list, str]
        :type clear: bool
        :return:
        """
        logger.info(msgs)
        if in_unit_test():
            var_names_in_workspace = set()
        else:
            var_names_in_workspace = set(self.extension_lib.Data.get_all_public_variable_names())
        logger.debug(str(msgs))
        # 清除所有效果
        if clear:
            self.text_edit.clear_highlight()
        if not isinstance(msgs, (tuple, list)):
            return
        for msg in msgs:
            from_line, to_col, n_type, desc = msg
            marker_type = self.get_number_and_f_color(n_type)
            # 把检测详情封装
            # e_msg = ''.join(s_msg[3:])
            e_msg = self.get_message(n_type, desc, var_names_in_workspace=var_names_in_workspace)
            if e_msg == '':
                continue
            self.text_edit.register_highlight(from_line - 1, to_col - 1, -1, marker_type, e_msg)

        self.text_edit.rehighlight()

    def set_marker_for_run(self):
        """
        在自身可见的情况下，才执行这一步操作。
        :return:
        """
        return
        if self.isVisible():
            super().set_marker_for_run()
            text_list = self.text().split('\n')
            for line, line_text in enumerate(text_list):
                if line_text.startswith('###'):
                    self.text_edit.markerAdd(line, self.text_edit.MARKER_RUN_CELL)
                vals_is_start = re.match(r'[ \t]*if[ \t]*__name__[ \t]*==[ \t]*[\'\"]__main__[\'\"][\t ]*:', line_text)
                if vals_is_start is not None:
                    self.text_edit.markerAdd(line, self.text_edit.MARKER_RUN_FILE)
                vals = re.findall(r'.*=.*#[ \t]*(?:mkval|mkvar)[ \t]*\[.*\]', line_text)
                if len(vals) != 0:
                    self.text_edit.markerAdd(line, self.text_edit.MARKER_SET_VALUE)
            self.adjust_line_number_area_width()

    def get_message(self, msgid: str, msg: str, var_names_in_workspace: set) -> str:
        """

        Args:
            msgid: error type such as 'F821'
            msg: message content such as 'xxxxxx'
            var_name_in_workspace:set such as {'a','b','c'}

        Returns:信息。也可能返回空字符串''。

        """
        try:
            matches = re.findall('\'.*?\'', msg)
            if msgid == 'F821' and len(matches) == 1 and matches[0].strip(
                    '\'') in var_names_in_workspace:  # 排除工作空间中已经定义的变量！
                return ''
            message = flake8_translations.get(msgid)
            if message is not None:
                try:
                    if len(matches) == 1:
                        try:
                            return message % matches[0]
                        except TypeError:
                            return message + '(%s)' % matches[0]
                    else:
                        return message % matches
                except:
                    import traceback
                    traceback.print_exc()
                    return message + '(%s)' % matches
            else:
                return msg
        except:
            import traceback
            traceback.print_exc()
            return msg

    def get_number_and_f_color(self, n_type: str) -> int:
        """
        返回警示类型。
        F开头的警告：
            F4xx是定义未引用、引用未定义等；
            F5xx是和字符串相关的问题；
            F6xx是和字典、参数解包、运算符相关的问题；
            F70x代表死循环、yield误用等，F72x代表doctest等等的问题。
            F8xx代表引用未定义、重复定义、函数参数重复。
            F9xx只有F901，代表NotImplementedError调用错误。
            具体链接可见：https://flake8.pycqa.org/en/latest/user/error-codes.html
        C开头的警告只有C901，代表圈复杂度太高。(目前似乎没调通。)
        E开头的警告除了E999之外都是和格式有关的警告。参考见这里：https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
            E1:缩进；
            E2:空格；
            E3:空行；
            E4：引用格式（单行多引用、多行单引用）
            E5:行长度
            E7：这个就是列的长度。
            E9:语法错误或者输入输出错误。
        除了E999之外其余E警告，都不是太重要，不妨忽略之。
        W开头的警告通常比较严重。
        :param n_type:
        :return:
        """
        if n_type == 'E999':
            return PythonHighlighter.ERROR
        elif n_type in show_as_infos:
            return PythonHighlighter.HINT
        elif n_type in show_as_warnings:
            return PythonHighlighter.WARNING

        if n_type.startswith('E'):
            return PythonHighlighter.HINT
        elif n_type.startswith('W'):
            return PythonHighlighter.WARNING
        elif n_type.startswith('F'):
            return PythonHighlighter.ERROR
        else:
            return PythonHighlighter.HINT

    def get_editor_cell(self, line: int) -> Tuple[str, int]:
        """
        获取当前单元格的文字。
        :param line:
        :return:
        """
        text = self.text().strip()
        l = text.split('\n')
        cell_text = ''
        cell_lines = 0
        for i, line_str in enumerate(l[line:]):
            if i != 0:  # 跳过第一行，因其可能含有很多注释。
                if line_str.startswith('###'):
                    break
                else:
                    cell_text += line_str + '\n'
                    cell_lines += 1
        if in_unit_test():
            logger.info('cell text:\n' + cell_text)
        return cell_text, cell_lines

    def get_help_in_console(self):
        """
        在console中获取帮助
        :return:
        """
        jedi_name_list = self.get_help_namelist()
        if len(jedi_name_list) > 0:
            full_name: str = jedi_name_list[0].full_name
            self.extension_lib.get_interface('ipython_console').run_command('help(\'%s\')' % full_name, hidden=False)
        else:
            return

    def get_hint(self):
        pos = self.text_edit.getCursorPosition()
        text = self.text(pos[0])
        try:
            line = text[:pos[1] + 1]
        except Exception as e:
            logger.debug(e)
            line = ''
        hint: str = re.split(r'[;,:\./ \\!&\|\*\+\s\(\)\{\}\[\]]', line)[-1].strip()
        return hint

    def get_help_namelist(self) -> List:
        """
        获取函数的帮助
        :return:
        """
        import jedi
        text = self.text_edit.text()
        path = self._path.replace(os.sep, '/')
        if path.startswith(QDir.tempPath().replace(os.sep, '/')):
            path = ''
        else:
            path = self._path
        script = jedi.Script(text, path=path)
        cursor_position = self.text_edit.getCursorPosition()
        try:
            jedi_name_list = script.infer(cursor_position[0] + 1, cursor_position[1])
        except Exception:
            import traceback
            traceback.print_exc()
            jedi_name_list = []
        if len(jedi_name_list) == 0:
            QMessageBox.warning(self.text_edit, self.tr('Help'),
                                self.tr('Cannot get name.\n' +
                                        'Maybe There is:\n' +
                                        '1、Syntax error in your code.\n' +
                                        '2、No word under text cursor.'), QMessageBox.Ok)
        return jedi_name_list

    def get_help(self):
        """
        获取函数的帮助
        :return:
        """
        jedi_name_list = self.get_help_namelist()
        if len(jedi_name_list) > 0:
            full_name: str = jedi_name_list[0].full_name
            name: str = jedi_name_list[0].name
            if not in_unit_test():
                path = 'https://cn.bing.com/search?q=%s' % full_name
                if self.browser_id is None:
                    self.browser_id = self.extension_lib.get_interface('embedded_browser').open_url(url=path,
                                                                                                    side='right')
                else:
                    self.browser_id = self.extension_lib.get_interface('embedded_browser').open_url(
                        url=path, browser_id=self.browser_id, side='right')
        else:

            return

    # def set_api(self, api_list: List[str], line, col):
    #     self._apis = PMAPI(self._lexer)
    #     for s in api_list:
    #         self._apis.add(s)
    #     self._apis.prepare()

    # def slot_text_changed(self) -> None:
    #     """
    #     文本内容变化槽函数
    #
    #     :return: None
    #     """
    #     super(PMPythonEditor, self).slot_text_changed()
    # if self._smart_autocomp_on:
    #     pos = self.text_edit.getCursorPosition()
    #     hint = self.get_hint()
    #     self.update_api(hint, pos)

    def slot_about_close(self, save_all=False) -> QMessageBox.StandardButton:
        """
        是否需要关闭以及保存

        :param save_all: 当整个窗口关闭时增加是否全部关闭
        :return:QMessageBox.StandardButton
        """
        return super(PMPythonEditor, self).slot_about_close(save_all)

    def slot_goto_definition(self):
        """
        获取函数的definition
        :return:
        """
        jedi_name_list = self.get_help_namelist()
        if len(jedi_name_list) > 0:
            # full_name: str = jedi_name_list[0].full_name
            # name: str = jedi_name_list[0].name
            # jedi_name_list[0].get_definition_end_position(), full_name, jedi_name_list[0].goto_assignments()
            # jedi_name_list[0], jedi_name_list[0].line, 'definitoins!!!!!'
            if jedi_name_list[0].line is not None and jedi_name_list[0].line is not None:
                line, column = jedi_name_list[0].line, 0
                path = jedi_name_list[0].module_path
                self.signal_goto_definition.emit(path, line, column)

    def slot_code_format(self):
        """
        格式化代码
        注意，格式化之前需要保存光标的位置，格式化之后再将光标设置回当前的位置。
        :return:
        """
        text = self.text().strip()
        first_line = self.text_edit.currentLine()
        if not text:
            return
        text = py3compat.removeBOM(text)
        try:
            reformatted_source, _ = yapf_api.FormatCode(
                text,
                filename=self.filename(),
                # print_diff=True,
                style_config=os.path.join(os.path.dirname(__file__), 'config', '.style.yapf')
            )
            self.set_text(reformatted_source)
            self.text_edit.goToLine(first_line)
        except Exception as e:
            logger.warning(str(e))
            lines = re.findall(r'line (\d+)\)', str(e))
            row = -1
            if lines:
                # 跳转到指定行
                row = int(lines[0])
                row = row - 1 if row else 0
                col = self.text_edit.lineLength(row)
                self.text_edit.setCursorPosition(row, col - 1)
                # 标记波浪线
                self.text_edit.fillIndicatorRange(row, 0, row, col, self._indicator_error2)
            QMessageBox.critical(self, self.tr('Error'), str(e))
            if row > -1:
                # 清除被标记波浪线
                self.text_edit.clearIndicatorRange(row, 0, row, col, self._indicator_error2)

    def slot_code_sel_run(self):
        """
        运行选中代码

        :return:
        """
        text = self.text(True).strip()
        if not text:
            text = self.current_line_text().strip()

        self._parent.slot_run_sel(text)

    def slot_code_run(self):
        """
        运行代码

        :return:
        """
        return
        logger.warning('run code' + repr(self._parent))
        text = self.text().strip()
        if not text:
            return

        self._parent.slot_run_script(text)

    def slot_run_in_terminal(self):
        """
        在终端中运行代码
        调用程序的进程管理接口。
        :return:
        """
        text = self.text().strip()
        if not text:
            return
        path = self._path

        run_python_file_in_terminal(path)

    # def slot_run_isolated(self):
    #     """
    #     在终端中运行代码
    #     调用程序的进程管理接口。
    #     :return:
    #     """
    #     text = self.text().strip()
    #     if not text:
    #         return
    #     path = self._path

    def slot_run_cell(self, start_line: int):
        text, lines = self.get_editor_cell(start_line)
        if not text:
            return

        self._parent.slot_run_script(text, self.tr(
            'Running Current Script Cell (Line %d to %d).' % (start_line + 1, start_line + lines + 1)))

    def slot_edit_widget_show(self, line):
        """
        处理显示设置控件的事件。
        :param line:
        :return:
        """

        def get_indent(line: str):
            for i, s in enumerate(line):
                if s != ' ':
                    return i

        code = self.text().split('\n')[line]
        if len(re.findall(r'.*=.*#[ \t]*(?:mkval|mkvar)[ \t]*\[.*\]', code)) > 0:
            splitted = code.split('#')
            if len(splitted) == 2:
                assign_expr, mkvar_expr = splitted
                indentation = get_indent(assign_expr)
                try:
                    val = self.check_mkval_expr(code)
                    if val is not None:
                        identifier, data, params = val
                        l = parse_simplified_pmgjson(identifier, data, params)
                        if l is None:
                            return
                        dlg = PMGPanelDialog(parent=self, views=[l], with_spacer=False)
                        dlg.exec_()
                        vals = dlg.panel.get_value()
                        for k, v in vals.items():
                            line_text = ' ' * indentation + '%s = %s #%s' % (k, repr(v), mkvar_expr)
                            break
                        line_text_list = self.text().split('\n')
                        line_text_list[line] = line_text
                        first_visible_line = self.text_edit.firstVisibleLine()
                        cursor_pos = self.text_edit.getCursorPosition()
                        text = '\n'.join(line_text_list)
                        self.set_text(text)
                        self.set_marker_for_run()
                        self.text_edit.setCursorPosition(line, cursor_pos[1])
                        self.text_edit.setFirstVisibleLine(first_visible_line)
                    else:
                        QMessageBox.warning(self, '警告', '控件创建命令\n\"%s\"\n格式错误。' % mkvar_expr)
                        return
                except:
                    import traceback
                    traceback.print_exc()
                    pass

    def create_context_menu(self) -> 'QMenu':
        print('create_context_menu')
        menu = super().create_context_menu()
        menu.addAction(self._action_help)
        menu.addAction(self._action_help_in_console)
        menu.addAction(self._action_goto_definition)
        return menu

    def check_mkval_expr(self, code: str) -> Optional[Tuple[str, object, List[object]]]:
        """
        判断一行是否满足mkval的需求。
        :param code:
        :return:
        """
        if len(re.findall(r'.*=.*#[ \t]*(?:mkval|mkvar)[ \t]*\[.*\]', code)) > 0:
            splitted = code.split('#')
            if len(splitted) == 2:
                assign_expr, mkvar_expr = splitted
                if len(assign_expr.split('=')) == 2:
                    assign_expr_splitted = assign_expr.split('=')
                    if assign_expr_splitted[0].strip().isidentifier():
                        identifier: str = assign_expr_splitted[0].strip()
                        value_str = assign_expr_splitted[1].strip()
                        if identifier != '' and value_str != '':
                            params_str = re.findall('\[.*\]', mkvar_expr)
                            try:
                                params = ast.literal_eval(params_str[0].strip())
                                data = ast.literal_eval(value_str)
                                return identifier, data, params
                            except:
                                import traceback
                                traceback.print_exc()
        return None

    # def update_api(self, hint: str, pos: tuple):
    #     if (len(self.last_hint) > len(hint) or hint == '') and not self.ends_with_dot():
    #         self.last_hint = hint
    #         return
    #     self.last_hint = hint
    #
    #     path = self._path.replace(os.sep, '/')
    #     if path.startswith(QDir.tempPath().replace(os.sep, '/')):
    #         path = ''
    #     else:
    #         path = self._path
    #     self.completer.worker.set_scan_task(code=self.text_edit.text(), line=pos[0], col=pos[1], path=path)

    def closeEvent(self, event: QCloseEvent) -> None:
        self.autocomp_stop()

    def deleteLater(self) -> None:
        super(PMPythonEditor, self).deleteLater()

    def default_save_path(self) -> str:
        return self.extension_lib.Program.get_work_dir()
