#!/usr/bin/env python

"""Graphical interface for Doorstop."""

import sys
from unittest.mock import Mock
try:  # pragma: no cover - not measurable
    import tkinter as tk
    from tkinter import ttk
    from tkinter import font, filedialog
except ImportError as _exc:  # pragma: no cover, not measurable
    sys.stderr.write("WARNING: {}\n".format(_exc))
    tk = Mock()  # pylint: disable=C0103
    ttk = Mock()  # pylint: disable=C0103
import os
import argparse
import functools
from itertools import chain
import logging


from doorstop import GUI, __project__, __version__
from doorstop.common import SHARED, WarningFormatter, DoorstopError
from doorstop.core import vcs
from doorstop.core import tree
from doorstop import settings


def main(args=None):
    """Process command-line arguments and run the program."""
    # Main parser
    parser = argparse.ArgumentParser(prog=GUI, description=__doc__, **SHARED)
    # Hidden argument to override the root sharing directory path
    parser.add_argument('-j', '--project', metavar="PATH",
                        help="path to the root of the project")

    # Parse arguments
    args = parser.parse_args(args=args)

    # Configure logging
    _configure_logging(args.verbose)

    # Run the program
    try:
        success = _run(args, os.getcwd(), parser.error)
    except KeyboardInterrupt:
        logging.debug("program interrupted")
        success = False
    if success:
        logging.debug("program exited")
    else:
        logging.debug("program exited with error")
        sys.exit(1)


def _configure_logging(verbosity=0):
    """Configure logging using the provided verbosity level (0+)."""
    # Configure the logging level and format
    if verbosity == 0:
        level = settings.VERBOSE_LOGGING_LEVEL
        default_format = settings.VERBOSE_LOGGING_FORMAT
        verbose_format = settings.VERBOSE_LOGGING_FORMAT
    else:
        level = settings.VERBOSE2_LOGGING_LEVEL
        default_format = settings.VERBOSE_LOGGING_FORMAT
        verbose_format = settings.VERBOSE_LOGGING_FORMAT

    # Set a custom formatter
    logging.basicConfig(level=level)
    formatter = WarningFormatter(default_format, verbose_format)
    logging.root.handlers[0].setFormatter(formatter)


def _run(args, cwd, error):
    """Start the GUI.

    @param args: Namespace of CLI arguments (from this module or the CLI)
    @param cwd: current working directory
    @param error: function to call for CLI errors

    """
    # Exit if tkinter is not available
    if isinstance(tk, Mock) or isinstance(ttk, Mock):
        return error("tkinter is not available")

    else:  # pragma: no cover, manual test

        root = tk.Tk()
        root.title("{} ({})".format(__project__, __version__))
        # TODO: set a minimum window size
        # root.minsize(1000, 600)

        # Start the application
        app = Application(root, cwd, args.project)
        app.mainloop()

        return True


# TODO: cleanup: remove this logging decorator when no longer used
def _log(func):  # pragma: no cover, manual test
    """Decorator for methods that should log calls."""
    @functools.wraps(func)
    def wrapped(self, *args, **kwargs):
        """Wrapped method to log name and arguments."""
        sargs = "{}, {}".format(', '.join(repr(a) for a in args),
                                ', '.join("{}={}".format(k, repr(v))
                                          for k, v in kwargs.items()))
        msg = "{}: {}".format(func.__name__, sargs.strip(", "))
        if not self.ignore:
            logging.critical(msg.strip())
        return func(self, *args, **kwargs)
    return wrapped


class Listbox2(tk.Listbox):  # pragma: no cover, manual test, pylint: disable=R0901,R0904

    """Listbox class with automatic width adjustment."""

    def autowidth(self, maxwidth=250):
        """Resize the widget width to fit contents."""
        fnt = font.Font(font=self.cget("font"))
        pixels = 0
        for item in self.get(0, "end"):
            pixels = max(pixels, fnt.measure(item))
        # bump listbox size until all entries fit
        pixels = pixels + 10
        width = int(self.cget("width"))
        for shift in range(0, maxwidth + 1, 5):
            if self.winfo_reqwidth() >= pixels:
                break
            self.config(width=width + shift)


