# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import importlib
import os
import sys

PYSIDE = None
is_py6 = False


def _import_binding(binding_name):
    # Import submodules explicitly.  A bare ``import PySide2/PySide6`` does
    # not guarantee that QtCore/QtGui/QtWidgets have already been attached to
    # the package; relying on Nuke to preload them makes startup order fragile.
    importlib.import_module(binding_name)
    QtCore = importlib.import_module(binding_name + ".QtCore")
    QtGui = importlib.import_module(binding_name + ".QtGui")
    if binding_name == "PySide":
        # Qt 4 / PySide 1 exposes widget classes from QtGui.  This path is
        # retained for standalone compatibility only; supported Nuke 13-17
        # use PySide2 or PySide6.
        QtWidgets = QtGui
    else:
        QtWidgets = importlib.import_module(binding_name + ".QtWidgets")
    Qt = QtCore.Qt
    return QtCore, QtGui, QtWidgets, Qt


_SUPPORTED_BINDINGS = ("PySide6", "PySide2", "PySide")
_KNOWN_QT_WRAPPERS = (
    "PySide6",
    "PyQt6",
    "PySide2",
    "PyQt5",
    "PySide",
    "PyQt4",
)


def _loaded_qt_wrappers():
    result = []
    for binding_name in _KNOWN_QT_WRAPPERS:
        if (
            binding_name in sys.modules
            or (binding_name + ".QtCore") in sys.modules
        ):
            result.append(binding_name)
    return result


def _loaded_bindings():
    return [
        binding_name
        for binding_name in _loaded_qt_wrappers()
        if binding_name in _SUPPORTED_BINDINGS
    ]


def _reject_incompatible_loaded_bindings(expected_binding, context):
    loaded = _loaded_qt_wrappers()
    incompatible = [
        binding_name
        for binding_name in loaded
        if binding_name != expected_binding
    ]
    if incompatible:
        raise ImportError(
            "Refusing to mix Qt bindings in {0}. Expected {1}; already loaded: "
            "{2}.".format(
                context,
                expected_binding,
                ", ".join(loaded),
            )
        )


def _nuke_major_version():
    nuke_module = sys.modules.get("nuke")
    if nuke_module is None:
        return 0

    try:
        return int(getattr(nuke_module, "NUKE_VERSION_MAJOR", 0) or 0)
    except (TypeError, ValueError):
        return 0


def _binding_candidates():
    """Return safe candidates for the current host process.

    Inside Nuke the Qt major is part of the host ABI, so trying another major
    after an import failure is unsafe even when a user-installed package is
    importable.  Outside Nuke, fallback remains useful for isolated tests and
    support utilities.
    """
    nuke_module = sys.modules.get("nuke")
    nuke_major = _nuke_major_version()
    if nuke_module is not None:
        if nuke_major >= 16:
            expected_binding = "PySide6"
        elif nuke_major >= 11:
            expected_binding = "PySide2"
        elif nuke_major:
            expected_binding = "PySide"
        else:
            expected_binding = None

        if expected_binding is not None:
            _reject_incompatible_loaded_bindings(
                expected_binding,
                "Nuke {0}".format(nuke_major),
            )
            return [expected_binding]

        loaded_wrappers = _loaded_qt_wrappers()
        loaded = [
            binding_name
            for binding_name in loaded_wrappers
            if binding_name in _SUPPORTED_BINDINGS
        ]
        if len(loaded) == 1 and len(loaded_wrappers) == 1:
            return loaded
        raise ImportError(
            "Unable to determine Nuke's Qt binding safely (Nuke major: {0!r}, "
            "loaded bindings: {1}).".format(
                getattr(nuke_module, "NUKE_VERSION_MAJOR", None),
                ", ".join(loaded_wrappers) or "none",
            )
        )

    force = os.environ.get("NCA_FORCE_PYSIDE", "").strip().lower()
    forced_binding = {
        "1": "PySide",
        "pyside": "PySide",
        "2": "PySide2",
        "pyside2": "PySide2",
        "6": "PySide6",
        "pyside6": "PySide6",
    }.get(force)
    if force:
        if forced_binding is None:
            raise ImportError(
                "Unsupported NCA_FORCE_PYSIDE value: {0!r}".format(force)
            )
        _reject_incompatible_loaded_bindings(
            forced_binding,
            "standalone forced-binding mode",
        )
        return [forced_binding]

    loaded_wrappers = _loaded_qt_wrappers()
    unsupported = [
        binding_name
        for binding_name in loaded_wrappers
        if binding_name not in _SUPPORTED_BINDINGS
    ]
    if unsupported:
        raise ImportError(
            "Refusing to mix PySide with already loaded Qt bindings: {0}.".format(
                ", ".join(loaded_wrappers)
            )
        )

    loaded = _loaded_bindings()
    if len(loaded) > 1:
        raise ImportError(
            "Multiple Qt bindings are already loaded: {0}".format(
                ", ".join(loaded)
            )
        )
    if loaded:
        return loaded

    return ["PySide6", "PySide2", "PySide"]


def _load_qt():
    candidates = _binding_candidates()

    last_error = None
    for binding_name in candidates:
        try:
            QtCore, QtGui, QtWidgets, Qt = _import_binding(binding_name)
            return binding_name, (binding_name == "PySide6"), QtCore, QtGui, QtWidgets, Qt
        except Exception as exc:
            last_error = exc

    raise ImportError(
        "Unable to import any supported Qt binding. "
        "Tried: {0}. Last error: {1!r}".format(", ".join(candidates), last_error)
    )


PYSIDE, is_py6, QtCore, QtGui, QtWidgets, Qt = _load_qt()

try:
    QAction = QtWidgets.QAction
except AttributeError:
    QAction = QtGui.QAction

try:
    QShortcut = QtWidgets.QShortcut
except AttributeError:
    QShortcut = QtGui.QShortcut

Signal = QtCore.Signal
Slot = QtCore.Slot
Property = QtCore.Property


def exec_dialog(dialog):
    fn = getattr(dialog, "exec", None) or getattr(dialog, "exec_", None)
    if fn is None:
        raise AttributeError("Dialog object has neither exec nor exec_")
    return fn()
