"""
ModuleManager.py

An example showing data bound collection UI.  The module files on disk are
collected and managed by the ModuleManager class, which is purely functional and
has no UI.  

The UI contains a list which is bound to the ModuleManager's collection of
mofTuple objects; the list displays the available modules as widgets (created
using the the ModuleTemplate clas).  

The widgets are directly bound to the ModTuple variables; this means that
closing the dialog checks the state of the list and updates the files on disk as
needed. You'll notice there is no checking the state of the UI widgets: the
values inside the ModuleManager are updated by the bindings using the update_status method.

Some other points of note:

Tags
----
The widgets use the Tag property to make sure that each widget knows what
object to work on without extra lambdas or partials 

LayoutDialogForm
---------------
The root of the UI is a LayoutDialogForm;  a convenience class which takes the existing
formLayout created by Maya's layoutDialog command and wraps it with an mGui Layout so that
it can be edited like other mGui widgets.

BindingContext
--------------
The BindingContext in the UI catches all of the bindings created while it's active and updates
them automatically when it closes; this makes sure that all the UI elements get set properly
at creation time.

"""
import sys

import maya.cmds as cmds

import os
import subprocess
import webbrowser as wb
from mGui import gui, lists, forms
from mGui.bindings import bind, BindingContext


# Mod manager classes
# ==============================================================================================

class ModTuple(object):
    """
    Represents a module file on disk
    """

    def __init__(self, enabled, name, version, path, filename):
        self.enabled = enabled
        self.name = name
        self.version = version
        self.path = path
        self.file = filename


class ModuleManager(object):
    """
    Manages the list of .mod files on the local maya's MAYA_MODULE_PATH.

    Note this class is purely functional - UI is handled in the ModuleManagerDialog via binding.
    """

    def __init__(self):
        self.modules = {}
        self.refresh()

    def refresh(self):
        """
        Update the internal module list to reflect the state of files on disk
        """
        self.modules.clear()
        module_files = []
        module_paths = os.environ['MAYA_MODULE_PATH'].split(os.pathsep)
        for p in module_paths:
            try:
                module_files += [os.path.join(p, x).replace(os.sep, os.altsep or os.sep) for x in os.listdir(p) if
                                 x.lower()[-3:] == "mod"]
            except OSError:
                pass  # ignore bad paths
        for eachfile in module_files:
            for eachmod in self.parse_mod(eachfile):
                self.modules["{0.name} ({0.version})".format(eachmod)] = eachmod

    def parse_mod(self, modfile):
        """
        Yields a modtuple describing the supplied .mod file
        """
        with open(modfile, 'rt') as filehandle:
            for line in filehandle:
                if line.startswith(("+", "-")):
                    enable, name, version, path = self.parse_mod_entry(line)
                    yield ModTuple(enable == "+", name, version, path, modfile)

    def parse_mod_entry(self, line):
        """
        parses a line from a mod file describing a given mod
        module description format:
            enabled [LOCALE:val] [PLATFORM:plat] [MAYAVERSION:version] name version path

        Flags within [] are optional, and can be in any order.
        Currently all optional flags are ignored.
        """
        split_line = line.split(' ')
        enable = split_line.pop(0)
        path = split_line.pop()
        version = split_line.pop()
        name = split_line.pop()
        return enable, name, version, path

    def enable(self, modtuple):
        self._rewrite_mod(modtuple, '+')

    def disable(self, modtuple):
        self._rewrite_mod(modtuple, '-')

    def _rewrite_mod(self, modtuple, character):
        all_lines = []
        with open(modtuple.file, 'rt') as filehandle:
            for line in filehandle:
                if line.startswith(("+", "-")):
                    enable, name, version, path = self.parse_mod_entry(line)
                    if name == modtuple.name and version == modtuple.version:
                        line = character + line[1:]
                all_lines.append(line)
        try:
            with open(modtuple.file, 'wt') as filehandle:
                filehandle.write('\n'.join(all_lines))
        except IOError:
            pass

# GUI classes
# ======================================
class ModuleTemplate(lists.ItemTemplate):
    """
    Create a complex display widget for each ModTuple
    """

    def widget(self, item):
        with BindingContext() as bc:
            with forms.HorizontalExpandForm(height=60, margin=(12, 0), backgroundColor=(.2, .2, .2)) as root:
                with forms.VerticalExpandForm(width=60) as cbf:
                    enabled = gui.CheckBox(label='', tag=item, value=item.enabled)
                    enabled.bind.value > bind() > (item, 'enabled')
                    cbf.dock(enabled, left=20, top=10, bottom=40, right=5)
                with forms.FillForm(width=300) as path:
                    with gui.ColumnLayout() as cl:
                        display_name = gui.Text(font='boldLabelFont')
                        display_name.bind.label < bind() < (item, 'name')
                        path = gui.Text(font='smallObliqueLabelFont')
                        path.bind.label < bind() < (item, 'path')
                with gui.GridLayout(width=200, numberOfColumns=2) as btns:
                    edit = gui.Button(label='Edit', tag=item)
                    show = gui.Button(label='Show', tag=item)
        enabled.changeCommand += self.update_status
        show.command += self.show_item
        edit.command += self.edit

        return lists.Templated(item, root, edit=edit.command, show=show.command)

    @classmethod
    def update_status(cls, *args, **kwargs):
        kwargs['sender'].update_bindings()

    @classmethod
    def show_item(cls, *args, **kwargs):
        wb.open(os.path.dirname(kwargs['sender'].tag.file))

    @classmethod
    def edit(cls, *args, **kwargs):
        try:
            os.startfile(kwargs['sender'].tag.file)
        except OSError:
            if sys.platform.startswith('win'):
                subprocess.call(['notepad', kwargs['sender'].tag.file])
            else:
                subprocess.call(['vi', kwargs['sender'].tag.file])


class ModuleManagerDialog(object):
    """
    Module manager dialog, implemented via LayoutDialog
    """

    def __init__(self):
        self._manager = ModuleManager()
        self._manager.refresh()

    def _layout(self):
        with forms.LayoutDialogForm() as base:
            with BindingContext() as bc:
                with forms.VerticalThreePane(width=512, margin=(4, 4), spacing=(0, 8)) as main:
                    with forms.VerticalForm() as header:
                        gui.Text(label='Installed Modules')

                    with forms.FillForm() as body:
                        mod_list = lists.VerticalList(itemTemplate=ModuleTemplate)
                        mod_list.collection < bind() < (self._manager.modules, 'values')
                        # binds the 'values' method of the ModuleManager's modules{} dictionary

                    with forms.HorizontalStretchForm() as footer:
                        cancel = gui.Button(label='Cancel')
                        cancel.command += self._cancel
                        gui.Separator(style=None)
                        save = gui.Button(label='Save')
                        save.command += self._save

        base.fill(main, 5)
        mod_list.update_bindings()

    def _cancel(self, *args, **kwargs):
        cmds.layoutDialog(dismiss="dismiss")

    def _save(self, *args, **kwargs):
        for v in self._manager.modules.values():
            if v.enabled:
                self._manager.enable(v)
            else:
                self._manager.disable(v)
        cmds.layoutDialog(dismiss="OK")

    def show(self):
        self._manager.refresh()
        cmds.layoutDialog(ui=self._layout, title='Module editor')


if __name__ == '__main__':
    m = ModuleManagerDialog()
    m.show()