from importlib import resources

from enaml.application import deferred_call
from enaml.core.api import Looper
from enaml.icon import Icon, IconImage
from enaml.image import Image
from enaml.layout.api import align, hbox, InsertTab, RemoveItem, spacer, TabLayout, vbox
from enaml.stdlib.fields import FloatField
from enaml.stdlib.message_box import critical, information, question
from enaml.qt.QtCore import Qt
from enaml.widgets.api import (Action, ButtonGroup, CheckBox, Container,
                               DockArea, DockItem, Feature, FileDialogEx, Form,
                               HGroup, Html, Label, MainWindow, Menu, MenuBar,
                               MPLCanvas, ObjectCombo, ProgressBar, PushButton,
                               Slider, Window)

from cochleogram import plot, util
from cochleogram.model import Cochlea
from cochleogram.presenter import Presenter


def load_icon(name):
    data = resources.files('cochleogram.icons').joinpath(f'{name}.png').read_bytes()
    img = Image(data=data)
    icg = IconImage(image=img)
    return Icon(images=[icg])


def bind(figure, presenter):
    figure.canvas.mpl_connect('key_press_event', lambda e: presenter.key_press(e))
    figure.canvas.mpl_connect('button_press_event', lambda e: presenter.button_press(e))
    figure.canvas.mpl_connect('button_release_event', lambda e: presenter.button_release(e))
    figure.canvas.mpl_connect('scroll_event', lambda e: presenter.scroll(e))
    figure.canvas.mpl_connect('motion_notify_event', lambda e: presenter.motion(e))


def open_processed_dataset(window):
    # Check for unsaved changes
    if any(p.unsaved_changes for p in window.presenters):
        q = 'There are unsaved changes. Your current analysis will be lost. Are you sure?'
        button = question(None, 'Confirm action', q)
        if button is None or button.text == 'No':
            return

    path = FileDialogEx.get_existing_directory(window, current_path=window.current_path)
    if path:
        load_processed_dataset(path, window)


def load_processed_dataset(path, window):
    workspace = window.find('dock_area')
    # Remove existing cochlea
    for p in window.presenters:
        remove_dock_item(workspace, p)

    # Now, load new cochlea
    window.current_path = path
    window.cochlea = Cochlea.from_path(path)
    window.presenters = [Presenter(p) for p in window.cochlea.pieces]
    target = 'help'
    for p in window.presenters:
        try:
            p.load_state()
        except IOError:
            pass
        target = add_dock_item(workspace, p, target)


def add_dock_item(dock_area, presenter, target):
    item = MPLDockItem(dock_area, name=f'dock_piece_{presenter.piece.name}', presenter=presenter)
    op = InsertTab(item=item.name, target=target)
    dock_area.update_layout(op)
    return item.name


def remove_dock_item(dock_area, presenter):
    op = RemoveItem(item=f'dock_piece_{presenter.piece.name}')
    dock_area.update_layout(op)


def save_state(parent, presenters):
    q = 'Your previous analysis will be overwritten. Are you sure?'
    button = question(parent, 'Confirm action', q)
    if button is not None and button.text == 'Yes':
        for presenter in presenters:
            presenter.save_state()
        information(parent, 'Analysis saved', 'Analysis has been saved.')


def load_state(parent, presenters):
    if any(p.unsaved_changes for p in presenters):
        q = 'There are unsaved changes. Your current analysis will be lost. Are you sure?'
        button = question(parent, 'Confirm action', q)
        if button is None or button.text == 'No':
            return
    for presenter in presenters:
        try:
            presenter.load_state()
            information(parent, 'Analysis loaded', 'Analysis has been loaded.')
        except Exception as e:
            information(parent, 'Error', str(e))


CELLS = ['IHC', 'OHC1', 'OHC2', 'OHC3', 'Extra']


class CochleogramCanvas(MPLCanvas):

    def set_focus(self, event=None):
        self.figure.canvas.setFocus(Qt.OtherFocusReason)


def bind_focus(items, cb):
    observe = ['toggled', 'selected', 'value', 'clicked']
    for item in items:
        for o in observe:
            if hasattr(item, o):
                item.observe(o, cb)
        if getattr(item, 'children', []):
            bind_focus(item.children, cb)


