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

import json
import os
from typing import List, Optional, Tuple
import warnings

from PySide6.QtCore import (QDir, QPoint, QRegularExpression,
                            QSortFilterProxyModel,
                            Qt, QTemporaryFile, Signal, Slot)
from PySide6.QtGui import (QAction, QGuiApplication, QFontDatabase, QIcon,
                           QKeySequence, QRegularExpressionValidator)
from PySide6.QtWidgets import (QFormLayout, QGroupBox, QHBoxLayout,
                               QLineEdit, QMenu, QPlainTextEdit, QPushButton,
                               QStyle, QTabWidget, QToolBar, QTreeView,
                               QVBoxLayout, QWidget)

from codemodel import CodeModel
from generator import (code_model_dumper, write_cmake_file,
                       write_typesystem_file)
from gui_utils import (FilterPathChooser, HeaderChooser, IncludePathChooser,
                       OutputPathChooser, add_form_row, ask_question,
                       choose_existing_file, choose_save_file, qt_resource_icon,
                       show_warning)
from qtmodulechooser import QtModuleChooser
from shibokenoptionspanel import ShibokenOptionsPanel
from utils import run_process_output

_PYSIDE_EXTENSIONS_OPTION = '--enable-pyside-extensions'


def _scan_for_classes(header_files: List[str],
                      include_paths: List[str]) -> Tuple[str, str]:
    """Run the code scanner and return a tuple of
       (typesystem XML, error_message)"""
    temp_header = QTemporaryFile(QDir.tempPath() + "/XXXXXX.hpp")
    if not temp_header.open():
        error = 'Temporary file failure: {}'.format(temp_header.errorString())
        return ('', error)
    for f in header_files:
        temp_header.write(b'#include "')
        temp_header.write(bytes(f, "UTF-8"))
        temp_header.write(b'"\n')
    temp_header_name = temp_header.fileName()
    temp_header.close()

    arguments = ['--join-namespaces']
    if include_paths:
        arguments.append('--')
        for i in include_paths:
            arguments.append('-I' + i)
    arguments.append(temp_header_name)

    process_result = run_process_output(code_model_dumper(), arguments)
    if process_result[1] != 0:
        error = "{}\n\n{}".format(process_result[4], process_result[3])
        return ('', error)
    return (process_result[2], '')


