"""Module containing the logic for the DLApp."""

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from tkinter.font import Font

from os import path
from pprint import pformat
import webbrowser

from dlapp import create_from_csv_data
from dlapp import create_from_json_data
from dlapp import create_from_yaml_data
from dlapp.collection import Tabular

from dlapp import edition
from dlapp.config import Data

import platform


def get_relative_center_location(parent, width, height):
    """get relative a center location of parent window.

    Parameters
    ----------
    parent (tkinter): tkinter component instance.
    width (int): a width of a child window.
    height (int): a height of a child window..

    Returns
    -------
    tuple: x, y location.
    """
    pwh, px, py = parent.winfo_geometry().split('+')
    px, py = int(px), int(py)
    pw, ph = [int(i) for i in pwh.split('x')]

    x = int(px + (pw - width) / 2)
    y = int(py + (ph - height) / 2)
    return x, y


def create_msgbox(title=None, error=None, warning=None, info=None,
                  question=None, okcancel=None, retrycancel=None,
                  yesno=None, yesnocancel=None, **options):
    """create tkinter.messagebox
    Parameters
    ----------
    title (str): a title of messagebox.  Default is None.
    error (str): an error message.  Default is None.
    warning (str): a warning message. Default is None.
    info (str): an information message.  Default is None.
    question (str): a question message.  Default is None.
    okcancel (str): an ok or cancel message.  Default is None.
    retrycancel (str): a retry or cancel message.  Default is None.
    yesno (str): a yes or no message.  Default is None.
    yesnocancel (str): a yes, no, or cancel message.  Default is None.
    options (dict): options for messagebox.

    Returns
    -------
    any: a string or boolean result
    """
    if error:
        # a return result is a "ok" string
        result = messagebox.showerror(title=title, message=error, **options)
    elif warning:
        # a return result is a "ok" string
        result = messagebox.showwarning(title=title, message=warning, **options)
    elif info:
        # a return result is a "ok" string
        result = messagebox.showinfo(title=title, message=info, **options)
    elif question:
        # a return result is a "yes" or "no" string
        result = messagebox.askquestion(title=title, message=question, **options)
    elif okcancel:
        # a return result is boolean
        result = messagebox.askokcancel(title=title, message=okcancel, **options)
    elif retrycancel:
        # a return result is boolean
        result = messagebox.askretrycancel(title=title, message=retrycancel, **options)
    elif yesno:
        # a return result is boolean
        result = messagebox.askyesno(title=title, message=yesno, **options)
    elif yesnocancel:
        # a return result is boolean or None
        result = messagebox.askyesnocancel(title=title, message=yesnocancel, **options)
    else:
        # a return result is a "ok" string
        result = messagebox.showinfo(title=title, message=info, **options)

    return result


def set_modal_dialog(dialog):
    """set dialog to become a modal dialog

    Parameters
    ----------
    dialog (tkinter.TK): a dialog or window application.
    """
    dialog.transient(dialog.master)
    dialog.wait_visibility()
    dialog.grab_set()
    dialog.wait_window()