enamldef MPLDockItem(DockItem):

    attr presenter
    closable = False

    title << 'Piece {}{}'.format(presenter.piece.piece, '*' if presenter.unsaved_changes else '')

    initialized ::
        bind_focus(container.children, canvas.set_focus)
        deferred_call(canvas.set_focus)

    Container: container:
        constraints = [
            vbox(
                hbox(display_label, display_channel, display_as_label,
                     display_mode, z_slice_number_label, z_slice,
                     display_apply, highlight_selected),
                hbox(tool_label, mode_buttons, spacer, load_analysis, save_analysis),
                hbox(action_label, action_clear_spiral, action_clear_cells,
                     action_simplify_exclusions, action_merge_ohc_exclusions,
                     spacer, spacing=0),
                action_guess_cells,
                action_copy_exclusion,
                canvas,
            ),
            align('v_center', display_label, display_channel, display_as_label,
                  display_mode, z_slice_number_label, z_slice, display_apply,
                  highlight_selected),
            align('v_center', tool_label, mode_buttons),
            align('v_center', action_label, action_clear_spiral,
                  action_clear_cells, action_simplify_exclusions,
                  action_merge_ohc_exclusions),
            align('left', display_label, tool_label, action_label),
            align('left', display_channel, mode_buttons, action_clear_spiral,
                  action_guess_cells, action_copy_exclusion),
            guess_width.width == 50,
            guess_spacing.width == 50,
        ]

        Label: display_label:
            text = 'Display'

        ObjectCombo: display_channel:
            items = ['All'] + presenter.piece.channel_names
            selected << 'All' if presenter.current_artist is None else presenter.current_artist.display_channel
            selected ::
                presenter.set_display_channel(selected, display_apply.checked)

        Label: display_as_label:
            text = 'as'

        ObjectCombo: display_mode:
            items = ['projection', 'slice']
            selected << 'projection' if presenter.current_artist is None else presenter.current_artist.display_mode
            selected ::
                presenter.set_display_mode(selected, display_apply.checked)

        Label: z_slice_number_label:
            text = 'number'
            enabled << display_mode.selected == 'slice'

        Slider: z_slice:
            enabled << display_mode.selected == 'slice'
            tick_interval = 1
            value << 0 if presenter.current_artist is None else presenter.current_artist.z_slice
            value ::
                presenter.set_z_slice(value, display_apply.checked)
            minimum << presenter.z_min if presenter.current_artist is None else presenter.current_artist.z_slice_min
            maximum << presenter.z_max if presenter.current_artist is None else presenter.current_artist.z_slice_max

        PushButton: display_apply:
            text = 'Apply to all tiles'
            checkable = True
            checked = True
            clicked ::
                if checked:
                    presenter.set_display_mode(display_mode.selected, True)
                    presenter.set_display_channel(display_channel.selected, True)

        PushButton: highlight_selected:
            text = 'Highlight selected'
            checkable = True
            checked := presenter.highlight_selected

        ButtonGroup: mode_group:
            pass

        ButtonGroup: tool_group:
            pass

        Label: tool_label:
            text = 'Tool'

        HGroup: mode_buttons:
            padding = 0
            spacing = 0
            align_widths = False
            trailing_spacer = spacer(0)

            PushButton:
                icon = load_icon('tile')
                icon_size = (20, 20)
                tool_tip = 'tile'
                group = tool_group
                checkable = True
                checked << presenter.tool == 'tile'
                toggled ::
                    if self.checked:
                        presenter.tool = 'tile'

            ObjectCombo:
                items = CELLS
                selected := presenter.cells

            Looper:
                iterable = ['spiral', 'exclude', 'cells']
                PushButton:
                    icon = load_icon(loop_item)
                    icon_size = (20, 20)
                    tool_tip = loop_item
                    enabled << not (presenter.cells == 'Extra' and loop_item != 'cells')
                    group = tool_group
                    checkable = True
                    checked << loop_item == presenter.tool
                    toggled ::
                        if self.checked:
                            presenter.tool = loop_item

        Label: action_label:
            text = 'Actions'

        PushButton: action_clear_spiral:
            text = 'Clear spiral'
            enabled << presenter.current_spiral_artist is not None and presenter.current_spiral_artist.has_nodes
            clicked ::
                button = question(self, 'Confirm action', 'Are you sure you want to remove the current spiral?')
                if button is not None and button.text == 'Yes':
                    presenter.action_clear_spiral()

        PushButton: action_clear_cells:
            text = 'Clear all cells'
            enabled << presenter.current_cells_artist is not None and presenter.current_cells_artist.has_nodes
            clicked ::
                button = question(self, 'Confirm action', 'Are you sure you want to remove the current cells?')
                if button is not None and button.text == 'Yes':
                    presenter.action_clear_cells()

        PushButton: action_simplify_exclusions:
            text = 'Simplify exclusions'
            enabled << presenter.current_spiral_artist is not None and presenter.current_spiral_artist.has_exclusion
            clicked ::
                m = f'Are you sure you want to simplify the excluded regions?'
                button = question(self, 'Confirm action', m)
                if button is not None and button.text == 'Yes':
                    try:
                        presenter.action_simplify_exclusion(presenter.cells)
                    except Exception as e:
                        information(self, 'Error', str(e))

        PushButton: action_merge_ohc_exclusions:
            text = 'Combine OHC exclusions'
            enabled << presenter.point_artists['OHC1', 'spiral'].has_spline and \
                presenter.point_artists['OHC2', 'spiral'].has_spline and \
                presenter.point_artists['OHC3', 'spiral'].has_spline and (\
                    presenter.point_artists['OHC1', 'spiral'].has_exclusion or \
                    presenter.point_artists['OHC2', 'spiral'].has_exclusion or \
                    presenter.point_artists['OHC3', 'spiral'].has_exclusion \
                                                                          )
            clicked ::
                m = f'Are you sure you want to merge all OHC excluded regions together?\n' \
                    'This will also simplify the excluded regions.'
                button = question(self, 'Confirm action', m)
                if button is not None and button.text == 'Yes':
                    try:
                        presenter.action_merge_exclusion('OHC1', 'OHC2', 'OHC3')
                    except Exception as e:
                        information(self, 'Error', str(e))

        PushButton: load_analysis:
            text = 'Load'
            clicked ::
                load_state(self, [presenter])

        PushButton: save_analysis:
            text = 'Save'
            clicked ::
                save_state(self, [presenter])

        HGroup: action_guess_cells:
            padding = 0
            align_widths = False
            share_layout = True
            trailing_spacer = spacer(0)
            enabled << presenter.current_spiral_artist is not None and presenter.current_spiral_artist.has_spline
            Label:
                text = 'Find cells within'
            FloatField: guess_width:
                value = 2.5
            Label:
                text = 'μm of spline with a spacing of'
            FloatField: guess_spacing:
                value = 5.0
            Label:
                text = 'μm using'
            ObjectCombo: guess_channel:
                items = presenter.piece.channel_names
                selected << items[0]

            PushButton:
                text = 'update'
                clicked ::
                    button = question(self, 'Confirm action', f'Are you sure you want to overwrite the existing cells?')
                    if button is not None and button.text == 'Yes':
                        n = presenter.action_guess_cells(guess_width.value,
                                                         guess_spacing.value,
                                                         guess_channel.selected)
                        information(self, 'Info', f'Found {n} cells')

        HGroup: action_copy_exclusion:
            padding = 0
            align_widths = False
            trailing_spacer = spacer(0)
            enabled << presenter.current_spiral_artist is not None and presenter.current_spiral_artist.has_exclusion
            Label:
                text = 'Copy excluded regions to'
            ObjectCombo: copy_exclusion_to:
                items << [c for c in CELLS if c != presenter.cells]
            PushButton:
                text = 'Copy'
                clicked ::
                    m = f'Are you sure you want to copy the excluded regions to {copy_exclusion_to.selected}?\n' \
                        f'The exclusion regions will be merged with any already-defined regions on {copy_exclusion_to.selected}.'
                    button = question(self, 'Confirm action', m)
                    if button is not None and button.text == 'Yes':
                        try:
                            presenter.action_copy_exclusion(copy_exclusion_to.selected)
                        except Exception as e:
                            information(self, 'Error', str(e))

        CochleogramCanvas: canvas:
            figure << presenter.figure
            toolbar_visible = False
            initialized ::
                deferred_call(bind, figure, presenter)


