import copy
import json

from maya import cmds

from ngSkinTools2.api import internals, plugin
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.pyside import QtCore
from ngSkinTools2.api.python_compatibility import Object

log = getLogger("api/paint")


# noinspection PyClassHasNoInit
class BrushProjectionMode(Object):
    surface = 0
    screen = 1


# noinspection PyClassHasNoInit
class PaintMode(Object):
    """
    Constants for paint mode
    """

    replace = 1
    add = 2
    scale = 3
    smooth = 4
    sharpen = 5

    @classmethod
    def all(cls):
        return cls.replace, cls.smooth, cls.add, cls.scale, cls.sharpen


# noinspection PyClassHasNoInit
class TabletMode(Object):
    unused = 0
    multiplyIntensity = 1
    multiplyOpacity = 2
    multiplyRadius = 3


# noinspection PyClassHasNoInit
class WeightsDisplayMode(Object):
    allInfluences = 0
    currentInfluence = 1
    currentInfluenceColored = 2


# noinspection PyClassHasNoInit
class MaskDisplayMode(Object):
    default_ = 0
    color_ramp = 1


# noinspection PyClassHasNoInit
class BrushShape(Object):
    solid = 0  # 1.0 for whole brush size
    smooth = 1  # feathered edges
    gaus = 2  # very smooth from center


# noinspection PyClassHasNoInit
class PaintModeSettings(Object):
    """
    Brush/Flood settings
    """

    __property_map = {
        "brush_projection_mode": 'brushProjectionMode',
        "mode": 'paintMode',
        "brush_radius": 'brushRadius',
        "brush_shape": 'brushShape',
        "intensity": 'brushIntensity',
        "iterations": 'brushIterations',
        "tablet_mode": 'tabletMode',
        "mirror": 'interactiveMirror',
        "influences_limit": 'influencesLimit',
        "fixed_influences_per_vertex": 'fixedInfluencesPerVertex',
        "limit_to_component_selection": 'limitToComponentSelection',
        "use_volume_neighbours": 'useVolumeNeighbours',
        "distribute_to_other_influences": 'redistributeRemovedWeight',
        "sample_joint_on_stroke_start": 'sampleJointOnStrokeStart',
    }

    mode = PaintMode.replace  #: Tool mode. One of the :py:class:`PaintMode` values.

    # varies by mode
    intensity = 1.0  #: tool intensity;
    iterations = 1
    """
     iterations; repeats the same smooth operation given number of times -
     using this parameter instead of calling `flood_weights` multiple times.
    """

    brush_shape = BrushShape.solid

    # varies by screen mode
    # can be modified inside the plugin
    brush_radius = 10

    mirror = False  #: is automatic mirroring on or off
    distribute_to_other_influences = False
    influences_limit = 0  #: influences limit per vertex to ensure while smoothing
    brush_projection_mode = BrushProjectionMode.surface  #: brush projection mode, one of :py:class:`BrushProjectionMode` values.
    sample_joint_on_stroke_start = False
    tablet_mode = TabletMode.unused
    use_volume_neighbours = False
    limit_to_component_selection = False
    fixed_influences_per_vertex = False
    """
    only applicable for smooth mode; when set to True, smoothing will not add additional influences to a vertex.
    """

    def apply_primary_brush(self):
        self.__apply(1)

    def apply_alternative_brush(self):
        self.__apply(2)

    def apply_inverted_brush(self):
        self.__apply(3)

    def __apply(self, settings_type):
        """
        apply settings to C++ plugin side.
        :param settings_type:
        """

        kwargs = {v: getattr(self, k) for k, v in self.__property_map.items()}
        kwargs['paintType'] = settings_type
        plugin.ngst2PaintSettingsCmd(**kwargs)

    def from_dict(self, values):
        # type: (dict) -> PaintModeSettings

        for k in self.__property_map.keys():
            if k in values:
                setattr(self, k, values[k])

        return self

    def to_dict(self):
        # type: () -> dict
        return {k: getattr(self, k) for k in self.__property_map.keys()}


def __make_common_property__(property_name):
    def setval(self, val):
        setattr(self.primary_settings, property_name, val)
        self.apply_settings()

    return property(lambda self: getattr(self.primary_settings, property_name), setval)


def __make_mode_property__(property_name):
    return __make_dimensional_property__(lambda self: self.mode, lambda self: self.mode_settings[self.mode], property_name)


def __make_projection_property__(property_name):
    return __make_dimensional_property__(
        lambda self: self.brush_projection_mode, lambda self: self.projection_settings[self.brush_projection_mode], property_name
    )


def __make_dimensional_property__(name, get_dimension_func, property_name):
    def setval(self, val):
        log.debug("setting dimensional property %s/%s: %r", name(self), property_name, val)
        dimension = get_dimension_func(self)
        dimension[property_name] = val
        setattr(self.primary_settings, property_name, val)
        self.apply_settings()

    def getval(self):
        return get_dimension_func(self).get(property_name, getattr(self.primary_settings, property_name))

    return property(getval, setval)