class ClassWindow(QWidget):
    """Class window asking for library and header, scanning the code and
       displaying the obtained classes for initial selection."""
    window_title = Signal(str)
    window_modified = Signal(bool)
    can_generate_enabled = Signal(bool)
    status_message = Signal(str)
    loaded = Signal()
    unloaded = Signal(str)

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

        self._file_name = ''

        self._dirty = False
        style = self.style()

        icon = QIcon.fromTheme('document-new')
        self._new_action = QAction(icon, "New", self)
        self._new_action.setShortcut(QKeySequence.New)
        self._new_action.setToolTip("Start a new Project")
        self._new_action.triggered.connect(self.new_project)

        fallback_icon = qt_resource_icon('standardbutton-open')
        icon = QIcon.fromTheme('document-open', fallback_icon)
        self._load_action = QAction(icon, "Open", self)
        self._load_action.setShortcut(QKeySequence.Open)
        self._load_action.setToolTip("Open a Project")
        self._load_action.triggered.connect(self.load_project)

        fallback_icon = qt_resource_icon('standardbutton-save')
        icon = QIcon.fromTheme('document-save', fallback_icon)
        self._save_action = QAction(icon, "Save", self)
        self._save_action.setShortcut(QKeySequence.Save)
        self._save_action.setToolTip("Save Project")
        self._save_action.triggered.connect(self.save_project)
        self._save_action.setEnabled(False)

        icon = QIcon.fromTheme('document-save-as')
        self._save_as_action = QAction(icon, "Save As...", self)
        self._save_as_action.setToolTip("Save Project As")
        self._save_as_action.setShortcut(QKeySequence.SaveAs)
        self._save_as_action.triggered.connect(self.save_project_as)
        self._save_as_action.setEnabled(False)

        fallback_icon = style.standardIcon(QStyle.SP_FileDialogContentsView)
        icon = QIcon.fromTheme('search', fallback_icon)
        self._scan_for_classes_action = QAction(icon, "Scan for Classes", self)
        tool_tip = "Scan the header for classes"
        self._scan_for_classes_action.setToolTip(tool_tip)
        self._scan_for_classes_action.setShortcut(Qt.CTRL | Qt.Key_R)
        self._scan_for_classes_action.triggered.connect(self._scan_for_classes)
        self._scan_for_classes_action.setEnabled(False)

        # Tree context menu
        icon = QIcon.fromTheme('edit-select-all')
        self._select_all_classes_action = QAction(icon, "Select All", self)
        icon = QIcon.fromTheme('kr_unselect')
        self._unselect_all_classes_action = QAction(icon, "Unselect All", self)

        central_layout = QHBoxLayout(self)
        left_col_layout = QVBoxLayout()
        central_layout.addLayout(left_col_layout, 1)

        # Form
        form_layout = QFormLayout()
        form_group = QGroupBox("* Project")
        form_group.setLayout(form_layout)
        left_col_layout.addWidget(form_group, 4)
        left_col_layout.addStretch(1)
        self._project_name_le = QLineEdit()
        name_pattern = QRegularExpression('^[a-zA-Z_0-9]+$')
        validator = QRegularExpressionValidator(name_pattern,
                                                self._project_name_le)
        self._project_name_le.setValidator(validator)
        self._project_name_le.setClearButtonEnabled(True)
        tool_tip = "Project name which will become the module name"
        add_form_row(form_layout, 'Name:', self._project_name_le, tool_tip)

        library_filter = 'C++ Libraries (*.lib *.a *.so *.dll *.dylib)'
        self._lib_chooser = FilterPathChooser('Library', [library_filter], self)
        tool_tip = "The library (shared object or static library) for which to create bindings."
        add_form_row(form_layout, 'Library file:', self._lib_chooser, tool_tip)
        self._lib_chooser.changed.connect(self._changed)

        self._output_chooser = OutputPathChooser(self)
        tool_tip = "The output directory to place the typesystem and cmake files"
        add_form_row(form_layout, 'Output directory:', self._output_chooser, tool_tip)

        # Shiboken panel
        self._shiboken_options_panel = ShibokenOptionsPanel(self)
        self._shiboken_options_panel.changed.connect(self._changed)
        left_col_layout.addWidget(self._shiboken_options_panel, 8)
        left_col_layout.addStretch(1)

        # Qt panel
        self._qtmodulechooser = QtModuleChooser()
        left_col_layout.addWidget(self._qtmodulechooser, 10)
        left_col_layout.addStretch(1)

        # Header panel
        self._header_chooser = HeaderChooser(self)
        self._header_chooser.setToolTip("The header file(s) of the library")
        left_col_layout.addWidget(self._header_chooser, 8)

        # Included panel
        self._include_chooser = IncludePathChooser(self)
        self._include_chooser.setToolTip("Additional include paths")
        left_col_layout.addWidget(self._include_chooser, 8)

        # Buttons
        #self._scan_button = QPushButton("Scan for classes")
        #self._scan_button.clicked.connect(self._scan_for_classes)
        #button_layout = QHBoxLayout()
        #button_layout.addStretch(3)
        #button_layout.addWidget(self._scan_button, 1)
        #left_col_layout.addLayout(button_layout, 1)

        self._header_chooser.changed.connect(self._changed)

        # Tabs from the right side of the UI
        self._result_tab_widget = QTabWidget()

        class_widget = QWidget()
        class_layout = QVBoxLayout(class_widget)
        filter_line_edit = QLineEdit()
        filter_line_edit.setPlaceholderText('Filter')
        filter_line_edit.setClearButtonEnabled(True)
        class_layout.addWidget(filter_line_edit)

        self._class_tree = QTreeView()
        self._class_tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self._class_tree.customContextMenuRequested.connect(self._tree_contextmenu)

        tool_tip = "Classes and namespaces obtained from scanning the code"
        self._class_tree.setToolTip(tool_tip)
        class_layout.addWidget(self._class_tree)
        self._generate_button = QPushButton("Generate")
        self._generate_button.setEnabled(False)
        self._generate_button.clicked.connect(self.parent()._generate)
        generate_layout = QHBoxLayout()
        generate_layout.addStretch(2)
        generate_layout.addWidget(self._generate_button)
        class_layout.addLayout(generate_layout)

        self._code_model = CodeModel(self)
        self._code_model.dataChanged.connect(self._changed)
        self._select_all_classes_action.triggered.connect(self._code_model.select_all)
        self._unselect_all_classes_action.triggered.connect(self._code_model.unselect_all)

        self._class_filter_model = QSortFilterProxyModel(self)
        self._class_filter_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self._class_filter_model.setFilterKeyColumn(1)
        self._class_filter_model.setSourceModel(self._code_model)

        self._class_tree.setModel(self._class_filter_model)
        filter_line_edit.textChanged.connect(self._class_filter_model.setFilterFixedString)
        self._result_tab_widget.addTab(class_widget, "Detected classes")

        # Output Tab that shows the generated typesystem or errors
        self._log_edit = QPlainTextEdit()
        fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
        self._log_edit.setFont(fixed_font)
        self._log_edit.setReadOnly(True)
        self._result_tab_widget.addTab(self._log_edit, "Output")

        # TODO: self.parent() does not work inside a Slot method, and in this
        # case it gets override for a QStackedWidget - ???
        self._parent = self.parent()
        central_layout.addWidget(self._result_tab_widget, 1)

    def populate_menus(self, file_menu: QMenu, tool_bar: QToolBar):
        file_menu.addAction(self._new_action)
        file_menu.addAction(self._load_action)
        file_menu.addAction(self._save_action)
        file_menu.addAction(self._save_as_action)
        file_menu.addAction(self._scan_for_classes_action)
        tool_bar.addAction(self._new_action)
        tool_bar.addAction(self._load_action)
        tool_bar.addAction(self._save_action)
        tool_bar.addAction(self._scan_for_classes_action)

    def output_directory(self) -> str:
        return self._output_chooser.path()

    @Slot()
    def set_output_directory(self, p: str) -> None:
        self._output_chooser.set_path(p)

    @Slot()
    def _scan_for_classes(self):
        QGuiApplication.setOverrideCursor(Qt.BusyCursor)
        try:
            self._do_scan_for_classes()
        finally:
            QGuiApplication.restoreOverrideCursor()

    def _do_scan_for_classes(self) -> None:
        self._clear_scan_result()
        parse_result = _scan_for_classes(self._header_chooser.files(),
                                         self._include_chooser.files())
        if parse_result[1]:
            self._log_edit.setPlainText(parse_result[1])
            self._log_edit.setStyleSheet('background : "red"')
            self._result_tab_widget.setCurrentIndex(1)
            return
        self._result_tab_widget.setCurrentIndex(0)
        xml = parse_result[0]
        self._code_model.parse(xml)
        self._log_edit.setPlainText(xml)
        if self._code_model.rowCount() == 0:
            warnings.warn('No classes found')
        else:
            self.status_message.emit("Check the 'Detected Classes' and 'Output' "
                                     "before clicking 'Generate'")
        self._update_actions()

    def project_name(self) -> str:
        return self._project_name_le.text()

    def set_project_name(self, name: str) -> None:
        self._project_name_le.setText(name)

    def library(self) -> str:
        return self._lib_chooser.path()

    def set_library(self, p: str) -> None:
        self._lib_chooser.set_path(p)

    def use_qt(self) -> bool:
        return self._qtmodulechooser.isChecked()

    def set_use_qt(self, u: bool) -> None:
        self._qtmodulechooser.setChecked(u)

    def set_dirty(self, d: bool) -> None:
        if self._dirty != d:
            self._dirty = d
            self.window_modified.emit(d)

    def is_dirty(self) -> bool:
        return self._dirty

    def ensure_clean(self, message: str = '') -> bool:
        """Ask to discard unsaved changes"""
        result = True
        if self._dirty:
            text = f"There are unsaved changes.\n{message}"
            result = ask_question(self, "Unsaved Changes", text)
        if result and self._file_name:
            self.unloaded.emit(self._file_name)
        return result

    def generate(self, cmake_file: str, typesystem_file: str) -> None:
        """Generate cmake and typesystem file"""
        name = self.project_name()
        selected_classes = self._code_model.selected_classes()
        qt_modules = self._qtmodulechooser.selection() if self.use_qt() else []
        shiboken_options = self._shiboken_options_panel.selection()
        if qt_modules and _PYSIDE_EXTENSIONS_OPTION not in shiboken_options:
            shiboken_options.append(_PYSIDE_EXTENSIONS_OPTION)
        write_cmake_file(name, self.library(), cmake_file,
                         typesystem_file, self._header_chooser.files(),
                         self._include_chooser.files(),
                         shiboken_options, qt_modules, selected_classes)

        dom_typesystem = self._code_model.typesystem()
        dom_typesystem.documentElement().setAttribute('package', name)
        write_typesystem_file(typesystem_file, dom_typesystem)

        # Adding content of generated files to the tabs
        self._parent._workbench.setTabEnabled(self._parent._workbench.id_typesystem, True)
        self._parent._workbench.setTabEnabled(self._parent._workbench.id_cmakelists, True)
        self._update_actions()
        self._parent._workbench._update_actions()
        self.status_message.emit("Check the 'Typesystem' and 'CmakeLists' files before 'Build'")

    @Slot(QPoint)
    def _tree_contextmenu(self, point: QPoint) -> None:
        menu = QMenu()
        menu.addAction(self._select_all_classes_action)
        menu.addAction(self._unselect_all_classes_action)
        menu.exec(self._class_tree.mapToGlobal(point))

    def _changed(self) -> None:
        self.set_dirty(True)
        self._update_actions()

    @Slot()
    def _update_actions(self) -> None:
        has_headers = self._header_chooser.count() > 0
        self._scan_for_classes_action.setEnabled(has_headers)
        self._save_action.setEnabled(self._file_name != '')
        self._save_as_action.setEnabled(has_headers)

        has_name = bool(self.project_name())
        has_library = bool(self.library())
        has_classes = self._code_model.rowCount() > 0
        can_generate = (has_name and has_headers and has_library
                        and has_classes)
        self._generate_button.setEnabled(can_generate)
        self.can_generate_enabled.emit(can_generate)

    @Slot()
    def _clear_scan_result(self) -> None:
        """Clear the UI displaying the scan result"""
        self._log_edit.setPlainText('')
        self._log_edit.setStyleSheet('')
        self._code_model.clear()

    @Slot()
    def clear_ui(self) -> None:
        """Clear the UI"""
        self._clear_scan_result()
        self.set_project_name('')
        self._header_chooser.set_files([])
        self._include_chooser.set_files([])
        self.set_library('')
        self.set_output_directory('')
        self._shiboken_options_panel.restore_defaults()
        self._qtmodulechooser.restore_defaults()
        self.set_use_qt(False)

    def _set_file_name(self, file: str) -> None:
        """Set file name after Save As/Open, clearing dirty, etc"""
        self._file_name = file
        self._update_actions()
        base = os.path.basename(self._file_name)
        self.window_title.emit(f'{base}[*]')
        self.set_dirty(False)

    @Slot()
    def save_project_as(self) -> None:
        file = choose_save_file(self, 'Project File', ['application/json'],
                                'json')
        if file:
            try:
                self.save_project_to_file(file)
                self._set_file_name(file)
            except Exception as e:
                show_warning(self, 'Save As Failed', str(e))

    @Slot()
    def save_project(self) -> None:
        try:
            self.save_project_to_file(self._file_name)
            self.set_dirty(False)
        except Exception as e:
            show_warning(self, 'Save Failed', str(e))

    @Slot()
    def new_project(self) -> None:
        if self.ensure_clean('Start new project anyways?'):
            self.clear_ui()
            self.set_dirty(False)

    @Slot()
    def load_project(self) -> None:
        if not self.ensure_clean('Load new project anyways?'):
            return
        file = choose_existing_file(self, 'Project File', ['application/json'])
        if file:
            try:
                self.load_project_from_file(file)
                self.loaded.emit()
            except Exception as e:
                self.window_title.emit('[*]')
                show_warning(self, 'Load Failed', str(e))

    def save_project_to_file(self, file_name: str) -> None:
        data = {'name': self.project_name(),
                'library': self.library(),
                'output_directory': self.output_directory(),
                'headers': self._header_chooser.files(),
                'include_paths': self._include_chooser.files(),
                'shiboken_options': self._shiboken_options_panel.selection(),
                'use_qt': self.use_qt(),
                'qt_modules': self._qtmodulechooser.selection()}

        self._code_model.save(data)
        with open(file_name, 'w') as f:
            json.dump(data, f)
            base = os.path.basename(file_name)
            self.status_message.emit(f'Wrote {base}')

    def load_project_from_file(self, file_name: str) -> None:
        self._clear_scan_result()
        with open(file_name, 'r') as f:
            data = json.load(f)
            self.set_project_name(data.get('name'))
            self._header_chooser.set_files(data.get('headers'))
            self._include_chooser.set_files(data.get('include_paths'))
            self.set_library(data.get('library'))
            self.set_output_directory(data.get('output_directory'))
            self._shiboken_options_panel.set_selection(data.get('shiboken_options'))
            self.set_use_qt(bool(data.get('use_qt')))
            self._qtmodulechooser.set_selection(data.get('qt_modules'))
            self._code_model.load(data)

            self._set_file_name(file_name)
            self._update_actions()