class Content:
    """Content class

    Attributes
    ----------
    data (str): a text.


    """
    def __init__(self, data='', filename='', filetype=''):
        self.case = 'file' if filename else 'data' if data else 'unknown'
        self.data = data
        self.filename = filename
        self.filetype = filetype
        self.ready = False
        self.query_obj = None
        self.process()

    @property
    def is_csv(self):
        """Check if filename or content is in csv format."""
        return self.filetype == 'csv'

    @property
    def is_json(self):
        """Check if filename or content is in json format."""
        return self.filetype == 'json'

    @property
    def is_yaml(self):
        """Check if filename or content is in yaml format."""
        return self.filetype in ['yaml', 'yml']

    @property
    def is_ready(self):
        """Check if content is ready to use."""
        return self.ready

    def process_filename(self):
        if self.filename:
            _, ext = path.splitext(self.filename)
            extension = ext[1:]
            ext = ext.lower()[1:]
            if ext in ['csv', 'json', 'yml', 'yaml']:
                ext = 'yaml' if ext in ['yml', 'yaml'] else ext
                self.filetype = ext
            else:
                if not ext:
                    message = ('Make sure to select file with '
                               'extension json, yaml, yml, or csv.')
                else:
                    fmt = ('Selecting file extension is {}.  Make sure it is '
                           'in form of json, yaml, yml, or csv.')
                    message = fmt.format(extension)

                title = 'File Extension'
                create_msgbox(title=title, warning=message)

            with open(self.filename, newline='') as stream:
                self.data = stream.read().strip()

                if not self.data:
                    message = 'This {} file is empty.'.format(self.filename)
                    title = 'File Extension'
                    create_msgbox(title=title, warnig=message)

    def process_data(self):
        if not self.data:
            if self.case != 'file':
                title = 'Empty Data'
                message = 'data is empty.'
                create_msgbox(title=title, warning=message)

            return

        if not self.filetype:
            if self.case != 'file':
                title = 'Unselecting File Extension'
                message = ('Need to check filetype radio button '
                           'such as json, yaml, or csv.')
                create_msgbox(title=title, warning=message)
                return

        if self.is_yaml:
            try:
                self.query_obj = create_from_yaml_data(self.data)
                self.ready = True
            except Exception as ex:
                title = 'Processing YAML Data'
                error = '{}: {}'.format(type(ex), ex)
                create_msgbox(title=title, error=error)
        elif self.is_json:
            try:
                self.query_obj = create_from_json_data(self.data)
                self.ready = True
            except Exception as ex:
                title = 'Processing JSON data'
                error = '{}: {}'.format(type(ex), ex)
                create_msgbox(title=title, error=error)
        elif self.is_csv:
            try:
                self.query_obj = create_from_csv_data(self.data)
                self.ready = True
            except Exception as ex:
                title = 'Processing CSV Data'
                error = '{}: {}'.format(type(ex), ex)
                create_msgbox(title=title, error=error)

    def process(self):
        """Analyze `self.filename` or `self.data` and
        assign equivalent `self.filetype`"""
        self.process_filename()
        self.process_data()


