__author__ = "Simon Nilsson; sronilsson@gmail.com"

import os
from tkinter import *
from typing import Optional, Union

import pandas as pd

from simba.mixins.config_reader import ConfigReader
from simba.mixins.pop_up_mixin import PopUpMixin
from simba.ui.px_to_mm_ui import GetPixelsPerMillimeterInterface
from simba.ui.tkinter_functions import (CreateLabelFrameWithIcon, Entry_Box,
                                        SimbaButton, SimBALabel,
                                        SimBASeperator)
from simba.utils.checks import (check_file_exist_and_readable, check_float,
                                check_if_dir_exists, check_int)
from simba.utils.enums import (ConfigKey, Dtypes, Formats, Keys, Links,
                               Options, TagNames)
from simba.utils.errors import InvalidInputError, PermissionError
from simba.utils.printing import log_event, stdout_success
from simba.utils.read_write import (find_files_of_filetypes_in_directory,
                                    get_video_meta_data, read_config_entry,
                                    read_frm_of_video, read_video_info_csv)

VALID_CLR = 'white'
INVALID_CLR = 'lightsalmon'


class VideoInfoTable(ConfigReader, PopUpMixin):
    """
    Create GUI that allows users to modify resolutions, fps, and pixels-per-mm
    interactively of videos within the SimBA project. Data is stored within the project_folder/logs/video_info.csv
    file in the SimBA project.

    :param Union[str, os.PathLike] config_path: path to SimBA project config file in Configparser format
    :param Union[str, os.PathLike] video_dir: Optional path to directory with video files. If None, then read from the SimBA project as dictated by project config. Default None.
    :param Optional[Union[str, os.PathLike]] video_info_path: Optional path to vide_info.csv file. If None, then read from the SimBA project as dictated by project config. Default None.

    ..seealso::
       `Tutorial <https://github.com/sgoldenlab/simba/blob/master/docs/Scenario1.md#step-3-set-video-parameters>`__.

    :example:
    >>> ui = VideoInfoTable(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini")
    >>> ui.run()
    """

    def __init__(self,
                 config_path: Union[str, os.PathLike],
                 video_dir: Optional[Union[str, os.PathLike]] = None,
                 video_info_path: Optional[Union[str, os.PathLike]] = None):

        ConfigReader.__init__(self, config_path=config_path, read_video_info=False)
        log_event(logger_name=str(__class__.__name__), log_type=TagNames.CLASS_INIT.value, msg=self.create_log_msg_from_init_args(locals=locals()))
        if video_dir is not None:
            check_if_dir_exists(in_dir=video_dir, source=self.__class__.__name__)
            self.video_dir = video_dir
        if video_info_path is not None:
            check_file_exist_and_readable(file_path=video_info_path)
            self.video_info_path = video_info_path
        if os.path.isfile(self.video_info_path):
            self.video_info_df = read_video_info_csv(self.video_info_path).reset_index(drop=True)
            self.prior_videos = list(self.video_info_df['Video'])
        else:
            self.video_info_df, self.prior_videos = None, []
        self.distance_mm = read_config_entry(self.config, ConfigKey.FRAME_SETTINGS.value, ConfigKey.DISTANCE_MM.value, Dtypes.FLOAT.value,0.00)
        self.distance_mm = 'None' if self.distance_mm == 0.0 else self.distance_mm
        self.video_paths = find_files_of_filetypes_in_directory(directory=self.video_dir, extensions=Options.ALL_VIDEO_FORMAT_OPTIONS.value, raise_error=True, as_dict=True)
        self.max_char_vid_name = len(max(self.video_paths.keys(), key=len))
        PopUpMixin.__init__(self, title="VIDEO INFO", size=(1550, 800), icon='video')

    def _check_float_bg(self, entry_box: Entry_Box, allow_blank: bool = False):
        allow_blank = allow_blank or getattr(entry_box, 'allow_blank', False)
        value = entry_box.entry_get
        if value.strip() == '' and allow_blank:
            entry_box.set_bg_clr(clr=VALID_CLR)
        else:
            bg_clr = VALID_CLR if check_float(value=value, name='', allow_zero=False, allow_negative=False, raise_error=False)[0] else INVALID_CLR
            entry_box.set_bg_clr(clr=bg_clr)

    def _check_int_bg(self, entry_box: Entry_Box, allow_blank: bool = False):
        allow_blank = allow_blank or getattr(entry_box, 'allow_blank', False)
        value = entry_box.entry_get
        if value.strip() == '' and allow_blank:
            entry_box.set_bg_clr(clr=VALID_CLR)
        else:
            bg_clr = VALID_CLR if check_int(value=value, name='', allow_zero=False, allow_negative=False, raise_error=False)[0] else INVALID_CLR
            entry_box.set_bg_clr(clr=bg_clr)


    def _get_video_table_rows(self):
        self.videos = {}
        padx = (0, 12)
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            self.videos[video_name] = {}
            row = cnt * 2 + 2 + 4
            if video_name in self.prior_videos:
                try:
                    prior_data = self.read_video_info(video_name=video_name, raise_error=False)[0].reset_index(drop=True).iloc[0].to_dict()

                    fps, width, height, distance, pixels_per_mm = prior_data['fps'], prior_data['Resolution_width'], prior_data['Resolution_height'], round(prior_data['Distance_in_mm'], 4), round(prior_data['pixels/mm'], 4)
                except:
                    fps, width, height = self.video_meta_data[video_name]['fps'], self.video_meta_data[video_name]['width'], self.video_meta_data[video_name]['height']
                    distance, pixels_per_mm = self.distance_mm, 'None'
            else:
                fps, width, height = self.video_meta_data[video_name]['fps'], self.video_meta_data[video_name]['width'], self.video_meta_data[video_name]['height']
                distance, pixels_per_mm = self.distance_mm, 'None'
            try:
                img = read_frm_of_video(video_path=video_path, frame_index=0, raise_error=False, size=(280, 120), keep_aspect_ratio=True)
            except:
                img = None
            self.videos[video_name]["video_idx_lbl"] = SimBALabel(parent=self.video_frm, txt=str(cnt+1), font=Formats.FONT_REGULAR_BOLD.value)
            self.videos[video_name]["video_name_lbl"] = SimBALabel(parent=self.video_frm, txt=video_name, font=Formats.FONT_REGULAR_BOLD.value, hover_img=img)
            self.videos[video_name]["video_name_w_ext"] = os.path.basename(video_path)
            self.videos[video_name]["video_fps_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=fps, entry_box_width=12, justify='center', trace=self._check_float_bg)
            self.videos[video_name]["video_width_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=width, entry_box_width=12, justify='center', validation='numeric', trace=self._check_int_bg)
            self.videos[video_name]["video_height_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=height, entry_box_width=12, justify='center', validation='numeric', trace=self._check_int_bg)
            self.videos[video_name]["video_known_distance_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=distance, entry_box_width=12, justify='center', trace=self._check_float_bg)
            self.videos[video_name]["find_dist_btn"] = SimbaButton(parent=self.video_frm, txt="CALCULATE DISTANCE", txt_clr='black', img='calipher', font=Formats.FONT_REGULAR_BOLD.value, cmd=lambda k=video_name: self._initate_find_distance(k), hover_font=Formats.FONT_REGULAR_BOLD.value)
            self.videos[video_name]["video_px_per_mm"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=pixels_per_mm, entry_box_width=12, justify='center', trace=self._check_float_bg)
            self.videos[video_name]["video_idx_lbl"].grid(row=row, column=0, sticky=NW, padx=padx)
            self.videos[video_name]["video_name_lbl"].grid(row=row, column=1, sticky=W, padx=padx)
            self.videos[video_name]["video_fps_eb"].grid(row=row, column=2, sticky=W, padx=padx)
            self.videos[video_name]["video_width_eb"].grid(row=row, column=3, sticky=W, padx=padx)
            self.videos[video_name]["video_height_eb"].grid(row=row, column=4, sticky=W, padx=padx)
            self.videos[video_name]["find_dist_btn"].grid(row=row, column=5, sticky=W, padx=padx)
            self.videos[video_name]["video_known_distance_eb"].grid(row=row, column=6, sticky=W, padx=padx)
            self.videos[video_name]["video_px_per_mm"].grid(row=row, column=7, sticky=W, padx=padx)
            # Apply initial background from current value (e.g. None -> invalid color)
            self._check_float_bg(self.videos[video_name]["video_fps_eb"])
            self._check_int_bg(self.videos[video_name]["video_width_eb"])
            self._check_int_bg(self.videos[video_name]["video_height_eb"])
            self._check_float_bg(self.videos[video_name]["video_known_distance_eb"])
            self._check_float_bg(self.videos[video_name]["video_px_per_mm"])
            if cnt != len(self.video_paths.keys()) -1:
                sep = SimBASeperator(parent=self.video_frm, orient='horizontal', height=1, color="#ccc")
                sep.grid(row=row + 1, column=0, columnspan=15, sticky="ew")

    def _get_quick_set(self):
        self.quick_set_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="BATCH QUICK SET", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_PARAMETERS.value)
        self.quick_set_frm_known_distance_eb = Entry_Box(parent=self.quick_set_frm, fileDescription='KNOWN DISTANCE (MM):', labelwidth=30, value='', entry_box_width=10, img='distance_red', justify='center', tooltip_key='VIDEO_INFO_KNOWN_DISTANCE', trace=self._check_float_bg, allow_blank=True)
        self.quick_set_frm_known_distance_btn = SimbaButton(parent=self.quick_set_frm, txt="APPLY", txt_clr='black', img='tick', font=Formats.FONT_REGULAR_BOLD.value, cmd=self._set_known_distance, cmd_kwargs={'distance': lambda: self.quick_set_frm_known_distance_eb.entry_get})
        self.quick_set_px_mm_eb = Entry_Box(parent=self.quick_set_frm, fileDescription='PIXEL PER MILLIMETER:', labelwidth=30, value='', entry_box_width=10, img='ruler', justify='center', tooltip_key='VIDEO_INFO_PIXEL_PER_MM', trace=self._check_float_bg, allow_blank=True)
        self.quick_set_px_mm_btn = SimbaButton(parent=self.quick_set_frm, txt="APPLY", txt_clr='black', img='tick', font=Formats.FONT_REGULAR_BOLD.value, cmd=self._set_px_per_mm, cmd_kwargs={'px_per_mm': lambda: self.quick_set_px_mm_eb.entry_get})
        self.quick_set_frm.grid(row=1, column=0, sticky=NW)
        self.quick_set_frm_known_distance_eb.grid(row=0, column=0, sticky=NW)
        self.quick_set_frm_known_distance_btn.grid(row=0, column=1, sticky=NW)
        self.quick_set_px_mm_eb.grid(row=1, column=0, sticky=NW)
        self.quick_set_px_mm_btn.grid(row=1, column=1, sticky=NW)





    def _set_known_distance(self, distance: str):
        check_float(name=f'{self.__class__.__name__} KNOWN DISTANCE', value=distance, allow_negative=False, allow_zero=False, raise_error=True)
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            eb = self.videos[video_name]["video_known_distance_eb"]
            eb.entry_set(val=distance)
            self._check_float_bg(eb)

    def _set_px_per_mm(self, px_per_mm: str):
        check_float(name=f'{self.__class__.__name__} PIXEL PER MILLIMETER', value=px_per_mm, allow_negative=False, allow_zero=False,  raise_error=True)
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            eb = self.videos[video_name]["video_px_per_mm"]
            eb.entry_set(val=px_per_mm)
            self._check_float_bg(eb)

    def _get_video_meta_data(self):
        self.video_meta_data = {}
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            self.video_meta_data[video_name] = get_video_meta_data(video_path=video_path)

    def _duplicate_idx_1_distance(self):
        first_key = list(self.video_paths.keys())[0]
        first_val = self.videos[first_key]["video_known_distance_eb"].entry_get
        is_none_or_empty = first_val is None or (isinstance(first_val, str) and first_val.strip().lower() in ('', 'none'))
        if not is_none_or_empty and not check_float(name=self.__class__.__name__, value=first_val, allow_negative=False, allow_zero=False, raise_error=False)[0]:
            raise InvalidInputError(msg=f'Distance for {first_key} cannot be duplicated. Not a valid distance value: {first_val}', source=self.__class__.__name__)
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            eb = self.videos[video_name]["video_known_distance_eb"]
            eb.entry_set(val=first_val)
            self._check_float_bg(eb)

    def _duplicate_idx_1_px_mm(self):
        first_key = list(self.video_paths.keys())[0]
        first_val = self.videos[first_key]["video_px_per_mm"].entry_get
        is_none_or_empty = first_val is None or (isinstance(first_val, str) and first_val.strip().lower() in ('', 'none'))
        if not is_none_or_empty and not check_float(name=self.__class__.__name__, value=first_val, allow_negative=False, allow_zero=False, raise_error=False)[0]:
            raise InvalidInputError(msg=f'Pixel per millimeter for {first_key} cannot be duplicated. Not a valid distance value: {first_val}', source=self.__class__.__name__)
        for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
            eb = self.videos[video_name]["video_px_per_mm"]
            eb.entry_set(val=first_val)
            self._check_float_bg(eb)

    def _initate_find_distance(self, video_name: str):
        video_path = self.video_paths[video_name]
        known_distance = self.videos[video_name]['video_known_distance_eb'].entry_get
        check_float(name=f'{video_name} known distance', value=known_distance, allow_negative=False, allow_zero=False, raise_error=True)
        px_per_mm_interface = GetPixelsPerMillimeterInterface(video_path=video_path, known_metric_mm=float(known_distance))
        px_per_mm_interface.run()
        eb = self.videos[video_name]["video_px_per_mm"]
        eb.entry_set(str(round(px_per_mm_interface.ppm, 4)))
        self._check_float_bg(eb)


    def _get_instructions_frm(self):
        self.instructions_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="INSTRUCTIONS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_PARAMETERS.value)
        self.instructions_label_1 = SimBALabel(parent=self.instructions_frm, txt='1. Enter the known distance (mm) in "DISTANCE IN MM" column. Use "autopopulate" or "BATCH QUICK SET" for similar videos.', font=Formats.FONT_REGULAR_ITALICS.value)
        self.instructions_label_2 = SimBALabel(parent=self.instructions_frm, txt='2. Click on "CALCULATE DISTANCE" button(s) to calculate pixels/mm for each video.', font=Formats.FONT_REGULAR_ITALICS.value)
        self.instructions_label_3 = SimBALabel(parent=self.instructions_frm, txt="3. Click <SAVE PIXEL PER MILLIMETER DATA> when all the data are filled in.", font=Formats.FONT_REGULAR_ITALICS.value)
        self.instructions_frm.grid(row=0, column=0, sticky=W)
        self.instructions_label_1.grid(row=0, column=0, sticky=W)
        self.instructions_label_2.grid(row=1, column=0, sticky=W)
        self.instructions_label_3.grid(row=2, column=0, sticky=W)

    def _get_save_frm(self, index: int):
        self.save_frm = LabelFrame(self.main_frm, text="SAVE", font=Formats.FONT_HEADER.value)
        self.save_data_btn = SimbaButton(parent=self.save_frm, txt="SAVE PIXEL PER MILLIMETER DATA", txt_clr='black', img='save_large', font=Formats.FONT_HEADER.value, hover_font=Formats.FONT_HEADER.value, cmd=self._save)
        self.save_frm.grid(row=index, column=0, sticky=NW)
        self.save_data_btn.grid(row=0, column=0, sticky=NW)

    def _get_video_table(self):
        padx = (0, 12)
        self.video_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="PROJECT VIDEOS", icon_name='video_large', icon_link=Links.VIDEO_PARAMETERS.value, font=Formats.FONT_HEADER.value)

        idx_header = SimBALabel(parent=self.video_frm, txt="INDEX", font=Formats.FONT_HEADER.value, justify='center', img='list', tooltip_key='VIDEO_INFO_HEADER_INDEX')
        video_name_header = SimBALabel(parent=self.video_frm, txt="VIDEO NAME", font=Formats.FONT_HEADER.value, justify='center', img='video_2', tooltip_key='VIDEO_INFO_HEADER_VIDEO_NAME')
        fps_header = SimBALabel(parent=self.video_frm, txt="FPS", font=Formats.FONT_HEADER.value, justify='center', img='camera', tooltip_key='VIDEO_INFO_HEADER_FPS')
        width_header = SimBALabel(parent=self.video_frm, txt="VIDEO WIDTH", font=Formats.FONT_HEADER.value, justify='center', img='width', tooltip_key='VIDEO_INFO_HEADER_VIDEO_WIDTH')
        height_header = SimBALabel(parent=self.video_frm, txt="VIDEO HEIGHT", font=Formats.FONT_HEADER.value, justify='center', img='height', tooltip_key='VIDEO_INFO_HEADER_VIDEO_HEIGHT')
        find_dist_header = SimBALabel(parent=self.video_frm, txt="FIND DISTANCE", font=Formats.FONT_HEADER.value, justify='center', tooltip_key='VIDEO_INFO_HEADER_FIND_DISTANCE')
        dist_header = SimBALabel(parent=self.video_frm, txt="DISTANCE IN MM", font=Formats.FONT_HEADER.value, justify='center', img='distance_red', tooltip_key='VIDEO_INFO_HEADER_DISTANCE_IN_MM')
        px_mm_header = SimBALabel(parent=self.video_frm, txt="PIXELS PER MM", font=Formats.FONT_HEADER.value, justify='center', img='ruler', tooltip_key='VIDEO_INFO_HEADER_PIXELS_PER_MM')
        seperator = SimBASeperator(parent=self.video_frm, color=None, orient='horizontal', borderwidth=1)

        seperator.grid(row=1, column=0, columnspan=15, rowspan=1, sticky="ew")
        px_mm_header.grid(row=0, column=7, sticky=W)
        dist_header.grid(row=0, column=6, sticky=W, padx=padx)
        find_dist_header.grid(row=0, column=5, sticky=W, padx=padx)
        height_header.grid(row=0, column=4, sticky=W, padx=padx)
        width_header.grid(row=0, column=3, sticky=W, padx=padx)
        fps_header.grid(row=0, column=2, sticky=W, padx=padx)
        video_name_header.grid(row=0, column=1, sticky=W, padx=padx)
        idx_header.grid(row=0, column=0, sticky=NW, padx=padx)

        self.video_frm.grid(row=3, column=0, sticky=W)
        self._get_video_table_rows()

    def _get_duplicate_btns(self):
        self.duplicate_distance_btn = SimbaButton(parent=self.video_frm, txt="DUPLICATE INDEX 1", txt_clr='red', img='danger', font=Formats.FONT_REGULAR_ITALICS.value, cmd=self._duplicate_idx_1_distance, tooltip_key='VIDEO_INFO_DUPLICATE_DISTANCE')
        self.duplicate_distance_btn.grid(row=2, column=6, sticky=W, padx=12)
        self.duplicate_btn = SimbaButton(parent=self.video_frm, txt="DUPLICATE INDEX 1", txt_clr='red', img='danger', font=Formats.FONT_REGULAR_ITALICS.value, cmd=self._duplicate_idx_1_px_mm, tooltip_key='VIDEO_INFO_DUPLICATE_PX_PER_MM')
        self.duplicate_btn.grid(row=2, column=7, sticky=W, padx=12)
        sep = SimBASeperator(parent=self.video_frm, orient='horizontal', height=2, color="#ccc")
        sep.grid(row=3, column=0, columnspan=15, sticky="ew")

    def _save(self):
        self.video_info_df = pd.DataFrame(columns=Formats.EXPECTED_VIDEO_INFO_COLS.value)
        for video_name, video_path in self.videos.items():
            fps = self.videos[video_name]["video_fps_eb"].entry_get
            width = self.videos[video_name]["video_width_eb"].entry_get
            height = self.videos[video_name]["video_height_eb"].entry_get
            known_distance = self.videos[video_name]["video_known_distance_eb"].entry_get
            px_per_mm = self.videos[video_name]["video_px_per_mm"].entry_get
            check_float(name=f'{video_name} fps', value=fps, allow_zero=False, allow_negative=False,)
            check_int(name=f'{video_name} width', value=width, min_value=1, raise_error=True)
            check_int(name=f'{video_name} height', value=height, min_value=1, raise_error=True)
            check_float(name=f'{video_name} known_distance', value=known_distance, allow_zero=False, allow_negative=False, raise_error=True)
            check_float(name=f'{video_name} px_per_mm', value=px_per_mm, allow_zero=False, allow_negative=False, raise_error=True)
            self.video_info_df.loc[len(self.video_info_df)] = [video_name, fps, width, height, known_distance, px_per_mm]
        self.video_info_df.drop_duplicates(subset=["Video"], inplace=True)
        self.video_info_df = self.video_info_df.set_index("Video")
        try:
            self.video_info_df.to_csv(self.video_info_path)
        except PermissionError:
            raise PermissionError(msg=f"SimBA tried to write to {self.video_info_path}, but was not allowed. If this file is open in another program, try closing it.", source=self.__class__.__name__)
        stdout_success(msg=f"Video info saved at {self.video_info_path}", source=self.__class__.__name__)

    def run(self):
        self._get_video_meta_data()
        self._get_instructions_frm()
        self._get_save_frm(index=2)
        self._get_quick_set()
        self._get_video_table()
        self._get_duplicate_btns()
        self._get_save_frm(index=4)


# ui = VideoInfoTable(config_path=r"E:\troubleshooting\mitra_pbn\mitra_pbn\project_folder\project_config.ini")
# ui.run()



# test.create_window()
#test.main_frm.mainloop()

# test = VideoInfoTable(config_path='/Users/simon/Desktop/troubleshooting/train_model_project/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()

#
# test = VideoInfoTable(config_path='/Users/simon/Desktop/envs/troubleshooting/sleap_5_animals_2/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()

# test = VideoInfoTable(config_path='/Users/simon/Desktop/envs/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()