class PaintSettingsModel(Object):
    """
    Paint settings model manages paint settings persistence and storage on CPP side;
    in CPP side three states need to be maintained:
    * primary settings
    * alternative settings (used when shift is pressed)
    * inverse settings (used when ctrl is pressed)
    """

    projection_settings = None  # type: dict
    mode_settings = None  # type: dict
    primary_settings = None  # type: PaintModeSettings

    paint_mode = __make_common_property__("mode")
    mode = __make_common_property__("mode")

    intensity = __make_mode_property__("intensity")
    iterations = __make_mode_property__("iterations")
    brush_shape = __make_mode_property__("brush_shape")

    brush_radius = __make_projection_property__("brush_radius")

    mirror = __make_common_property__("mirror")
    distribute_to_other_influences = __make_common_property__("distribute_to_other_influences")
    influences_limit = __make_common_property__("influences_limit")
    brush_projection_mode = __make_common_property__("brush_projection_mode")
    sample_joint_on_stroke_start = __make_common_property__("sample_joint_on_stroke_start")
    tablet_mode = __make_common_property__("tablet_mode")
    use_volume_neighbours = __make_common_property__("use_volume_neighbours")
    limit_to_component_selection = __make_common_property__("limit_to_component_selection")
    fixed_influences_per_vertex = __make_common_property__("fixed_influences_per_vertex")

    def __init__(self):
        self.projection_settings = None
        self.mode_settings = None
        self.primary_settings = None
        self.storage_func_save = lambda data: None
        self.storage_func_load = lambda: ""
        self.apply_settings_func = self.apply_plugin_settings

        self.setup_maya_option_var_persistence()

    def __save_settings(self):
        data = {
            "common": self.primary_settings.to_dict(),
            "mode_settings": self.mode_settings,
            "projection_settings": self.projection_settings,
        }
        serialized_data = json.dumps(data)
        log.info("saving brush settings: %s", serialized_data)
        self.storage_func_save(serialized_data)

    def load_settings(self):
        def to_int_keys(d):
            return {int(k): v for k, v in d.items()}

        try:
            saved_data = self.storage_func_load()
            log.info("loading brush settings from %s", saved_data)
            if saved_data is None:
                self.initialize_defaults()
                return
            data = json.loads(saved_data)
            self.primary_settings = PaintModeSettings().from_dict(data['common'])
            self.mode_settings = to_int_keys(data['mode_settings'])
            self.projection_settings = to_int_keys(data['projection_settings'])
            self.apply_settings()
        except Exception as err:
            log.info(err)

    def setup_maya_option_var_persistence(self):
        from ngSkinTools2.ui import options

        val = options.PersistentValue(options.VAR_OPTION_PREFIX + "_brush_settings")

        self.storage_func_load = val.get
        self.storage_func_save = val.set
        self.load_settings()

    def __bake_settings(self, mode):
        result = copy.copy(self.primary_settings)
        result.mode = mode

        for k, v in self.mode_settings[mode].items():
            setattr(result, k, v)

        for k, v in self.projection_settings[self.brush_projection_mode].items():
            setattr(result, k, v)

        return result

    def apply_settings(self):
        # TODO: very inelegant here; we should not have to re-bake and re-set all settings at once
        # if we're switching any of dimensional settings, we need to reflect this
        self.primary_settings = self.__bake_settings(self.mode)
        primary = self.primary_settings

        inverted_modes = {
            PaintMode.replace: PaintMode.replace,
            PaintMode.add: PaintMode.scale,
            PaintMode.scale: PaintMode.add,
            PaintMode.smooth: PaintMode.sharpen,
            PaintMode.sharpen: PaintMode.smooth,
        }

        alternative = self.__bake_settings(PaintMode.smooth)
        inverted = self.__bake_settings(inverted_modes.get(self.mode, PaintMode.replace))

        if self.mode == PaintMode.replace:
            inverted.intensity = 0

        log.debug("normal mode intensity: %r", primary.intensity)
        self.apply_settings_func(primary, alternative, inverted)
        self.__save_settings()

    def initialize_defaults(self):
        self.primary_settings = PaintModeSettings()
        self.primary_settings.mode = PaintMode.replace
        self.primary_settings.brush_radius = 2
        self.primary_settings.intensity = 1.0
        self.primary_settings.tablet_mode = TabletMode.unused
        self.primary_settings.brush_shape = BrushShape.solid
        self.primary_settings.distribute_to_other_influences = False
        self.mode_settings = {
            PaintMode.replace: {
                "intensity": 1.0,
                "brush_shape": BrushShape.solid,
            },
            PaintMode.add: {
                "intensity": 0.1,
                "brush_shape": BrushShape.solid,
            },
            PaintMode.scale: {
                "intensity": 0.95,
                "brush_shape": BrushShape.solid,
            },
            PaintMode.smooth: {
                "intensity": 0.2,
                "iterations": 5,
                "brush_shape": BrushShape.smooth,
            },
            PaintMode.sharpen: {
                "intensity": 0.2,
                "iterations": 5,
                "brush_shape": BrushShape.smooth,
            },
        }
        self.projection_settings = {
            BrushProjectionMode.surface: {
                "brush_radius": 2,
            },
            BrushProjectionMode.screen: {
                "brush_radius": 100,
            },
        }
        self.apply_settings()

    # noinspection PyMethodMayBeStatic
    def apply_plugin_settings(self, primary, alternative, inverted):
        # type: (PaintModeSettings, PaintModeSettings, PaintModeSettings) -> None
        primary.apply_primary_brush()
        alternative.apply_alternative_brush()
        inverted.apply_inverted_brush()