class Application:
    """A DLApp application class.

    Attributes
    ----------
    root (tkinter.Tk): a top tkinter app.
    content (Content): a Content instance.

    Methods
    -------
    build_menu() -> None
    run() -> None
    callback_file_open() -> None
    callback_help_documentation() -> None
    callback_help_view_licenses() -> None
    callback_help_about() -> None
    """

    browser = webbrowser

    def __init__(self):
        # support platform: macOS, Linux, and Window
        self.is_macos = platform.system() == 'Darwin'
        self.is_linux = platform.system() == 'Linux'
        self.is_window = platform.system() == 'Windows'

        # standardize tkinter widget for macOS, Linux, and Window operating system
        self.RadioButton = tk.Radiobutton if self.is_linux else ttk.Radiobutton
        self.CheckBox = tk.Checkbutton if self.is_linux else ttk.Checkbutton
        self.Label = ttk.Label
        self.Frame = ttk.Frame
        self.LabelFrame = ttk.LabelFrame
        self.Button = ttk.Button
        self.TextBox = ttk.Entry
        self.TextArea = tk.Text
        self.PanedWindow = ttk.PanedWindow

        self._base_title = 'DLApp {}'.format(edition)
        self.root = tk.Tk()
        self.root.geometry('800x600+100+100')
        self.root.minsize(200, 200)
        self.root.option_add('*tearOff', False)
        self.content = None

        self.paned_window = None
        self.text_frame = None
        self.entry_frame = None
        self.result_frame = None

        self.radio_btn_var = tk.StringVar()
        self.radio_btn_var.set(None)
        self.lookup_entry_var = tk.StringVar()
        self.select_entry_var = tk.StringVar()
        self.result = None

        self.input_textarea = None
        self.result_textarea = None
        self.csv_radio_btn = None
        self.json_radio_btn = None
        self.yaml_radio_btn = None

        self.set_title()
        self.build_menu()
        self.build_frame()
        self.build_textarea()
        self.build_entry()
        self.build_result()

    def set_title(self, widget=None, title=''):
        """Set a new title for tkinter widget.

        Parameters
        ----------
        widget (tkinter): a tkinter widget.
        title (str): a title.  Default is empty.
        """
        widget = widget or self.root
        btitle = self._base_title
        title = '{} - {}'.format(title, btitle) if title else btitle
        widget.title(title)

    def create_custom_label(self, parent, text='', link='',
                            increased_size=0, bold=False, underline=False,
                            italic=False):
        """create custom label

        Parameters
        ----------
        parent (tkinter): a parent of widget.
        text (str): a text of widget.
        link (str): a label hyperlink.
        increased_size (int): a increased size for font.
        bold (bool): True will set bold font.
        underline (bool): True will set to underline font.
        italic (bool): True will set to italic font.

        Returns
        -------
        tkinter.Label: a label widget.
        """

        def mouse_over(event):
            if 'underline' not in event.widget.font:
                event.widget.configure(
                    font=event.widget.font + ['underline'],
                    cursor='hand2'
                )

        def mouse_out(event):
            event.widget.config(
                font=event.widget.font,
                cursor='arrow'
            )

        def mouse_press(event):
            self.browser.open_new_tab(event.widget.link)

        style = ttk.Style()
        style.configure("Blue.TLabel", foreground="blue")
        if link:
            label = self.Label(parent, text=text, style='Blue.TLabel')
            label.bind('<Enter>', mouse_over)
            label.bind('<Leave>', mouse_out)
            label.bind('<Button-1>', mouse_press)
        else:
            label = self.Label(parent, text=text)
        font = Font(name='TkDefaultFont', exists=True, root=label)
        font = [font.cget('family'), font.cget('size') + increased_size]
        bold and font.append('bold')
        underline and font.append('underline')
        italic and font.append('italic')
        label.configure(font=font)
        label.font = font
        label.link = link
        return label

    def callback_file_open(self):
        """Callback for Menu File > Open."""
        filetypes = [
            ('JSON Files', '*json'),
            ('YAML Files', '*yaml'),
            ('YML Files', '*yml'),
            ('CSV Files', '*csv')
        ]
        filename = filedialog.askopenfilename(filetypes=filetypes)
        if filename:
            content = Content(filename=filename)
            if content.is_ready:
                self.set_title(title=filename)
                self.input_textarea.delete("1.0", "end")
                self.input_textarea.insert(tk.INSERT, content.data)
                self.radio_btn_var.set(content.filetype)

    def callback_help_documentation(self):
        """Callback for Menu Help > Getting Started."""
        self.browser.open_new_tab(Data.documentation_url)

    def callback_help_view_licenses(self):
        """Callback for Menu Help > View Licenses."""
        self.browser.open_new_tab(Data.license_url)

    def callback_help_about(self):
        """Callback for Menu Help > About"""

        about = tk.Toplevel(self.root)
        self.set_title(widget=about, title='About')
        width, height = 460, 460
        x, y = get_relative_center_location(self.root, width, height)
        about.geometry('{}x{}+{}+{}'.format(width, height, x, y))
        about.resizable(False, False)

        top_frame = self.Frame(about)
        top_frame.pack(fill=tk.BOTH, expand=True)

        paned_window = self.PanedWindow(top_frame, orient=tk.VERTICAL)
        paned_window.pack(fill=tk.BOTH, expand=True, padx=8, pady=12)

        # company
        frame = self.Frame(paned_window, width=450, height=20)
        paned_window.add(frame, weight=4)

        self.create_custom_label(
            frame, text=Data.main_app_text,
            increased_size=2, bold=True
        ).grid(row=0, column=0, columnspan=2, sticky=tk.W)

        # URL
        cell_frame = self.Frame(frame, width=450, height=5)
        cell_frame.grid(row=1, column=0, sticky=tk.W, columnspan=2)

        url = Data.repo_url
        self.Label(cell_frame, text='URL:').pack(side=tk.LEFT)

        self.create_custom_label(
            cell_frame, text=url, link=url
        ).pack(side=tk.LEFT)

        # dependencies
        self.create_custom_label(
            frame, text='Pypi.com Dependencies:', bold=True
        ).grid(row=2, column=0, sticky=tk.W)

        # compare_versions package
        self.create_custom_label(
            frame, text=Data.compare_versions_text,
            link=Data.compare_versions_link
        ).grid(row=3, column=0, padx=(20, 0), sticky=tk.W)

        # python-dateutil package
        self.create_custom_label(
            frame, text=Data.python_dateutil_text,
            link=Data.python_dateutil_link
        ).grid(row=4, column=0, padx=(20, 0), pady=(0, 10), sticky=tk.W)

        # PyYAML package
        self.create_custom_label(
            frame, text=Data.pyyaml_text,
            link=Data.pyyaml_link
        ).grid(row=3, column=1, padx=(20, 0), sticky=tk.W)

        # license textbox
        lframe = self.LabelFrame(
            paned_window, height=200, width=450,
            text=Data.license_name
        )
        paned_window.add(lframe, weight=7)

        width = 58 if self.is_macos else 51
        height = 18 if self.is_macos else 14 if self.is_linux else 15
        txtbox = self.TextArea(lframe, width=width, height=height, wrap='word')
        txtbox.grid(row=0, column=0, padx=5, pady=5)
        scrollbar = ttk.Scrollbar(lframe, orient=tk.VERTICAL, command=txtbox.yview)
        scrollbar.grid(row=0, column=1, sticky='nsew')
        txtbox.config(yscrollcommand=scrollbar.set)
        txtbox.insert(tk.INSERT, Data.license)
        txtbox.config(state=tk.DISABLED)

        # footer - copyright
        frame = self.Frame(paned_window, width=450, height=20)
        paned_window.add(frame, weight=1)

        self.Label(frame, text=Data.copyright_text).pack(side=tk.LEFT, pady=(10, 10))

        self.create_custom_label(
            frame, text=Data.company, link=Data.company_url
        ).pack(side=tk.LEFT, pady=(10, 10))

        self.Label(frame, text='.  All right reserved.').pack(side=tk.LEFT, pady=(10, 10))

        set_modal_dialog(about)

    def build_menu(self):
        """Build menubar for dlapp."""
        menu_bar = tk.Menu(self.root)
        self.root.config(menu=menu_bar)
        file = tk.Menu(menu_bar)
        help_ = tk.Menu(menu_bar)

        menu_bar.add_cascade(menu=file, label='File')
        menu_bar.add_cascade(menu=help_, label='Help')

        file.add_command(label='Open', command=lambda: self.callback_file_open())
        file.add_separator()
        file.add_command(label='Quit', command=lambda: self.root.quit())

        help_.add_command(label='Documentation',
                          command=lambda: self.callback_help_documentation())
        help_.add_command(label='View Licenses',
                          command=lambda: self.callback_help_view_licenses())
        help_.add_separator()
        help_.add_command(label='About', command=lambda: self.callback_help_about())

    def build_frame(self):
        """Build layout for DLApp."""
        self.paned_window = self.PanedWindow(self.root, orient=tk.VERTICAL)
        self.paned_window.pack(fill=tk.BOTH, expand=True)

        self.text_frame = self.Frame(
            self.paned_window, width=600, height=400, relief=tk.RIDGE
        )
        self.entry_frame = self.Frame(
            self.paned_window, width=600, height=100, relief=tk.RIDGE
        )
        self.result_frame = self.Frame(
            self.paned_window, width=600, height=100, relief=tk.RIDGE
        )
        self.paned_window.add(self.text_frame, weight=7)
        self.paned_window.add(self.entry_frame)
        self.paned_window.add(self.result_frame, weight=2)

    def build_textarea(self):
        """Build input text for DLApp."""

        self.text_frame.rowconfigure(0, weight=1)
        self.text_frame.columnconfigure(0, weight=1)
        self.input_textarea = self.TextArea(self.text_frame, width=20, height=5, wrap='none')
        self.input_textarea.grid(row=0, column=0, sticky='nswe')
        vscrollbar = ttk.Scrollbar(
            self.text_frame, orient=tk.VERTICAL, command=self.input_textarea.yview
        )
        vscrollbar.grid(row=0, column=1, sticky='ns')
        hscrollbar = ttk.Scrollbar(
            self.text_frame, orient=tk.HORIZONTAL, command=self.input_textarea.xview
        )
        hscrollbar.grid(row=1, column=0, sticky='ew')
        self.input_textarea.config(
            yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set
        )

    def build_entry(self):
        """Build input entry for DLApp."""
        def callback_run_btn():
            data = self.input_textarea.get('1.0', 'end').strip()
            filetype = self.radio_btn_var.get()
            lookup = self.lookup_entry_var.get()
            select = self.select_entry_var.get()

            content = Content(data=data, filetype=filetype)
            if not content.is_ready:
                return

            try:
                result = content.query_obj.find(lookup=lookup, select=select)
                self.result = result
                self.result_textarea.delete("1.0", "end")
                self.result_textarea.insert(tk.INSERT, str(result))

            except Exception as ex:
                title = 'Query Problem'
                error = '{}: {}'.format(type(ex).__name__, ex)
                create_msgbox(title=title, error=error)

        def callback_tabular_btn():
            data = self.input_textarea.get('1.0', 'end').strip()
            filetype = self.radio_btn_var.get()
            lookup = self.lookup_entry_var.get()
            select = self.select_entry_var.get()

            content = Content(data=data, filetype=filetype)
            if not content.is_ready:
                return

            try:
                result = content.query_obj.find(lookup=lookup, select=select)
                self.result = result
                tabular_obj = Tabular(self.result)

                if tabular_obj.is_tabular:
                    text = tabular_obj.get()
                else:
                    fmt = 'CANNOT convert to tabular format because {!r}\n{}\n{}'
                    pretty_text = pformat(self.result)
                    text = fmt.format(tabular_obj.failure, '-' * 40, pretty_text)

                self.result_textarea.delete("1.0", "end")
                self.result_textarea.insert(tk.INSERT, str(text))

            except Exception as ex:
                title = 'Query Problem'
                error = '{}: {}'.format(type(ex).__name__, ex)
                create_msgbox(title=title, error=error)

        def callback_clear_text_btn():
            self.input_textarea.delete("1.0", "end")
            self.result_textarea.delete("1.0", "end")
            self.radio_btn_var.set(None)
            self.lookup_entry_var.set('')
            self.select_entry_var.set('')
            self.result = None
            self.set_title()

        def callback_paste_text_btn():
            filetype = self.radio_btn_var.get()
            if filetype == 'None':
                title = 'Unselect CSV/JSON/YAML'
                message = 'Please select CSV, JSON, or YAML.'
                create_msgbox(title=title, warning=message)
                return

            try:
                data = self.root.clipboard_get()
                if data:
                    self.input_textarea.delete("1.0", "end")
                    # filetype = self.radio_btn_var.get()
                    self.content = Content(data=data, filetype=filetype)
                    if self.content.is_ready:
                        self.set_title(title='<<PASTE - Clipboard>>')
                        self.input_textarea.insert(tk.INSERT, data)
                        self.radio_btn_var.set(self.content.filetype)
            except Exception as ex:     # noqa
                title = 'Empty Clipboard',
                message = 'CAN NOT paste because there is no data in pasteboard.'
                create_msgbox(title=title, warning=message)

        def callback_clear_lookup_entry():
            self.lookup_entry_var.set('')

        def callback_clear_select_entry():
            self.select_entry_var.set('')

        width = 70 if self.is_macos else 79 if self.is_linux else 107
        x1 = 2 if self.is_linux else 0

        # frame for row 0
        frame = self.Frame(self.entry_frame, width=600, height=30)
        frame.grid(row=0, column=0, padx=10, pady=(4, 0), sticky=tk.W)

        # radio buttons
        self.csv_radio_btn = self.RadioButton(
            frame, text='csv', variable=self.radio_btn_var,
            value='csv'
        )
        self.csv_radio_btn.pack(side=tk.LEFT)

        self.json_radio_btn = self.RadioButton(
            frame, text='json', variable=self.radio_btn_var,
            value='json'
        )
        self.json_radio_btn.pack(side=tk.LEFT, padx=(x1, 0))

        self.yaml_radio_btn = self.RadioButton(
            frame, text='yaml', variable=self.radio_btn_var,
            value='yaml'
        )
        self.yaml_radio_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # open button
        open_file_btn = self.Button(frame, text='Open',
                                    command=self.callback_file_open)
        open_file_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # paste button
        paste_text_btn = self.Button(frame, text='Paste',
                                     command=callback_paste_text_btn)
        paste_text_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # clear button
        clear_text_btn = self.Button(frame, text='Clear',
                                     command=callback_clear_text_btn)
        clear_text_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # run button
        run_btn = self.Button(frame, text='Run',
                              command=callback_run_btn)
        run_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # pprint button
        tabular_btn = self.Button(frame, text='Tabular',
                                  command=callback_tabular_btn)
        tabular_btn.pack(side=tk.LEFT, padx=(x1, 0))

        # frame for row 1 & 2
        frame = ttk.Frame(self.entry_frame, width=600, height=30)
        frame.grid(row=1, column=0, padx=10, pady=(0, 4), sticky=tk.W)

        # lookup entry
        lbl = self.Label(frame, text='Lookup')
        lbl.grid(row=0, column=0, padx=(0, 4), pady=0, sticky=tk.W)
        lookup_entry = self.TextBox(frame, width=width,
                                    textvariable=self.lookup_entry_var)
        lookup_entry.grid(row=0, column=1, padx=0, pady=0, sticky=tk.W)
        lookup_entry.bind('<Return>', lambda event: callback_run_btn())

        # clear button
        clear_lookup_btn = self.Button(frame, text='Clear',
                                       command=callback_clear_lookup_entry)
        clear_lookup_btn.grid(row=0, column=2, padx=(x1, 0), pady=0, sticky=tk.W)

        # select statement entry
        lbl = self.Label(frame, text='Select')
        lbl.grid(row=1, column=0, padx=(0, 4), pady=0, sticky=tk.W)
        select_entry = self.TextBox(frame, width=width,
                                    textvariable=self.select_entry_var)
        select_entry.grid(row=1, column=1, padx=0, pady=0, sticky=tk.W)
        select_entry.bind('<Return>', lambda event: callback_run_btn())

        # clear button
        clear_select_btn = self.Button(frame, text='Clear',
                                       command=callback_clear_select_entry)
        clear_select_btn.grid(row=1, column=2, padx=(x1, 0), pady=0, sticky=tk.W)

    def build_result(self):
        """Build result text"""
        self.result_frame.rowconfigure(0, weight=1)
        self.result_frame.columnconfigure(0, weight=1)
        self.result_textarea = self.TextArea(
            self.result_frame, width=20, height=5, wrap='none'
        )
        self.result_textarea.grid(row=0, column=0, sticky='nswe')
        vscrollbar = ttk.Scrollbar(
            self.result_frame, orient=tk.VERTICAL,
            command=self.result_textarea.yview
        )
        vscrollbar.grid(row=0, column=1, sticky='ns')
        hscrollbar = ttk.Scrollbar(
            self.result_frame, orient=tk.HORIZONTAL,
            command=self.result_textarea.xview
        )
        hscrollbar.grid(row=1, column=0, sticky='ew')
        self.result_textarea.config(
            yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set
        )

    def run(self):
        """Launch DLApp."""
        self.root.mainloop()


def execute():
    """Launch DLApp."""
    app = Application()
    app.run()
