<?xml version="1.0" encoding="UTF-8"?>
<pythonPanelDocument>
  <!-- This file contains definitions of Python interfaces and the
 interfaces menu.  It should not be hand-edited when it is being
 used by the application.  Note, that two definitions of the
 same interface or of the interfaces menu are not allowed
 in a single file. -->
  <interface name="hpackage" label="hpackage" icon="COMMON_subnet" showNetworkNavigationBar="false" help_url="">
    <script><![CDATA[
import importlib
import os
import platform
import re
import stat
import subprocess
import sys
from pathlib import Path

import hou
from PySide6 import QtCore, QtWidgets


MINIMUM_HPACKAGE_VERSION = "0.4.3"
ALLOW_PRERELEASES_ENV_VAR = "HOUDINI_PACKAGE_ALLOW_PRERELEASES"
FALSE_ENV_VALUES = {"", "0", "false", "no", "off"}


class BootstrapPanel(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.install_dialog = None
        self.install_thread = None
        self.install_worker = None
        self.panel_widget = None

        self.status_label = QtWidgets.QLabel(
            "Preparing the hpackage panel...")
        self.status_label.setAlignment(QtCore.Qt.AlignCenter)
        self.progress_bar = QtWidgets.QProgressBar()
        self.progress_bar.setRange(0, 0)
        self.progress_bar.setTextVisible(False)
        self.details = QtWidgets.QPlainTextEdit()
        self.details.setReadOnly(True)
        self.details.setVisible(False)

        self.main_layout = QtWidgets.QVBoxLayout(self)
        self.main_layout.addStretch(1)
        self.main_layout.addWidget(self.status_label)
        self.main_layout.addWidget(self.progress_bar)
        self.main_layout.addWidget(self.details)
        self.main_layout.addStretch(1)

        QtCore.QTimer.singleShot(0, self.start_bootstrap)

    def start_bootstrap(self):
        hpackage = find_importable_hpackage()
        if (hpackage is not None and
                hpackage_version_at_least(
                    hpackage.__version__, MINIMUM_HPACKAGE_VERSION)):
            self.load_hpackage_interface(hpackage)
            return

        self.status_label.setText(
            "Installing hpackage. This may take a minute...")
        self.show_install_dialog()
        self.install_thread = QtCore.QThread(get_stable_qt_parent())
        self.install_worker = InstallWorker()
        self.install_worker.moveToThread(self.install_thread)
        self.install_thread.started.connect(self.install_worker.run)
        self.install_worker.finished.connect(self.on_install_finished)
        self.install_worker.finished.connect(self.install_thread.quit)
        self.install_worker.finished.connect(self.install_worker.deleteLater)
        self.install_thread.finished.connect(self.install_thread.deleteLater)
        self.install_thread.finished.connect(self.clear_install_thread_refs)
        self.install_thread.start()

    def show_install_dialog(self):
        self.install_dialog = QtWidgets.QDialog(get_houdini_main_window())
        self.install_dialog.setWindowTitle("Installing hpackage")
        layout = QtWidgets.QVBoxLayout(self.install_dialog)
        label = QtWidgets.QLabel(
            "Installing or upgrading hpackage.\n"
            "Houdini will continue when this is finished.")
        progress_bar = QtWidgets.QProgressBar()
        progress_bar.setRange(0, 0)
        progress_bar.setTextVisible(False)
        layout.addWidget(label)
        layout.addWidget(progress_bar)
        self.install_dialog.setModal(False)
        self.destroyed.connect(self.install_dialog.close)
        self.install_dialog.show()

    def close_install_dialog(self):
        if self.install_dialog is None:
            return
        self.install_dialog.close()
        self.install_dialog.deleteLater()
        self.install_dialog = None

    @QtCore.Slot(bool, str)
    def on_install_finished(self, succeeded, message):
        self.close_install_dialog()
        if not succeeded:
            self.show_error(message)
            return

        try:
            hpackage = reload_or_import_hpackage()
            self.load_hpackage_interface(hpackage)
        except Exception as e:
            self.show_error(str(e))

    @QtCore.Slot()
    def clear_install_thread_refs(self):
        self.install_thread = None
        self.install_worker = None

    def load_hpackage_interface(self, hpackage):
        self.status_label.setText("Loading hpackage panel...")
        self.progress_bar.setVisible(False)
        self.details.setVisible(False)
        from hpackage.ui import onCreateInterface
        self.panel_widget = onCreateInterface(self)
        clear_layout(self.main_layout)
        self.main_layout.setContentsMargins(0, 0, 0, 0)
        self.main_layout.addWidget(self.panel_widget)

    def show_error(self, message):
        self.status_label.setText("hpackage could not be installed.")
        self.progress_bar.setVisible(False)
        self.details.setVisible(True)
        self.details.setPlainText(message)
        QtWidgets.QMessageBox.critical(
            self, "hpackage Installation Failed", message)


class InstallWorker(QtCore.QObject):
    finished = QtCore.Signal(bool, str)

    @QtCore.Slot()
    def run(self):
        try:
            install_or_upgrade_hpackage()
        except Exception as e:
            self.finished.emit(False, str(e))
        else:
            self.finished.emit(True, "")


def clear_layout(layout):
    while layout.count():
        item = layout.takeAt(0)
        widget = item.widget()
        if widget is not None:
            widget.setParent(None)
            widget.deleteLater()


def get_houdini_main_window():
    try:
        return hou.qt.mainWindow()
    except Exception:
        return get_stable_qt_parent()


def get_stable_qt_parent():
    return QtWidgets.QApplication.instance()


def find_importable_hpackage():
    ensure_home_dir_in_sys_path()
    try:
        import hpackage
    except ImportError:
        return None
    return hpackage


def reload_or_import_hpackage():
    ensure_home_dir_in_sys_path()
    importlib.invalidate_caches()
    hpackage = find_importable_hpackage()
    if hpackage is not None:
        reload_hpackage(hpackage)
    else:
        import hpackage

    hpackage = sys.modules["hpackage"]
    if not hpackage_version_at_least(
            hpackage.__version__, MINIMUM_HPACKAGE_VERSION):
        purge_hpackage_modules()
        importlib.invalidate_caches()
        import hpackage

    if not hpackage_version_at_least(
            hpackage.__version__, MINIMUM_HPACKAGE_VERSION):
        raise RuntimeError(
            "hpackage was installed, but Houdini is still loading version "
            f"{hpackage.__version__}. Please restart Houdini and try "
            "again.")
    return hpackage


def purge_hpackage_modules():
    for module_name in list(sys.modules):
        if module_name == "hpackage" or module_name.startswith("hpackage."):
            del sys.modules[module_name]


def ensure_home_dir_in_sys_path(create=False):
    home_python_libs_dir = get_home_python_libs_dir()
    if create:
        home_python_libs_dir.mkdir(exist_ok=True)
    if (home_python_libs_dir.exists() and
            str(home_python_libs_dir) not in sys.path):
        sys.path.insert(0, str(home_python_libs_dir))
    return home_python_libs_dir


def get_home_python_libs_dir():
    """Returns the per-user directory where Houdini will search for Python
    packages.
    """
    major_ver = sys.version_info.major
    minor_ver = sys.version_info.minor
    return Path(os.path.expandvars(
        f"$HOUDINI_USER_PREF_DIR/python{major_ver}.{minor_ver}libs"))


def install_or_upgrade_hpackage():
    # On Windows, particularly where the user's Documents directory is on
    # OneDrive, some directories/files may be read-only and we need to make
    # them writable so that pip can work.
    python_binary = get_houdini_python_binary()
    home_python_libs_dir = ensure_home_dir_in_sys_path(create=True)
    make_contents_writable(home_python_libs_dir)
    pip_command = [python_binary, "-m", "pip", "install", "-U", "hpackage",
        "--target", str(home_python_libs_dir)]
    if allow_hpackage_prereleases():
        pip_command.append("--pre")

    try:
        subprocess.run(
            pip_command, capture_output=True, text=True, check=True,
            **get_subprocess_kwargs())
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"{e}\n{e.stdout or ''}\n{e.stderr or ''}")


def get_subprocess_kwargs():
    if (platform.system() == "Windows" and
            hasattr(subprocess, "CREATE_NO_WINDOW")):
        return {"creationflags": subprocess.CREATE_NO_WINDOW}
    return {}


def get_houdini_python_binary():
    major_ver = sys.version_info.major
    minor_ver = sys.version_info.minor
    if platform.system() == "Windows":
        return os.path.expandvars(
            f"$HFS/python{major_ver}{minor_ver}/python.exe")

    if platform.system() == "Darwin":
        return os.path.expandvars("$HFS/bin/hython")

    return sys.executable


def make_contents_writable(toplevel_dir_path):
    if not toplevel_dir_path.exists():
        return

    clear_readonly_flag(toplevel_dir_path)
    stack = [toplevel_dir_path]
    while stack:
        dir_path = stack.pop()
        with os.scandir(dir_path) as entries:
            for entry in entries:
                entry_path = Path(entry.path)
                clear_readonly_flag(entry_path)
                if entry.is_dir(follow_symlinks=False):
                    stack.append(entry_path)


def clear_readonly_flag(path):
    try:
        existing_mode = path.stat().st_mode
    except FileNotFoundError:
        return

    if not (existing_mode & stat.S_IWRITE):
        path.chmod(existing_mode | stat.S_IWRITE)


def reload_hpackage(hpackage):
    importlib.invalidate_caches()
    module_names = [
        module_name for module_name in sys.modules
        if module_name == "hpackage" or module_name.startswith("hpackage.")]

    module_names.sort(key=hpackage_module_reload_key)

    for module_name in module_names:
        importlib.reload(sys.modules[module_name])


def hpackage_module_reload_key(module_name):
    # Reload the root package first to pick up package-level values such as
    # __version__, then core before modules that import it, then descendants
    # deepest-first.  That keeps "from ..." imports fresh before their parent
    # package rebinds them.
    if module_name == "hpackage":
        return (0, 0, "")
    if module_name == "hpackage.core":
        return (1, 0, "")
    return (2, -module_name.count("."), module_name)


def allow_hpackage_prereleases():
    value = os.environ.get(ALLOW_PRERELEASES_ENV_VAR, "")
    return value.strip().lower() not in FALSE_ENV_VALUES


def hpackage_version_at_least(version_str, required_version_str):
    try:
        return compare_hpackage_versions(version_str, required_version_str) >= 0
    except ValueError:
        return False


def compare_hpackage_versions(left_str, right_str):
    left_release, left_alpha = parse_hpackage_version(left_str)
    right_release, right_alpha = parse_hpackage_version(right_str)
    max_len = max(len(left_release), len(right_release))
    left_release += (0,) * (max_len - len(left_release))
    right_release += (0,) * (max_len - len(right_release))
    if left_release < right_release:
        return -1
    if left_release > right_release:
        return 1
    if left_alpha == right_alpha:
        return 0
    if left_alpha is None:
        return 1
    if right_alpha is None:
        return -1
    return -1 if left_alpha < right_alpha else 1


def parse_hpackage_version(version_str):
    match = re.match(r"^(\d+(?:\.\d+)*)(?:a(\d+))?$", version_str)
    if not match:
        raise ValueError(f"Invalid hpackage version: {version_str!r}")
    release_str, alpha_str = match.groups()
    return (
        tuple(int(segment) for segment in release_str.split(".")),
        int(alpha_str) if alpha_str is not None else None)


def onCreateInterface(parent=None):
    return BootstrapPanel(parent)]]></script>
    <includeInPaneTabMenu menu_position="40" create_separator="false"/>
    <help><![CDATA[]]></help>
  </interface>
</pythonPanelDocument>