class Application(ttk.Frame):  # pragma: no cover, manual test, pylint: disable=R0901,R0902,R0904

    """Graphical application for Doorstop."""

    def __init__(self, root, cwd, project):
        ttk.Frame.__init__(self, root)

        # Create Doorstop variables
        self.cwd = cwd
        self.tree = None
        self.document = None
        self.item = None
        self.index = None

        # Create string variables
        self.stringvar_project = tk.StringVar(value=project or '')
        self.stringvar_project.trace('w', self.display_tree)
        self.stringvar_document = tk.StringVar()
        self.stringvar_document.trace('w', self.display_document)
        self.stringvar_item = tk.StringVar()
        self.stringvar_item.trace('w', self.display_item)
        self.stringvar_text = tk.StringVar()
        self.stringvar_text.trace('w', self.update_item)
        self.intvar_active = tk.IntVar()
        self.intvar_active.trace('w', self.update_item)
        self.intvar_derived = tk.IntVar()
        self.intvar_derived.trace('w', self.update_item)
        self.intvar_normative = tk.IntVar()
        self.intvar_normative.trace('w', self.update_item)
        self.intvar_heading = tk.IntVar()
        self.intvar_heading.trace('w', self.update_item)
        self.stringvar_link = tk.StringVar()  # no trace event
        self.stringvar_ref = tk.StringVar()
        self.stringvar_ref.trace('w', self.update_item)
        self.stringvar_extendedkey = tk.StringVar()
        self.stringvar_extendedkey.trace('w', self.display_extended)
        self.stringvar_extendedvalue = tk.StringVar()
        self.stringvar_extendedvalue.trace('w', self.update_item)

        # Create widget variables
        self.combobox_documents = None
        self.listbox_outline = None
        self.text_items = None
        self.text_item = None
        self.listbox_links = None
        self.combobox_extended = None
        self.text_extendedvalue = None
        self.text_parents = None
        self.text_children = None

        # Initialize the GUI
        self.ignore = False  # flag to ignore internal events
        frame = self.init(root)
        frame.pack(fill=tk.BOTH, expand=1)

        # Start the application
        root.after(500, self.find)

    def init(self, root):  # pylint: disable=R0914
        """Initialize and return the main frame."""  # pylint: disable=C0301
        # Shared arguments
        width_outline = 20
        width_text = 40
        width_code = 30
        width_id = 10
        height_text = 10
        height_ext = 5
        height_code = 3

        # Shared keyword arguments
        kw_f = {'padding': 5}  # constructor arguments for frames
        kw_gp = {'padx': 2, 'pady': 2}  # grid arguments for padded widgets
        kw_gs = {'sticky': tk.NSEW}  # grid arguments for sticky widgets
        kw_gsp = dict(chain(kw_gs.items(), kw_gp.items()))  # grid arguments for sticky padded widgets

        # Shared style
        if sys.platform == 'darwin':
            size = 14
        else:
            size = 10
        normal = font.Font(family='TkDefaultFont', size=size)
        fixed = font.Font(family='Courier New', size=size)

        # Configure grid
        frame = ttk.Frame(root, **kw_f)
        frame.rowconfigure(0, weight=0)
        frame.rowconfigure(1, weight=1)
        frame.columnconfigure(0, weight=2)
        frame.columnconfigure(1, weight=1)
        frame.columnconfigure(2, weight=1)
        frame.columnconfigure(3, weight=2)

        # Create widgets
        def frame_project(root):
            """Frame for the current project."""
            # Configure grid
            frame = ttk.Frame(root, **kw_f)
            frame.rowconfigure(0, weight=1)
            frame.columnconfigure(0, weight=0)
            frame.columnconfigure(1, weight=1)
            frame.columnconfigure(2, weight=0)

            # Place widgets
            ttk.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
            ttk.Entry(frame, textvariable=self.stringvar_project).grid(row=0, column=1, **kw_gsp)
            ttk.Button(frame, text="...", command=self.browse).grid(row=0, column=2, **kw_gp)

            return frame

        def frame_tree(root):
            """Frame for the current document."""
            # Configure grid
            frame = ttk.Frame(root, **kw_f)
            frame.rowconfigure(0, weight=1)
            frame.columnconfigure(0, weight=0)
            frame.columnconfigure(1, weight=1)
            frame.columnconfigure(2, weight=0)

            # Place widgets
            ttk.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
            self.combobox_documents = ttk.Combobox(frame, textvariable=self.stringvar_document, state='readonly')
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
            ttk.Button(frame, text="New...", command=self.new).grid(row=0, column=2, **kw_gp)

            return frame

        def frame_document(root):
            """Frame for current document's outline and items."""
            # Configure grid
            frame = ttk.Frame(root, **kw_f)
            frame.rowconfigure(0, weight=0)
            frame.rowconfigure(1, weight=5)
            frame.rowconfigure(2, weight=0)
            frame.rowconfigure(3, weight=0)
            frame.rowconfigure(4, weight=1)
            frame.columnconfigure(0, weight=0)
            frame.columnconfigure(1, weight=0)
            frame.columnconfigure(2, weight=0)
            frame.columnconfigure(3, weight=0)
            frame.columnconfigure(4, weight=1)
            frame.columnconfigure(5, weight=1)

            def listbox_outline_listboxselect(event):
                """Callback for selecting an item."""
                widget = event.widget
                index = int(widget.curselection()[0])
                value = widget.get(index)
                self.stringvar_item.set(value)

            # Place widgets
            ttk.Label(frame, text="Outline:").grid(row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp)
            ttk.Label(frame, text="Items:").grid(row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp)
            self.listbox_outline = Listbox2(frame, width=width_outline, font=normal)
            self.listbox_outline.bind('<<ListboxSelect>>', listbox_outline_listboxselect)
            self.listbox_outline.grid(row=1, column=0, columnspan=4, **kw_gsp)
            self.text_items = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
            ttk.Button(frame, text="<", width=0, command=self.left).grid(row=2, column=0, sticky=tk.EW, padx=(2, 0))
            ttk.Button(frame, text="v", width=0, command=self.down).grid(row=2, column=1, sticky=tk.EW)
            ttk.Button(frame, text="^", width=0, command=self.up).grid(row=2, column=2, sticky=tk.EW)
            ttk.Button(frame, text=">", width=0, command=self.right).grid(row=2, column=3, sticky=tk.EW, padx=(0, 2))
            ttk.Button(frame, text="Add Item", command=self.add).grid(row=2, column=4, sticky=tk.W, **kw_gp)
            ttk.Button(frame, text="Remove Selected Item", command=self.remove).grid(row=2, column=5, sticky=tk.E, **kw_gp)
            ttk.Label(frame, text="Items Filter:").grid(row=3, column=0, columnspan=6, sticky=tk.W, **kw_gp)
            tk.Text(frame, height=height_code, width=width_code, wrap=tk.WORD, font=fixed).grid(row=4, column=0, columnspan=6, **kw_gsp)

            return frame

        def frame_item(root):
            """Frame for the currently selected item."""
            # Configure grid
            frame = ttk.Frame(root, **kw_f)
            frame.rowconfigure(0, weight=0)
            frame.rowconfigure(1, weight=4)
            frame.rowconfigure(2, weight=0)
            frame.rowconfigure(3, weight=1)
            frame.rowconfigure(4, weight=1)
            frame.rowconfigure(5, weight=1)
            frame.rowconfigure(6, weight=1)
            frame.rowconfigure(7, weight=0)
            frame.rowconfigure(8, weight=0)
            frame.rowconfigure(9, weight=0)
            frame.rowconfigure(10, weight=0)
            frame.rowconfigure(11, weight=4)
            frame.columnconfigure(0, weight=0, pad=kw_f['padding'] * 2)
            frame.columnconfigure(1, weight=1)
            frame.columnconfigure(2, weight=1)

            def text_item_focusout(event):
                """Callback for updating text."""
                widget = event.widget
                value = widget.get('1.0', 'end')
                self.stringvar_text.set(value)

            def text_extendedvalue_focusout(event):
                """Callback for updating extended attributes."""
                widget = event.widget
                value = widget.get('1.0', 'end')
                self.stringvar_extendedvalue.set(value)

            # Place widgets
            ttk.Label(frame, text="Selected Item:").grid(row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp)
            self.text_item = tk.Text(frame, width=width_text, height=height_text, wrap=tk.WORD, font=fixed)
            self.text_item.bind('<FocusOut>', text_item_focusout)
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
            ttk.Label(frame, text="Properties:").grid(row=2, column=0, sticky=tk.W, **kw_gp)
            ttk.Label(frame, text="Links:").grid(row=2, column=1, columnspan=2, sticky=tk.W, **kw_gp)
            ttk.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(row=3, column=0, sticky=tk.W, **kw_gp)
            self.listbox_links = tk.Listbox(frame, width=width_id, height=6)
            self.listbox_links.grid(row=3, column=1, rowspan=4, **kw_gsp)
            ttk.Entry(frame, width=width_id, textvariable=self.stringvar_link).grid(row=3, column=2, sticky=tk.EW + tk.N, **kw_gp)
            ttk.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(row=4, column=0, sticky=tk.W, **kw_gp)
            ttk.Button(frame, text="<< Link Item", command=self.link).grid(row=4, column=2, **kw_gp)
            ttk.Checkbutton(frame, text="Normative", variable=self.intvar_normative).grid(row=5, column=0, sticky=tk.W, **kw_gp)
            ttk.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(row=6, column=0, sticky=tk.W, **kw_gp)
            ttk.Button(frame, text=">> Unlink Item", command=self.unlink).grid(row=6, column=2, **kw_gp)
            ttk.Label(frame, text="External Reference:").grid(row=7, column=0, columnspan=3, sticky=tk.W, **kw_gp)
            ttk.Entry(frame, width=width_text, textvariable=self.stringvar_ref, font=fixed).grid(row=8, column=0, columnspan=3, **kw_gsp)
            ttk.Label(frame, text="Extended Attributes:").grid(row=9, column=0, columnspan=3, sticky=tk.W, **kw_gp)
            self.combobox_extended = ttk.Combobox(frame, textvariable=self.stringvar_extendedkey, font=fixed)
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
            self.text_extendedvalue = tk.Text(frame, width=width_text, height=height_ext, wrap=tk.WORD, font=fixed)
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)

            return frame

        def frame_family(root):
            """Frame for the parent and child document items."""
            # Configure grid
            frame = ttk.Frame(root, **kw_f)
            frame.rowconfigure(0, weight=0)
            frame.rowconfigure(1, weight=1)
            frame.rowconfigure(2, weight=0)
            frame.rowconfigure(3, weight=1)
            frame.columnconfigure(0, weight=1)

            # Place widgets
            ttk.Label(frame, text="Linked To:").grid(row=0, column=0, sticky=tk.W, **kw_gp)
            self.text_parents = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
            self.text_parents.grid(row=1, column=0, **kw_gsp)
            ttk.Label(frame, text="Linked From:").grid(row=2, column=0, sticky=tk.W, **kw_gp)
            self.text_children = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
            self.text_children.grid(row=3, column=0, **kw_gsp)

            return frame

        # Place widgets
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
        frame_document(frame).grid(row=1, column=0, **kw_gs)
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
        frame_family(frame).grid(row=1, column=3, **kw_gs)

        return frame

    def find(self):
        """Find the root of the project."""
        if not self.stringvar_project.get():
            try:
                path = vcs.find_root(self.cwd)
            except DoorstopError as exc:
                logging.error(exc)
            else:
                self.stringvar_project.set(path)

    @_log
    def browse(self):
        """Browse for the root of a project."""
        path = filedialog.askdirectory()
        logging.debug("path: {}".format(path))
        if path:
            self.stringvar_project.set(path)

    def display_tree(self, *_):
        """Display the currently selected tree."""
        # Set the current tree
        self.tree = tree.build(root=self.stringvar_project.get())
        logging.info("displaying tree...")

        # Display the documents in the tree
        values = [document.prefix_relpath for document in self.tree]
        self.combobox_documents['values'] = values

        # Select the first document
        self.combobox_documents.current(0)

    def display_document(self, *_):
        """Display the currently selected document."""
        # Set the current document
        index = self.combobox_documents.current()
        self.document = list(self.tree)[index]
        logging.info("displaying document {}...".format(self.document))

        # Display the items in the document
        self.listbox_outline.delete(0, tk.END)
        self.text_items.delete('1.0', 'end')
        for item in self.document.items:

            # Add the item to the document outline
            indent = '  ' * (item.depth - 1)
            level = '.'.join(str(l) for l in item.level)
            value = "{s}{l} {i}".format(s=indent, l=level, i=item.id)
            self.listbox_outline.insert(tk.END, value)

            # Add the item to the document text
            value = "{t} [{i}]\n\n".format(t=item.text or item.ref or '???',
                                           i=item.id)
            self.text_items.insert('end', value)
        self.listbox_outline.autowidth()

        # Select the first item
        self.listbox_outline.selection_set(self.index or 0)
        identifier = self.listbox_outline.selection_get()
        self.stringvar_item.set(identifier)  # manual call

    def display_item(self, *_):
        """Display the currently selected item."""
        self.ignore = True

        # Set the current item
        identifier = self.stringvar_item.get().rsplit(' ', 1)[-1]
        self.item = self.tree.find_item(identifier)
        self.index = self.listbox_outline.curselection()[0]
        logging.info("displaying item {}...".format(self.item))

        # Display the item's text
        self.text_item.replace('1.0', 'end', self.item.text)

        # Display the item's properties
        self.stringvar_text.set(self.item.text)  # manual call
        self.intvar_active.set(self.item.active)
        self.intvar_derived.set(self.item.derived)
        self.intvar_normative.set(self.item.normative)
        self.intvar_heading.set(self.item.heading)

        # Display the item's links
        self.listbox_links.delete(0, tk.END)
        for identifier in self.item.links:
            self.listbox_links.insert(tk.END, identifier)
        self.stringvar_link.set('')

        # Display the item's external reference
        self.stringvar_ref.set(self.item.ref)

        # Display the item's extended attributes
        values = self.item.extended
        self.combobox_extended['values'] = values or ['']
        self.combobox_extended.current(0)

        # Display the items this item links to
        self.text_parents.delete('1.0', 'end')
        for identifier in self.item.links:
            try:
                item = self.tree.find_item(identifier)
            except DoorstopError:
                text = "???"
            else:
                text = item.text or item.ref or '???'
                identifier = item.id
            chars = "{t} [{i}]\n\n".format(t=text, i=identifier)
            self.text_parents.insert('end', chars)

        # Display the items this item has links from
        self.text_children.delete('1.0', 'end')
        identifiers = self.item.find_rlinks(self.document, self.tree)[0]
        for identifier in identifiers:
            item = self.tree.find_item(identifier)
            text = item.text or item.ref or '???'
            identifier = item.id
            chars = "{t} [{i}]\n\n".format(t=text, i=identifier)
            self.text_children.insert('end', chars)

        self.ignore = False

    @_log
    def display_extended(self, *_):
        """Display the currently selected extended attribute."""
        self.ignore = True

        name = self.stringvar_extendedkey.get()
        logging.debug("displaying extended attribute '{}'...".format(name))
        self.text_extendedvalue.replace('1.0', 'end', self.item.get(name, ''))

        self.ignore = False

    def update_item(self, *_):
        """Update the current item from the fields."""
        if self.ignore:
            return

        # Update the current item
        logging.info("updating {}...".format(self.item))
        self.item.auto = False
        self.item.text = self.stringvar_text.get()
        self.item.active = self.intvar_active.get()
        self.item.derived = self.intvar_derived.get()
        self.item.normative = self.intvar_normative.get()
        self.item.heading = self.intvar_heading.get()
        self.item.links = self.listbox_links.get(0, tk.END)
        self.item.ref = self.stringvar_ref.get()
        name = self.stringvar_extendedkey.get()
        if name:
            self.item.set(name, self.stringvar_extendedvalue.get())
        self.item.save()

        # Re-select this item
        self.display_document()

    @_log
    def new(self):
        """Create a new document."""

    @_log
    def left(self):
        """Dedent the current item's level."""

    @_log
    def down(self):
        """Increment the current item's level."""

    @_log
    def up(self):  # pylint: disable=C0103
        """Decrement the current item's level."""

    @_log
    def right(self):
        """Indent the current item's level."""

    @_log
    def add(self):
        """Add a new item to the document."""

    @_log
    def remove(self):
        """Remove the selected item from the document."""

    def link(self):
        """Add the specified link to the current item."""
        # Add the specified link to the list
        identifier = self.stringvar_link.get()
        if identifier:
            self.listbox_links.insert(tk.END, identifier)
            self.stringvar_link.set('')

            # Update the current item
            self.update_item()

    def unlink(self):
        """Remove the currently selected link from the current item."""
        # Remove the selected link from the list
        index = self.listbox_links.curselection()
        self.listbox_links.delete(index)

        # Update the current item
        self.update_item()


if __name__ == '__main__':  # pragma: no cover - manual test
    main()
