# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial

import os
import re
import sys
from typing import Optional
import warnings

from PySide6.QtCore import (QDir, QObject, QProcess, Qt,
                            QTemporaryFile, Slot)
from PySide6.QtGui import (QAction, QColor, QGuiApplication,
                           QFontDatabase, QIcon, QPixmap, QSyntaxHighlighter,
                           QTextCharFormat)
from PySide6.QtWidgets import (QMenu, QPlainTextEdit,
                               QPushButton, QTabWidget, QToolBar,
                               QVBoxLayout, QWidget)

from gui_utils import load_file, qt_refresh_icon, qt_resource_icon
from filedisplay import FileDisplay
from options import Options
from processview import ProcessView
from utils import edit_file, launch_creator

SHIBOKEN_WARNINGS = re.compile(r'^qt\.shiboken: \(([^)]+)\) ([^:]+):(\d+):(?:\d+:)? (.*)$')
# \...\tst_qtjson.cpp(2427): warning C4996: 'QJsonDocument::fromBinaryData': Use CBOR format instead
MSVC_WARNING = re.compile(r'^([^(]+)\((\d+)\) ?: warning (C\d+:.*)$')
MSVC_ERROR = re.compile(r'^([^(]+)\((\d+)\) ?: error (C\d+:.*)$')
GCC_WARNING = re.compile(r'^([^:]+):(\d+):\d*:? warning: (.*)$')
GCC_ERROR = re.compile(r'^([^:]+):(\d+):\d*:? error: (.*)$')

COMPILER_WARNINGS = GCC_WARNING if sys.platform != 'win32' else MSVC_WARNING
COMPILER_ERRORS = GCC_ERROR if sys.platform != 'win32' else MSVC_ERROR

CMAKE_BUILD = ['cmake', '--build', '.']


def build_output_to_task(build_dir: str, line: str) -> Optional[str]:
    """Filter warnings from the build log and return a task file line"""
    match = SHIBOKEN_WARNINGS.match(line)
    if match:
        module = match.group(1)
        file_name = match.group(2).replace('\\', '/')
        line_number = match.group(3)
        text = match.group(4)
        return f"{file_name}\t{line_number}\twarn\t{module}: {text}\n"
    type = ""
    match = COMPILER_WARNINGS.match(line)
    if match:
        type = "warn"
    else:
        match = COMPILER_ERRORS.match(line)
        if match:
            type = "err"
    if match:
        file_name = match.group(1)
        if not os.path.isabs(file_name):
            file_name = os.path.join(build_dir, file_name)
        file_name = file_name.replace('\\', '/')
        line_number = match.group(2)
        text = match.group(3)

        return f"{file_name}\t{line_number}\t{type}\t{text}\n"
    return None


class BuildLogHighlighter(QSyntaxHighlighter):
    """Highlight warnings and errors in a build log (shiboken/compilers)."""

    def __init__(self, parent: QObject = None):
        QSyntaxHighlighter.__init__(self, parent)

        self._mappings = {}
        warnings_format = QTextCharFormat()
        warnings_format.setBackground(Qt.yellow)
        self._mappings[SHIBOKEN_WARNINGS] = warnings_format
        self._mappings[COMPILER_WARNINGS] = warnings_format
        error_format = QTextCharFormat()
        error_color = QColor(Qt.red).lighter()
        error_format.setBackground(error_color)
        self._mappings[COMPILER_ERRORS] = error_format

    def highlightBlock(self, text):
        for pattern, format in self._mappings.items():
            if pattern.match(text):
                self.setFormat(0, len(text), format)


