import logging
import os
import sys
import tkinter as tk
from tkinter import IntVar, Radiobutton, filedialog, messagebox, ttk
from typing import Dict

import numpy as np
import Pmw
from skimage.color import label2rgb
from skimage.measure import label

from dosma.cli import SUPPORTED_QUANTITATIVE_VALUES, SUPPORTED_SCAN_TYPES, parse_args
from dosma.core.io import format_io_utils as fio_utils
from dosma.core.orientation import AXIAL, CORONAL, SAGITTAL
from dosma.gui.dosma_gui import ScanReader
from dosma.gui.gui_utils.filedialog_reader import FileDialogReader
from dosma.gui.im_viewer import IndexTracker
from dosma.gui.preferences_viewer import PreferencesManager
from dosma.msk import knee

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk

matplotlib.use("TkAgg")
LARGE_FONT = ("Verdana", 12)

_logger = logging.getLogger(__name__)


class DosmaViewer(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        for F in (StartPage, DosmaFrame, PageThree, AnalysisFrame):
            frame = F(container, self)

            self.frames[F] = frame

            frame.grid(row=0, column=0, sticky="nsew")

        self.show_frame(StartPage)

        self.pref = PreferencesManager()

    def on_closing(self):
        if messagebox.askokcancel("Quit", "Do you want to quit?"):
            sys.exit()

    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()

    def show_preferences(self):
        self.pref.show_window(self)


class StartPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        # photo = tk.PhotoImage(file="./defaults/skel-rotate.gif")
        # label1 = tk.Label(image=photo)
        # label1.pack()

        label = tk.Label(self, text="Start Page", font=LARGE_FONT)
        label.pack(pady=10, padx=10)

        button2 = ttk.Button(self, text="Scan", command=lambda: controller.show_frame(DosmaFrame))
        button2.pack()

        button3 = ttk.Button(
            self, text="Knee Analysis", command=lambda: controller.show_frame(AnalysisFrame)
        )
        button3.pack()

        button3 = ttk.Button(
            self, text="Image Viewer", command=lambda: controller.show_frame(PageThree)
        )
        button3.pack()

        button3 = ttk.Button(
            self, text="Preferences", command=lambda: controller.show_preferences()
        )
        button3.pack()


class AnalysisFrame(tk.Frame):
    __TISSUES_KEY = "Tissues"
    __QUANTITATIVE_VALUES_KEY = "Quantitative values"
    __LOAD_PATH_KEY = "Load data"

    __PID_KEY = "pid"
    __MEDIAL_TO_LATERAL_ORIENTATION_KEY = "ml"

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.manager: Dict = {}
        self.gui_manager: Dict = {}
        self.balloon = Pmw.Balloon()

        self.__init_manager()

        self.__base_gui()
        self.preferences = PreferencesManager()
        self.file_dialog_reader = FileDialogReader()
        self.scan_reader = ScanReader(self)

        button1 = ttk.Button(self, text="Home", command=lambda: controller.show_frame(StartPage))
        button1.pack(anchor="se", side="right")

        button1 = ttk.Button(self, text="Run", command=lambda: self.execute())
        button1.pack(anchor="sw", side="left")

    def execute(self):
        try:
            load_path = self.manager[self.__LOAD_PATH_KEY].get()
            if not load_path:
                raise ValueError("Load path not defined")

            preferences_str = self.preferences.get_cmd_line_str().strip()

            tissue_str = ""
            for c, t in enumerate(self.manager[self.__TISSUES_KEY]):
                if t.get():
                    tissue_str += "--%s " % knee.SUPPORTED_TISSUES[c].STR_ID
            tissue_str = tissue_str.strip()

            if not tissue_str:
                raise ValueError("No tissues selected")

            qv_str = ""
            for c, qv in enumerate(self.manager[self.__QUANTITATIVE_VALUES_KEY]):
                if qv.get():
                    qv_str += "--%s " % SUPPORTED_QUANTITATIVE_VALUES[c].name.lower()
            qv_str = qv_str.strip()

            if not qv_str:
                raise ValueError("No quantitative values selected")

            pid = self.manager[self.__PID_KEY].get()
            medial_to_lateral = self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY].get()

            if not pid:
                raise ValueError("No PID was provided")

            # analysis string
            str_f = "--l %s %s knee %s --pid %s %s %s" % (
                load_path,
                preferences_str,
                tissue_str,
                pid,
                "--ml" if medial_to_lateral else "",
                qv_str,
            )
            str_f = str_f.strip()
            parse_args(str_f.split())
        except Exception as e:
            tk.messagebox.showerror(str(type(e)), e.__str__())

    def __init_manager(self):
        self.manager[self.__LOAD_PATH_KEY] = tk.StringVar()
        self.manager[self.__TISSUES_KEY] = [
            tk.BooleanVar() for i in range(len(knee.SUPPORTED_TISSUES))
        ]
        self.manager[self.__QUANTITATIVE_VALUES_KEY] = [
            tk.BooleanVar() for i in range(len(SUPPORTED_QUANTITATIVE_VALUES))
        ]

        self.manager[self.__PID_KEY] = tk.StringVar()
        self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY] = tk.BooleanVar()

    def __display_pid_info(self):
        hb = tk.Frame(self)
        hb.pack(side="top", anchor="nw")
        _label = tk.Label(hb, text=self.__PID_KEY.upper())
        _label.pack(side="left", anchor="w", pady=10)
        t = tk.Entry(hb, textvariable=self.manager[self.__PID_KEY])
        t.pack(side="left", anchor="w", pady=10)
        self.balloon.bind(_label, "Patient id")

    def __display_data_loader(self):
        hb = tk.Frame(self)

        filedialog = FileDialogReader(self.manager[self.__LOAD_PATH_KEY])
        b = tk.Button(
            hb,
            text=self.__LOAD_PATH_KEY,
            command=lambda fd=filedialog: self.manager[self.__LOAD_PATH_KEY].set(
                fd.get_save_dirpath()
            ),
        )
        b.pack(side="left", anchor="nw", pady=10)

        _label = tk.Label(hb, textvariable=self.manager[self.__LOAD_PATH_KEY])
        _label.pack(side="left", anchor="nw", pady=10)

        hb.pack(side="top", anchor="nw")

    def __display_multi_option(self, label, options_list, boolvar_list):
        hb = tk.Frame(self)
        _label = tk.Label(hb, text="%s:" % label)
        _label.pack(side="left", anchor="w")
        hb.pack(side="top", anchor="nw")
        frames = [tk.Frame(hb)] * (len(options_list) // 3 + 1)
        for ind, option in enumerate(options_list):
            f = frames[ind // 3]
            b = tk.Checkbutton(f, text=option, variable=boolvar_list[ind])
            b.pack(side="top", anchor="nw", pady=5)

        for f in frames:
            f.pack(side="left", anchor="nw")

        return hb

    def __display_tissues(self):
        tissue_names = [x.FULL_NAME for x in knee.SUPPORTED_TISSUES]
        _label = self.__display_multi_option(
            self.__TISSUES_KEY, tissue_names, self.manager[self.__TISSUES_KEY]
        )
        self.balloon.bind(_label, "Tissues to analyze")

    def __display_quant_vals(self):
        quantitative_value_names = [x.name for x in SUPPORTED_QUANTITATIVE_VALUES]
        _label = self.__display_multi_option(
            self.__QUANTITATIVE_VALUES_KEY,
            quantitative_value_names,
            self.manager[self.__QUANTITATIVE_VALUES_KEY],
        )
        self.balloon.bind(_label, "Quantitative values to analyze")

    def __display_knee_info(self):
        hb = tk.Frame(self)
        hb.pack(side="top", anchor="nw")
        _label = tk.Label(hb, text="Medial -> Lateral: ")
        _label.pack(side="left", anchor="w", pady=10)
        t = tk.Checkbutton(hb, variable=self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY])
        t.pack(side="left", anchor="w", pady=10)

        self.balloon.bind(_label, "Select if Dicoms proceed in medial->lateral direction")

    def __base_gui(self):
        self.__display_data_loader()
        self.__display_pid_info()
        self.__display_tissues()
        self.__display_knee_info()
        self.__display_quant_vals()


class DosmaFrame(tk.Frame):
    __SCAN_KEY = "Scan"
    __TISSUES_KEY = "Tissues"

    __DICOM_PATH_KEY = "Read dicoms"
    __LOAD_PATH_KEY = "Load data"

    __SAVE_PATH_KEY = "Save path"

    __DATA_KEY = "data"  # Track option menu for dicom/load path
    __DATA_PATH_KEY = "datapath"  # Track filepath associated with option menu

    __IGNORE_EXTENSION_KEY = "Ignore extension"

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        self.file_dialog_reader = FileDialogReader()

        self.manager: Dict = {}
        self.gui_manager: Dict = {}
        self.balloon = Pmw.Balloon()

        self.__init_manager()

        self.__base_gui()
        self.preferences = PreferencesManager()
        self.scan_reader = ScanReader(self)

        button1 = ttk.Button(self, text="Home", command=lambda: controller.show_frame(StartPage))
        button1.pack(anchor="se", side="right")

        button1 = ttk.Button(self, text="Run", command=lambda: self.execute())
        button1.pack(anchor="sw", side="left")

        self.InitUI()

    def execute(self):
        try:
            save_path = self.manager[self.__SAVE_PATH_KEY].get()
            if not save_path:
                raise ValueError("Save path not defined")

            action_str = self.scan_reader.get_cmd_line_str().strip()

            if not action_str:
                raise ValueError("No action selected")

            preferences_str = self.preferences.get_cmd_line_str().strip()

            source = "d"
            if self.manager[self.__DATA_KEY].get() == self.__LOAD_PATH_KEY:
                source = "l"

            tissue_str = ""
            for c, t in enumerate(self.manager[self.__TISSUES_KEY]):
                if t.get():
                    tissue_str += "--%s " % knee.SUPPORTED_TISSUES[c].STR_ID
            tissue_str = tissue_str.strip()

            if not tissue_str:
                raise ValueError("No tissues selected")

            ignore_ext = self.manager[self.__IGNORE_EXTENSION_KEY].get()

            str_f = "--%s %s --s %s %s %s %s %s %s" % (
                source,
                self.manager[self.__DATA_PATH_KEY].get(),
                save_path,
                preferences_str,
                "--ignore_ext" if ignore_ext else "",
                self.manager[self.__SCAN_KEY].get(),
                tissue_str,
                action_str,
            )

            _logger.info("CMD LINE INPUT: %s" % str_f)

            parse_args(str_f.split())
        except Exception as e:
            tk.messagebox.showerror(str(type(e)), e.__str__())

    def __init_manager(self):
        self.manager[self.__SCAN_KEY] = tk.StringVar()
        self.manager[self.__TISSUES_KEY] = [
            tk.BooleanVar() for i in range(len(knee.SUPPORTED_TISSUES))
        ]
        self.manager[self.__DATA_KEY] = tk.StringVar()
        self.manager[self.__DATA_PATH_KEY] = tk.StringVar()

        self.manager[self.__SCAN_KEY].trace_add("write", self.__on_scan_change)
        self.manager[self.__SAVE_PATH_KEY] = tk.StringVar()
        self.manager[self.__IGNORE_EXTENSION_KEY] = tk.BooleanVar()

    def __on_scan_change(self, *args):
        scan_id = self.manager[self.__SCAN_KEY].get()
        scan = None
        for x in SUPPORTED_SCAN_TYPES:
            if x.NAME == scan_id:
                scan = x

        self.scan_reader.load_scan(scan)

        assert scan is not None, "No scan selected"

    def __update_svar(self, *args):
        svar = self.manager[self.__DATA_PATH_KEY]
        selected_option = self.manager[self.__DATA_KEY].get()
        if selected_option == self.__DICOM_PATH_KEY:
            fp = self.file_dialog_reader.get_volume_filepath(
                selected_option, im_type=fio_utils.ImageDataFormat.dicom
            )
        elif selected_option == self.__LOAD_PATH_KEY:
            fp = self.file_dialog_reader.get_dirpath(selected_option)
        else:
            raise ValueError("%s key not found" % self.__DATA_KEY)

        if not fp:
            svar.set("")
            return

        svar.set(fp)

        if selected_option == self.__LOAD_PATH_KEY:
            self.manager[self.__SAVE_PATH_KEY].set(fp)

    def __display_data_loader(self):
        s_var = self.manager[self.__DATA_PATH_KEY]

        hb = tk.Frame(self)

        label = tk.Label(hb, text="Data source: ")
        label.pack(side="left", anchor="nw", pady=10)

        options = [self.__DICOM_PATH_KEY, self.__LOAD_PATH_KEY]
        menu = tk.OptionMenu(
            hb, self.manager[self.__DATA_KEY], *options, command=self.__update_svar
        )
        menu.pack(side="left", anchor="nw", pady=10)

        label = tk.Label(hb, textvariable=s_var)
        label.pack(side="left", anchor="nw", pady=10)

        hb.pack(side="top", anchor="nw")
        self.balloon.bind(hb, "Read dicoms or load data")

        hb = tk.Frame(self)

        # filedialog = FileDialogReader(self.manager[self.__SAVE_PATH_KEY])
        b = tk.Button(
            hb,
            text=self.__SAVE_PATH_KEY,
            command=lambda fd=self.file_dialog_reader: self.manager[self.__SAVE_PATH_KEY].set(
                fd.get_save_dirpath()
            ),
        )
        b.pack(side="left", anchor="nw", pady=10)

        label = tk.Label(hb, textvariable=self.manager[self.__SAVE_PATH_KEY])
        label.pack(side="left", anchor="nw", pady=10)

        hb.pack(side="top", anchor="nw")

        hb = tk.Frame(self)

        b = tk.Checkbutton(
            hb, text=self.__IGNORE_EXTENSION_KEY, variable=self.manager[self.__IGNORE_EXTENSION_KEY]
        )
        b.pack(side="left", anchor="nw", pady=10)
        self.balloon.bind(b, "Ignore '.dcm' extension when loading dicoms")

        hb.pack(side="top", anchor="nw")

    def __display_tissues(self):
        hb = tk.Frame(self)
        _label = tk.Label(hb, text="Tissues:")
        _label.pack(side="left", anchor="w")
        hb.pack(side="top", anchor="nw")
        frames = [tk.Frame(hb)] * (len(knee.SUPPORTED_TISSUES) // 3 + 1)
        for ind, tissue in enumerate(knee.SUPPORTED_TISSUES):
            f = frames[ind // 3]
            b = tk.Checkbutton(
                f, text=tissue.FULL_NAME, variable=self.manager[self.__TISSUES_KEY][ind]
            )
            b.pack(side="top", anchor="nw", pady=5)

        for f in frames:
            f.pack(side="left", anchor="nw")

        self.balloon.bind(_label, "Tissues to analyze")

    def __base_gui(self):
        self.__display_data_loader()
        self.__display_tissues()

        hb = tk.Frame(self)
        scan_label = tk.Label(hb, text="Scan:")
        scan_label.pack(side="left", anchor="nw", pady=10)
        options = [x.NAME for x in SUPPORTED_SCAN_TYPES]
        scan_dropdown = tk.OptionMenu(hb, self.manager[self.__SCAN_KEY], *options)
        scan_dropdown.pack(side="left", anchor="nw", pady=10)
        hb.pack(side="top", anchor="nw")

    def InitUI(self):
        self.text_box = tk.Text(self, wrap="word", height=11, width=50)
        self.text_box.pack(anchor="s", side="bottom")


class PageThree(tk.Frame):
    SUPPORTED_FORMATS = (("nifti files", "*.nii\.gz"), ("dicom files", "*.dcm"))  # noqa: W605
    __base_filepath = "../"

    _ORIENTATIONS = [("sagittal", SAGITTAL), ("coronal", CORONAL), ("axial", AXIAL)]

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self._im_display = None
        self.binding_vars: Dict = {}
        fig, ax = plt.subplots(1, 1)
        X = np.random.rand(20, 20, 40)

        self.tracker = IndexTracker(ax, X)

        canvas = FigureCanvasTkAgg(fig, self)
        canvas.draw()
        canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        canvas.mpl_connect("scroll_event", self.tracker.onscroll)

        toolbar = NavigationToolbar2Tk(canvas, self)
        toolbar.update()
        canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.im = None
        self.mask = None
        self._im_display = None

        button1 = ttk.Button(
            self, text="Back to Home", command=lambda: controller.show_frame(StartPage)
        )
        button1.pack(side=tk.BOTTOM, anchor="sw")

        button2 = ttk.Button(self, text="Load main image", command=self.load_volume_callback)
        button2.pack()

        button3 = ttk.Button(self, text="Load mask", command=self.load_mask_callback)
        button3.pack()

        self.init_reformat_display()

    def __reformat_callback(self, *args):
        self.im_update()

    def init_reformat_display(self):
        orientation_var = IntVar(0)
        orientation_var.trace_add("write", self.__reformat_callback)
        count = 0
        for text, _value in self._ORIENTATIONS:
            b = Radiobutton(self, text=text, variable=orientation_var, value=count)
            b.pack(side=tk.TOP, anchor="w")
            count += 1
        self._orientation = orientation_var

    def load_volume_callback(self):
        im = self.load_volume()
        if not im:
            return
        self.im = im
        self.mask = None

        self.im_update()

    def load_mask_callback(self):
        if not self.im:
            messagebox.showerror("Loading mask failed", "Main image must be loaded prior to mask")
            return

        mask = self.load_volume("Load mask")
        mask.reformat(self.im.orientation, inplace=True)
        try:
            self.__verify_mask_size(self.im.volume, mask.volume)
        except Exception as e:
            messagebox.showerror("Loading mask failed", str(e))
            return

        self.mask = mask
        self.im_update()

    def __verify_mask_size(self, im: np.ndarray, mask: np.ndarray):
        if mask.ndim != 3:
            raise ValueError("Dimension mismatch. Mask must be 3D")
        if im.shape != mask.shape:
            raise ValueError(
                "Dimension mismatch. Image of shape %s, but mask of shape %s"
                % (str(im.shape), str(mask.shape))
            )

    def im_update(self):
        orientation = self.orientation
        self.im.reformat(orientation, inplace=True)
        im = self.im.volume
        im = im / np.max(im)
        if self.mask:
            self.mask.reformat(orientation, inplace=True)
            label_image = label(self.mask.volume)
            im = self.__labeltorgb_3d__(im, label_image, 0.3)

        self.im_display = im

    def __labeltorgb_3d__(self, im: np.ndarray, labels: np.ndarray, alpha: float = 0.3):
        im_rgb = np.zeros(im.shape + (3,))  # rgb channel
        for s in range(im.shape[2]):
            im_slice = im[..., s]
            labels_slice = labels[..., s]
            im_rgb[..., s, :] = label2rgb(labels_slice, image=im_slice, bg_label=0, alpha=alpha)
        return im_rgb

    def load_volume(self, title="Select volume file(s)"):
        files = filedialog.askopenfilenames(initialdir=self.__base_filepath, title=title)
        if len(files) == 0:
            return

        filepath = files[0]
        self.__base_filepath = os.path.dirname(filepath)

        if filepath.endswith(".dcm"):
            filepath = os.path.dirname(filepath)

        im = fio_utils.generic_load(filepath, 1)

        return im

    @property
    def orientation(self):
        ind = self._orientation.get()
        return self._ORIENTATIONS[ind][1]

    @property
    def im_display(self):
        return self._im_display

    @im_display.setter
    def im_display(self, value):
        self._im_display = value
        self.tracker.x = self._im_display