class DisplaySettings(Object):
    def __init__(self):
        from ngSkinTools2.ui import options

        self.persistence = options.PersistentDict("paint_display_settings")

    weights_display_mode = internals.make_editable_property('weightsDisplayMode')
    mask_display_mode = internals.make_editable_property('maskDisplayMode')
    layer_effects_display = internals.make_editable_property('layerEffectsDisplay')
    display_masked = internals.make_editable_property('displayMasked')
    show_selected_verts_only = internals.make_editable_property('showSelectedVertsOnly')
    wireframe_color = internals.make_editable_property('wireframeColor')
    wireframe_color_single_influence = internals.make_editable_property('wireframeColorSingleInfluence')

    # noinspection PyMethodMayBeStatic
    def __edit__(self, **kwargs):
        plugin.ngst2PaintSettingsCmd(**kwargs)
        for k, v in kwargs.items():
            self.persistence[k] = v

    @property
    def display_node_visible(self):
        """
        gets/sets visibility of temporary node that displays weight colors. when set to false, displays original mesh instead.
        """
        return plugin.ngst2PaintSettingsCmd(q=True, displayNodeVisible=True)

    @display_node_visible.setter
    def display_node_visible(self, value):
        plugin.ngst2PaintSettingsCmd(displayNodeVisible=value)

    # noinspection PyMethodMayBeStatic
    def __query__(self, **kwargs):
        for k in kwargs:
            persisted = self.persistence[k]
            if persisted is not None:
                return persisted
        return plugin.ngst2PaintSettingsCmd(q=True, **kwargs)


class PaintTool(PaintSettingsModel):
    __paint_context = None

    def __init__(self):
        PaintSettingsModel.__init__(self)
        self.display_settings = DisplaySettings()

    def update_plugin_brush_radius(self):
        new_value = plugin.ngst2PaintSettingsCmd(q=True, brushRadius=True)
        if self.brush_radius != new_value:
            self.brush_radius = new_value

    def update_plugin_brush_intensity(self):
        new_value = plugin.ngst2PaintSettingsCmd(q=True, brushIntensity=True)
        if self.intensity != new_value:
            self.intensity = new_value

    @classmethod
    def start(cls):
        if cls.__paint_context is None:
            cls.__paint_context = plugin.ngst2PaintContext()
        cmds.setToolTo(cls.__paint_context)

    def flood(self, layer, influence=None, influences=None):
        from ngSkinTools2.api import tools

        tools.flood_weights(target=layer, influence=influence, influences=influences, settings=self.primary_settings)

    @classmethod
    def is_painting(cls):
        return cmds.contextInfo(cmds.currentCtx(), c=True) == 'ngst2PaintContext'


class Popups(Object):
    def __init__(self):
        self.windows = []

    def add(self, w):
        self.windows.append(w)
        w.destroyed.connect(lambda *args: self.remove(w))

    def remove(self, w):
        self.windows = [i for i in self.windows if i != w]

    def close_all(self):
        for i in self.windows:
            i.close()
        self.windows = []


popups = Popups()


class TabletEventFilter(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)
        self.pressure = 1.0

    def eventFilter(self, obj, event):
        if event.type() in [QtCore.QEvent.TabletPress, QtCore.QEvent.TabletMove]:
            self.pressure = event.pressure()
            # log.info("tablet pressure: %r", self.pressure)

        return QtCore.QObject.eventFilter(self, obj, event)

    def install(self):
        from ngSkinTools2.ui import qt

        log.info("installing event filter...")
        qt.mainWindow.installEventFilter(self)
        log.info("...done")

    def uninstall(self):
        from ngSkinTools2.ui import qt

        qt.mainWindow.removeEventFilter(self)
        log.info("event filter uninstalled")


tabletEventFilter = TabletEventFilter()