enamldef ProgressWindow(Window): window:

    title = 'Processing ...'
    modality = 'application_modal'
    always_on_top = True
    alias progress: pb.value

    Container:
        ProgressBar: pb:
            pass

        PushButton:
            text = 'Ok'
            enabled << (pb.value == pb.maximum)
            clicked ::
                window.close()


enamldef CochleagramWindow(MainWindow): window:

    initial_size = (900, 900)
    title = 'Cochelogram'
    icon = load_icon('main-icon')

    attr cochlea
    attr presenters = []
    attr current_path = ''

    MenuBar:
        Menu:
            title = '&File'
            Action:
                text = 'Open processed dataset\tCtrl+O'
                triggered ::
                    open_processed_dataset(window)

            Action:
                text = 'Process LIF file\tCtrl+P'
                triggered ::
                    path = FileDialogEx.get_open_file_name(window, current_path=window.current_path)
                    if path:
                        pp = ProgressWindow()
                        pp.show()
                        cb = lambda x, pp=pp: setattr(pp, 'progress', x)
                        deferred_call(util.process_lif, path, reprocess=True, cb=cb)

            Action:
                text = 'Save analysis\tCtrl+S'
                triggered ::
                    save_state(window, window.presenters)

            Action:
                text = 'Load analysis\tCtrl+L'
                triggered ::
                    load_state(window, window.presenters)

            Action:
                separator = True

            Action:
                text = 'Generate frequency map\tCtrl+F'
                enabled << cochlea is not None
                triggered ::
                    try:
                        fig = plot.frequency_map(cochlea)
                        fig.suptitle(cochlea.name)
                        fig.savefig(cochlea.path / f'{cochlea.name}_frequency_map.pdf')
                        information(window, 'Frequency map', 'Frequency map has been generated.')
                    except Exception as e:
                        critical(window, 'Frequency map', str(e))

    initialized ::
        for presenter in presenters:
            deferred_call(add_dock_item, workspace, presenter)

    closing ::
        if any(p.unsaved_changes for p in presenters):
            button = question(window, 'Question', 'There are unsaved changes. Are you sure you want to exit?')
            if button is None or button.text == 'No':
                change['value'].ignore()

    Container:
        DockArea: workspace:
            name = 'dock_area'

            layout = TabLayout('help')

            DockItem:
                name = 'help'
                title = 'Help'
                closable = False

                Container:
                    Html:
                        source = resources.files('cochleogram') \
                            .joinpath('instructions.html') \
                            .read_text()