class Workbench(QTabWidget):
    """A workbench letting the user fine-tune the typesystem by re-running
       the generator and building, displaying the log files."""

    def __init__(self, parent: Optional[QWidget] = None):
        super(Workbench, self).__init__(parent)

        self._cmake_file = ''
        self._typesystem_file = ''
        self._build_dir = ''
        self._cmake_cache_file = ''
        self._tasks_temp_file = None

        self._edit_cmake_action = QAction("Edit CMakeLists.txt", self)
        self._edit_cmake_action.setShortcut(Qt.CTRL | Qt.Key_E)
        self._edit_cmake_action.triggered.connect(self._edit_cmake)
        self._edit_cmake_action.setEnabled(False)

        self._edit_typesystem_action = QAction("Edit Typesystem", self)
        self._edit_typesystem_action.triggered.connect(self._edit_typesystem)
        self._edit_typesystem_action.setShortcut(Qt.CTRL | Qt.Key_E)
        self._edit_typesystem_action.setEnabled(False)

        fallback_icon = qt_resource_icon('standardbutton-delete')
        icon = QIcon.fromTheme('run-build-clean', fallback_icon)
        self._clean_action = QAction(icon, "Clean", self)
        tt = 'Build the clean target'
        self._clean_action.setToolTip(tt)
        self._clean_action.triggered.connect(self._clean)
        self._clean_action.setEnabled(False)

        fallback_icon = qt_refresh_icon()
        icon = QIcon.fromTheme('user-trash', fallback_icon)
        self._delete_cache_action = QAction(icon, "Delete Cache", self)
        tt = 'Delete CMakeCache.txt, forcing a re-run of cmake'
        self._delete_cache_action.setToolTip(tt)
        self._delete_cache_action.triggered.connect(self._delete_cache)
        self._delete_cache_action.setEnabled(False)

        fallback_icon = QIcon(':/images/run.png')
        fallback_icon.addPixmap(QPixmap(':/images/run@2x.png'))
        icon = QIcon.fromTheme('run-build', fallback_icon)
        self._build_action = QAction(icon, "Build", self)
        self._build_action.setShortcut(Qt.CTRL | Qt.Key_B)
        self._build_action.triggered.connect(self._build)
        self._build_action.setEnabled(False)

        self._process_view = ProcessView()
        self._log_highlighter = BuildLogHighlighter()
        self._log_highlighter.setDocument(self._process_view.document())
        self._process_view.finished.connect(self._build_finished)
        self._process_view.setToolTip("Build output")

        fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
        # Typesystem preview tab
        self._typesystem_tab = QWidget()
        typesystem_layout = QVBoxLayout()
        self._typesystem_tab.setLayout(typesystem_layout)
        self._typesystem_edit = FileDisplay()
        self._typesystem_edit.setFont(fixed_font)
        self._typesystem_edit.setPlainText("Project has not been generated")
        self._typesystem_edit.setReadOnly(True)
        self._typesystem_edit_button = QPushButton("Edit Typesystem")
        self._typesystem_edit_button.clicked.connect(self._edit_typesystem)
        typesystem_layout.addWidget(self._typesystem_edit)
        typesystem_layout.addWidget(self._typesystem_edit_button)

        self.addTab(self._typesystem_tab, "Typesystem")
        self.id_typesystem = self.indexOf(self._typesystem_tab)
        self.setTabEnabled(self.id_typesystem, False)

        # CMakeLists preview tab
        self._cmakelists_tab = QWidget()
        cmakelists_layout = QVBoxLayout()
        self._cmakelists_tab.setLayout(cmakelists_layout)
        self._cmakelists_edit = FileDisplay()
        self._cmakelists_edit.setFont(fixed_font)
        self._cmakelists_edit.setReadOnly(True)
        self._cmakelists_edit_button = QPushButton("Edit CMakeLists")
        self._cmakelists_edit_button.clicked.connect(self._edit_cmake)
        cmakelists_layout.addWidget(self._cmakelists_edit)
        cmakelists_layout.addWidget(self._cmakelists_edit_button)

        self.addTab(self._cmakelists_tab, "CMakeLists")
        self.id_cmakelists = self.indexOf(self._cmakelists_tab)
        self.setTabEnabled(self.id_cmakelists, False)

        # Tabs for the build
        self.addTab(self._process_view, 'Output')
        self.id_output = self.indexOf(self._process_view)
        self.setTabEnabled(self.id_output, False)

        # Build a list of tuple log file/widget to load shiboken2 logs into
        self._logs = []
        logger = QPlainTextEdit()
        logger.setFont(fixed_font)
        self.addTab(logger, 'Rejected Classes')
        self.id_rejected_classes = self.indexOf(logger)
        self.setTabEnabled(self.id_rejected_classes, False)
        self._logs.append((logger, 'mjb_rejected_classes.log'))

        logger = QPlainTextEdit()
        logger.setFont(fixed_font)
        self.addTab(logger, 'Rejected Functions')
        self.id_rejected_functions = self.indexOf(logger)
        self.setTabEnabled(self.id_rejected_functions, False)
        self._logs.append((logger, 'mjb_rejected_functions.log'))

        logger = QPlainTextEdit()
        logger.setFont(fixed_font)
        self.addTab(logger, 'Rejected Enums')
        self.id_rejected_enums = self.indexOf(logger)
        self.setTabEnabled(self.id_rejected_enums, False)
        self._logs.append((logger, 'mjb_rejected_enums.log'))

        logger = QPlainTextEdit()
        logger.setFont(fixed_font)
        self.addTab(logger, 'Rejected Fields')
        self.id_rejected_fields = self.indexOf(logger)
        self.setTabEnabled(self.id_rejected_fields, False)
        self._logs.append((logger, 'mjb_rejected_fields.log'))

        self._update_actions()
        self._parent = self.parent()

    def populate_menus(self, menu: QMenu, tool_bar: QToolBar) -> None:
        menu.addAction(self._edit_cmake_action)
        menu.addAction(self._edit_typesystem_action)
        menu.addAction(self._build_action)
        menu.addAction(self._delete_cache_action)
        menu.addAction(self._clean_action)

        tool_bar.addAction(self._build_action)
        tool_bar.addAction(self._delete_cache_action)
        tool_bar.addAction(self._clean_action)

    def set_files(self, cmake_file: str, typesystem_file: str) -> None:
        directory = QDir.toNativeSeparators(os.path.dirname(cmake_file))
        self._cmake_file = cmake_file
        self._cmakelists_edit.load(cmake_file)

        self._typesystem_file = typesystem_file
        self._typesystem_edit.load(typesystem_file)

        self._build_dir = os.path.join(directory, 'build')
        self._cmake_cache_file = os.path.join(self._build_dir,
                                              'CMakeCache.txt')
        self._update_actions()

    @Slot()
    def _build(self) -> None:
        QGuiApplication.setOverrideCursor(Qt.BusyCursor)
        try:
            self._do_build()
        finally:
            QGuiApplication.restoreOverrideCursor()

    def _do_build(self) -> None:
        if not os.path.isdir(self._build_dir):
            os.mkdir(self._build_dir)
        commands = [['cmake', '-H..', '-G', 'Ninja',
                     '-DCMAKE_BUILD_TYPE=Release'],
                    CMAKE_BUILD]
        self._process_view.start(commands, self._build_dir)
        # Change to the Build tab (Workbench)
        #self._parent._mode_tab_widget.setCurrentIndex(self._parent._id_workbench_tab)
        self.setTabEnabled(self.id_output, True)
        self.setCurrentIndex(self.id_output)

    @Slot()
    def _edit_cmake(self) -> None:
        edit_file(self._cmake_file)

    @Slot()
    def _edit_typesystem(self) -> None:
        edit_file(self._typesystem_file)

    @Slot(int, QProcess.ExitStatus)
    def _build_finished(self, code: int, state: QProcess.ExitStatus) -> None:
        # Activating the rejected tabs
        self.setTabEnabled(self.id_rejected_classes, True)
        self.setTabEnabled(self.id_rejected_functions, True)
        self.setTabEnabled(self.id_rejected_enums, True)
        self.setTabEnabled(self.id_rejected_fields, True)
        self._parent._class_window.status_message.emit("Build finished")

        for log in self._logs:
            file = os.path.join(self._build_dir, log[1])
            load_file(log[0], file)
        if Options.use_creator():
            self._process_log()
        self._update_actions()

    def _process_log(self) -> None:
        """Filter the warnings from the log and load them into Qt Creator"""
        if not Options.use_creator():
            return
        if not self._tasks_temp_file:
            tp = QDir.tempPath()
            self._tasks_temp_file = QTemporaryFile(f'{tp}/XXXXXX.tasks', self)
        if not self._tasks_temp_file.open():
            warnings.warn(self._tasks_temp_file.errorString())
            return
        for line in self._process_view.toPlainText().split('\n'):
            task = build_output_to_task(self._build_dir, line)
            if task:
                self._tasks_temp_file.write(task.encode('utf-8'))
        self._tasks_temp_file.close()
        launch_creator(self._tasks_temp_file.fileName())

    @Slot()
    def _clean(self) -> None:
        if os.path.exists(self._cmake_cache_file):
            command = CMAKE_BUILD.copy()
            command.append('--target')
            command.append('clean')
            self._process_view.start([command], self._build_dir)

    @Slot()
    def _delete_cache(self) -> None:
        if os.path.exists(self._cmake_cache_file):
            try:
                os.remove(self._cmake_cache_file)
                self._update_actions()
            except:  # noqa: E722
                pass

    @Slot()
    def _update_actions(self) -> None:
        cmake_exists = os.path.exists(self._cmake_file)
        self._edit_cmake_action.setEnabled(cmake_exists)
        self._edit_typesystem_action.setEnabled(cmake_exists)
        self._build_action.setEnabled(cmake_exists)
        self._clean_action.setEnabled(cmake_exists)
        cache_exists = os.path.exists(self._cmake_cache_file)
        self._delete_cache_action.setEnabled(cache_exists)
