import os
import webbrowser
import pyperclip
import multiprocessing
import PySimpleGUI as sg
import traceback
import sys
from os.path import exists
from threading import Thread
from threading import Lock
from random import randint, randrange
from pydub import AudioSegment
from multiprocessing.pool import ThreadPool
from PyUtils.SkyAudioEncoder import SkyAudioEncoder
from PyUtils.MusicUtils import MusicUtils
from PyUtils.FileUtils import FileUtils
from PyUtils.FileUtils import Exts
from PyUtils.Functions import *
from PyUtils.Logger import Logger
from Gui.AudioData import AudioData
from Gui.ReportBatchCmd import ReportBatchCmd
from Gui.ReportAudioDetails import ReportAudioDetails
from Settings.AppInfo import AppInfo
from Settings.ProfileManager import ProfileManager

class AudioLogicLayer:

    STR_INFO_POPUP = "Info"
    STR_ERROR_POPUP = "Error"
    STR_CANCEL = "Cancel"
    STR_OK = "Ok"
    batchReportMutex = Lock()

    def __init__(self, app_dir):
        self.app = AppInfo(app_dir)
        self.profile_manager = ProfileManager(app_dir)
        self.encoder = SkyAudioEncoder(self.app.audio_encoder_dir)
        self.player = MusicUtils()
        self.console_output = "CK Audio Manager initialized...\n"
        self._console_has_change = True

    def generate_list_audio_data(self):
        """
        Generates the list of audio data used to fill the AudioWindow table.
        :return:
        """
        _log = Logger.get()
        _log.debug("-- generate_list_audio_data()")
        skyrim_files_path = self.profile_manager.profile_db_dir()
        comments_csv = self.app.settings_obj.comments_file
        actors_csv = self.app.settings_obj.actors_file
        scene_order_csv = self.app.settings_obj.scene_order_file
        list_audio_data = AudioData.generate_list_audio_data(skyrim_files_path, comments_csv, actors_csv, scene_order_csv)
        return list_audio_data

    def create_audio_details_report(self, list_audio_data, ask_popup=True):
        _log = Logger.get()
        _log.debug(" -- create_audio_details_report()")
        list_details = []
        item: AudioData
        for item in list_audio_data:
            status_dict = AudioLogicLayer.file_status_dic(item.file_path)
            details = ReportAudioDetails()
            # audio status
            details.mp3 = status_dict["mp3"]
            details.wav = status_dict["wav"]
            details.xwm = status_dict["xwm"]
            details.lip = status_dict["lip"]
            details.fuz = status_dict["fuz"]
            # dialog data
            details.quest_id = item.quest_id
            details.actor = item.actor_name
            details.file = item.file_name
            details.subtitle = item.subtitle
            details.file_path = item.file_path
            details.dialog_type = item.dialog_type
            details.emotion = item.emotion
            details.voice_type = item.voice_type
            details.topic_id = item.topic_id
            details.branch_id = item.branch_id
            details.scene_id = item.scene_id
            details.scene_phase = item.scene_phase
            list_details.append(details)
        # file_name = ReportAudioDetails.export_report(list_details, self.app)
        file_name = ReportAudioDetails.export_report(list_audio_details=list_details,
                                                     output_dir=self.profile_manager.profile_docgen_dir(),
                                                     app_name=self.app.app_name_LARGE,
                                                     url_github=self.app.url_github)
        _log.debug("file_name:" + str(file_name))
        _log.debug("Report generation finished.")
        self._console_add("Report generation finished.")
        if ask_popup:
            popup_ret = ""
            popup_text = "Report generation finished. Do you want to open it?"
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            url_report = "file:///" + os.path.realpath(file_name)
            if popup_ret != AudioLogicLayer.STR_CANCEL:
                webbrowser.open(url_report, new=2)

        return file_name

    @staticmethod
    def file_status_dic(sound_path: str):
        """
        Return a dict with the file status.
        :param sound_path:
        :return:
        """
        if sound_path == "":
            return ""
        file_status_dict = {
            "mp3": "missing",
            "wav": "missing",
            "xwm": "missing",
            "lip": "missing",
            "fuz": "missing"
        }
        mp3_file = FileUtils.change_ext(sound_path, Exts.EXT_MP3)
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        lip_file = FileUtils.change_ext(sound_path, Exts.EXT_LIP)
        fuz_file = FileUtils.change_ext(sound_path, Exts.EXT_FUZ)
        msg = ""
        # mp3
        if exists(mp3_file):
            file_status_dict["mp3"] = "ok"
        # wav
        if exists(wav_file):
            file_status_dict["wav"] = "ok"
        # xwm
        if exists(xwm_file):
            file_status_dict["xwm"] = "ok"
        # lip
        if exists(lip_file):
            file_status_dict["lip"] = "ok"
        # fuz
        if exists(fuz_file):
            file_status_dict["fuz"] = "ok"
        return file_status_dict

    @staticmethod
    def file_status(sound_path: str):
        """
        Return a string with the file status.
        :return:
        """
        # print("** sound_path:" + sound_path)
        if sound_path == "":
            return ""
        mp3_file = FileUtils.change_ext(sound_path, Exts.EXT_MP3)
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        lip_file = FileUtils.change_ext(sound_path, Exts.EXT_LIP)
        fuz_file = FileUtils.change_ext(sound_path, Exts.EXT_FUZ)
        msg = ""
        # mp3
        if exists(mp3_file):
            msg = " mp3[ok] "
        # wav
        if exists(wav_file):
            msg += " wav[ok] "
        else:
            msg += "wav[missing] "
        # xwm
        if exists(xwm_file):
            msg += "xmw[ok] "
        else:
            msg += "xmw[missing] "
        # lip
        if exists(lip_file):
            msg += "lip[ok] "
        else:
            msg += "lip[missing] "
        # fuz
        if exists(fuz_file):
            msg += "fuz[ok] "
        else:
            msg += "fuz[missing] "
        return msg

    def set_sound(self, sound_path: str):
        """
        Screen Element: Select row
        :param sound_path:
        :return:
        """
        _log = Logger.get()
        self._console_add("set_sound() sound_path: " + sound_path)
        _log.debug("-- set_sound() sound_path:" + sound_path)
        if sound_path.strip() == "":
            return
        ret_val = self.encoder.try_to_gen_wav(sound_path, force_generation=False)
        if ret_val == SkyAudioEncoder.RET_SUCCESS:
            wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
            self.player.set(wav_file)
        else:
            ret_msg = self.encoder.get_last_error()
            err_description = self.encoder.get_last_stdout()
            popup_text = "Error selecting track " + sound_path + ": " + ret_msg + "\n\n" + err_description
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title="Play Audio Error")
            self._console_add(popup_text)

    def play_sound(self, sound_path: str):
        """
        Screen Element: Play Button
        :param sound_path:
        :return:
        """
        _log = Logger.get()
        self._console_add("play_sound() sound_path: " + sound_path)
        _log.debug("-- play_sound() sound_path:" + sound_path)
        if sound_path == "":
            popup_text = "No sound file selected."
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
            self._console_add(popup_text)
            return
        ret_val = self.encoder.try_to_gen_wav(sound_path, force_generation=False)
        if ret_val == SkyAudioEncoder.RET_SUCCESS:
            wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
            self.player.play(wav_file)
        else:
            ret_msg = self.encoder.get_last_error()
            err_description = self.encoder.get_last_stdout()
            popup_text = "Error playing track " + sound_path + ": " + ret_msg + "\n\n" + err_description
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title="Play Audio Error")
            self._console_add(popup_text)

    def stop_sound(self):
        """
        Screen Element: Stop Button
        :return:
        """
        _log = Logger.get()
        self._console_add("stop_sound()")
        _log.debug("-- stop_sound()")
        self.player.stop()

    def pause_sound(self):
        """
        Screen Element: Pause Button
        :return:
        """
        _log = Logger.get()
        self._console_add("pause_sound()")
        _log.debug("-- stop_sound()")
        self.player.pause()

    def set_volume(self, volume: int):
        """
        Screen Element: Volume bar.
        :param volume:
        :return:
        """
        _log = Logger.get()
        self._console_add("set_volume() volume:" + str(volume))
        _log.debug("-- set_volume() " + str(volume))
        v_in = volume/100
        self.player.set_volume(v_in)

    def open_folder(self, sound_path: str):
        """
        Screen Element: Open Folder Button
        :param sound_path:
        :return:
        """
        _log = Logger.get()
        _log.debug("-- open_folder()")
        self._console_add("open_folder() sound_path:" + sound_path)
        mp3_file = FileUtils.change_ext(sound_path, Exts.EXT_MP3)
        lip_file = FileUtils.change_ext(sound_path, Exts.EXT_LIP)
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        fuz_file = FileUtils.change_ext(sound_path, Exts.EXT_FUZ)
        if exists(wav_file):
            FileUtils.open_file_on_file_explorer(wav_file)
            return
        if exists(xwm_file):
            FileUtils.open_file_on_file_explorer(xwm_file)
            return
        if exists(fuz_file):
            FileUtils.open_file_on_file_explorer(fuz_file)
            return
        if exists(lip_file):
            FileUtils.open_file_on_file_explorer(lip_file)
            return
        if exists(mp3_file):
            FileUtils.open_file_on_file_explorer(mp3_file)
            return
        popup_text = "File " + sound_path + " not found."
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
        self._console_add(popup_text)

    def copy_track_name(self, sound_path: str):
        """
        Copy the track name to the clipboard.
        :param sound_path:
        :return:
        """
        _log = Logger.get()
        self._console_add("copy_track_name() sound_path:" + sound_path)
        _log.debug("-- copy_track_name()")
        pyperclip.copy(sound_path)
        popup_text = "Track name " + sound_path + " was copied to the clipboard."
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
        self._console_add(popup_text)

    def copy_track_info(self, sound_path: str, list_audio_data):
        """
        Copy track info to clipboard.
        :param sound_path:
        :param list_audio_data:
        """
        _log = Logger.get()
        _log.debug("-- copy_track_info()")
        self._console_add("copy_track_info() sound_path:" + sound_path)
        data: AudioData
        out_data = ""
        for data in list_audio_data:
            if data.file_path == sound_path:
                out_data = data.to_string()
        pyperclip.copy(out_data)
        popup_text = "Track information as copied to the clipboard: \n" + out_data
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
        self._console_add(popup_text)

    def audio_gen_lip(self, sound_path: str, list_audio_data):
        "NOT WORKING PROPERLY"
        _log = Logger.get()
        _log.debug("-- audio_gen_lip()")
        self._console_add("audio_gen_lip() sound_path:" + sound_path)
        data: AudioData
        subtitles = ""
        wavfile = sound_path + ".wav"
        if exists(wavfile) != True:
            msg = "Error, could not find WAV file " + wavfile + " required to generate the lip file!"
            _log.warning("**WARNING** " + msg)
            self._console_add(msg)
            sg.popup_ok_cancel(msg, keep_on_top=True, icon=self.app.app_icon_ico,
                               title=AudioLogicLayer.STR_ERROR_POPUP)
            return False
        for data in list_audio_data:
            if data.file_path == sound_path:
                subtitles = data.subtitle
        # 0 - Creation Kit Exe
        # 1 - WAV file
        # 3 - Subtitle
        cmd = "{0}  -GenerateSingleLip:\"{1}\" \"{2}\"".format(self.app.creation_kit_exe, sound_path, subtitles)
        # print("##############" + cmd)

    def audio_gen_xwm(self, sound_path: str):
        """
        Generate XWM file.
        :param sound_path:
        """
        _log = Logger.get()
        _log.debug("-- audio_gen_xwm()")
        self._console_add("audio_gen_xwm(): " + sound_path)
        if sound_path.strip() == "":
            popup_text = "Error: No sound file selected!"
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                     title=AudioLogicLayer.STR_INFO_POPUP)
            self._console_add(popup_text)
            return
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        # in case a XWM file already exists, ask first
        if exists(xwm_file):
            popup_text = "The following file is going to be overwritten: \n" + str(xwm_file) +\
                         "\n\nDo you want to continue?"
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                                 title=AudioLogicLayer.STR_INFO_POPUP)
            self._console_add(popup_text)
            if popup_ret == AudioLogicLayer.STR_CANCEL:
                return
        # XWM file is going to be generated!
        # in case WAV does not exist, create one
        ret_flag = True
        ret_int = SkyAudioEncoder.RET_SUCCESS
        ret_msg = ""
        ret_stdout = ""
        if not exists(wav_file):
            [ret_flag, ret_val, ret_msg, ret_stdout] = self._generate_wav_if_not_exit(sound_path, xwm_to_wav=False)
        if not ret_flag:
            _log.warn("Last Stdout:" + ret_stdout)
            _log.warn("Last error:" + ret_msg + " for sound_path:" + sound_path)
            self._console_add("Last Stdout:\n" + ret_stdout)
            self._console_add("Last error:\n" + ret_msg + " for sound_path:" + sound_path)
            popup_text = "Error Generating XWM file: Error creating WAV file.\nSound Path: " + sound_path + \
                         "\nError Message:" + ret_msg
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            return
        ret_val = self.encoder.wav_to_xwm(sound_path)
        ret_stdout = self.encoder.get_last_stdout()
        ret_msg = self.encoder.get_last_error()
        _log.warn("Last Stdout:\n" + ret_stdout)
        self._console_add("Last Stdout:\n" + ret_stdout)
        if ret_val != SkyAudioEncoder.RET_SUCCESS:
            _log.error("Last error:" + ret_msg + " for sound_path:" + sound_path)
            self._console_add("Last error:" + ret_msg + " for sound_path:" + sound_path)
            popup_text = "Error encoding WAV into XWM.\nSound Path:" + sound_path + "\nError Code:" + str(ret_val) + \
                         ".\nError Message:" + ret_msg
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                     title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            return
        popup_text = "Success generating XWM file."
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
        self._console_add(popup_text)

    def audio_gen_fuz(self, sound_path: str):
        """
        Generate fuz file.
        :param sound_path:
        :return:
        """
        _log = Logger.get()
        _log.debug("-- audio_gen_fuz()")
        self._console_add("audio_gen_fuz() sound_path:" + sound_path)
        fuz_file = FileUtils.change_ext(sound_path, Exts.EXT_FUZ)
        ret_val = self.encoder.fuz(sound_path)
        if ret_val != SkyAudioEncoder.RET_SUCCESS:
            popup_text = "Error encoding file into FUZ format: " + self.encoder.get_last_error()
            _log.error("PopUp Error: " + popup_text)
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            return
        _log.debug("FUZ file " + fuz_file + " generated successfully!")
        popup_text = "Success generating FUZ file " + fuz_file + "."
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
        self._console_add(popup_text)

    def audio_gen_fuz_all(self, list_sound_path, parallel_method: int, ask_popup=True):
        """
        Execute fuz command massively, and returns a report.
        Tests with parallel methods, using the same dataset:
        *Method 1*
        (1) QuickTimer: 32.77095009999999 seconds
        (2) QuickTimer: 19.1045664 seconds (after restart, fow apps)
        (3) QuickTimer: 21.5257244 seconds (no restart, few apps)
        *Method 2*
        (1) QuickTimer: 29.6214035 seconds (many apps opened)
        (2) QuickTimer: 25.2366236 seconds (after restart, fow apps)
        (3) QuickTimer: 15.642155099999997 seconds (no restart, few apps)
        :param list_sound_path:
        :param parallel_method: 1 for one thread per core, 2 for thread ThreadPool (built-in).
        :return:
        """
        _log = Logger.get()
        _log.debug("audio_gen_fuz_all()")
        self._console_add("audio_gen_fuz_all()")
        # popup of confirmation
        if ask_popup:
            popup_text = "This procedure will overwrite any .fuz and xwm pre-existing file.\n If the audios are recoded in mp3 format, wav files are going to be overwritten as well.\n\n Do you want to continue?"
            self._console_add(popup_text)
            _log.debug("popup_text:" + popup_text)
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                                           title=AudioLogicLayer.STR_INFO_POPUP)
            self._console_add("User option: " + popup_ret)
            if popup_ret == AudioLogicLayer.STR_CANCEL:
                return
        # init the batch generation
        self._console_add("audio_gen_fuz_all() list_sound_path:" + str(list_sound_path) + ", parallel_method:" +
                          str(parallel_method))
        curr_exec_path = self.encoder.get_exe_dir()
        report_list_arg = []
        report_list_async = []
        ncores = multiprocessing.cpu_count()
        if ncores <= 1:
            ncores = 2
        self._console_add("-- ncores:" + str(ncores))
        # print("-- ncores:" + str(ncores))
        splitted_list = split_list(list_sound_path, ncores)
        # Thread managing, one per core
        if parallel_method != 2:
            threads = []
            ret_list = []
            for small_list in splitted_list:
                process = Thread(target=AudioLogicLayer._exec_fuz_list, args=[small_list, curr_exec_path, report_list_arg])
                process.start()
                threads.append(process)
            for process in threads:
                process.join()
        # Builtin thread pool
        else:
            pool = ThreadPool()
            async_result_list = []
            for small_list in splitted_list:
                async_result = pool.apply_async(AudioLogicLayer._exec_fuz_list, (small_list, curr_exec_path, report_list_arg))
                async_result_list.append(async_result)
            for item in async_result_list:
                return_val = item.get()
                report_list_async.append(return_val)
        n_errors = ReportBatchCmd.count_errors(report_list_arg)
        n_success = ReportBatchCmd.count_success(report_list_arg)
        _log.debug("Batch finished. n_errors:" + str(n_errors) + ", n_success:" + str(n_success))
        if ask_popup:
            popup_ret = ""
            popup_text = "Batch execution finished with {0} errors and {1} successes. Do you want to open the report?".format(n_errors, n_success)
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            html_report = ReportBatchCmd.export_report(report_list_arg, self.app.app_dir)
            url_report = "file:///" + os.path.realpath(html_report)
            # print("url_report:" + url_report)
            # print("popup_ret:" + popup_ret)
            if popup_ret != AudioLogicLayer.STR_CANCEL:
                webbrowser.open(url_report, new=2)

    def audio_gen_silent(self, sound_path: str, list_audio_data, ask_popup=True):
        _log = Logger.get()
        _log.debug("-- audio_gen_silent()")
        self._console_add("audio_gen_silent(): " + sound_path)
        # (1) check if no file was selected
        if sound_path.strip() == "":
            popup_text = "Error: No sound file selected!"
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                     title=AudioLogicLayer.STR_INFO_POPUP)
            self._console_add(popup_text)
            return
        # (2) filter the right file text from table data
        audio_text = ""
        for data in list_audio_data:
            if data.file_path == sound_path:
                audio_text = data.subtitle
                break
        # (3) calc usefull info
        sound_no_ext = FileUtils.remove_ext(sound_path)
        reading_time = AudioLogicLayer._calc_reading_time(text_str=audio_text,
                                                          wpm=self.app.settings_obj.audio_wpm,
                                                          word_len=self.app.settings_obj.audio_word_len,
                                                          min_time=self.app.settings_obj.audio_min_time,
                                                          padding=self.app.settings_obj.audio_padding)
        sound_wav = sound_no_ext + ".wav"
        bkp_name = sound_no_ext + ".rand" + str(randint(10000, 99999)) + ".wav.bkp"
        file_already_exist = os.path.exists(sound_wav)
        # (4) Popup and backup
        if ask_popup and file_already_exist:
            popup_text = "Audio file " + sound_wav + " already exists. Continuing will overwrite this file.\n\n" +\
                         "New generated file are going to have " + str(reading_time) + " seconds.\n\n" +\
                         "Are you sure?\n\n"
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                                           title=AudioLogicLayer.STR_INFO_POPUP)
            if (popup_ret == AudioLogicLayer.STR_CANCEL) or (popup_ret is None):
                _log.debug("-- audio_gen_silent() CANCELED")
                self._console_add("-- audio_gen_silent() CANCELED")
                return
            _log.info("creating backup file " + str(bkp_name))
            self.player.reset()
            os.rename(sound_wav, bkp_name)
        # (5) GENERATE EMPTY AUDIO
        ret_val, trace = AudioLogicLayer._create_silent_audio(sound_no_ext, reading_time)
        # in case of failure, report the error and restore the backup
        if not ret_val:
            _log.error(trace)
            sg.popup_ok(trace, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)
            if file_already_exist:
                os.rename(bkp_name, sound_wav)
            self.player.set(sound_wav)
            return
        # in case of success, delete the backup
        else:
            _log.info("Deleting BACKUP file " + bkp_name)
            os.remove(bkp_name)
            self.player.set(sound_wav)

    def audio_unfuz(self, sound_path: str):
        """
        Decode FUZ file into lip, xwm an wav format.
        :param sound_path: sound file.
        """
        _log = Logger.get()
        _log.debug("-- audio_unfuz()")
        self._console_add("audio_unfuz() sound_path:" + sound_path)
        lip_file = FileUtils.change_ext(sound_path, Exts.EXT_LIP)
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        exit_file = []
        resp = True
        if exists(wav_file):
            exit_file.append(wav_file)
        if exists(xwm_file):
            exit_file.append(xwm_file)
        if exists(lip_file):
            exit_file.append(lip_file)
        if len(exit_file) > 0:
            popup_text = "The following files are going to be overwritten: \n" + str(exit_file) +\
                         "\n\nDo you want to continue?"
            _log.error("PopUp Error: " + popup_text)
            popup_ret = sg.popup_ok_cancel(popup_text, keep_on_top=True, icon=self.app.app_icon_ico,
                                           title=AudioLogicLayer.STR_INFO_POPUP)
            if popup_ret == AudioLogicLayer.STR_CANCEL:
                _log.info("Operation {0} was CANCELLED".format("audio_unfuz()"))
                self._console_add("Operation {0} was CANCELLED".format("audio_unfuz()"))
                return
        ret_val = self.encoder.unfuz(sound_path)
        if ret_val != SkyAudioEncoder.RET_SUCCESS:
            popup_text = "Error decoding file " + sound_path + "\nError Code:" + str(ret_val) + "\n. Error message:" + \
                         self.encoder.get_last_error()
            _log.error("PopUp Error: " + popup_text)
            sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_ERROR_POPUP)
            self._console_add(popup_text)
            return
        _log.debug("UNFUZ on file " + sound_path + " was successful!")
        popup_text = "Success on UNFUZ file " + sound_path + "."
        self._console_add(popup_text)
        sg.Popup(popup_text, keep_on_top=True, icon=self.app.app_icon_ico, title=AudioLogicLayer.STR_INFO_POPUP)

    def get_current_track_len(self):
        """
        Returns track len in seconds.
        :return:
        """
        return self.player.len()

    def get_current_track_progress(self):
        """
        Returns track progress in seconds.
        :return:
        """
        return self.player.position()

    def console_has_change(self):
        """
        Tells if the console has any change or not.
        :return: True or false.
        """
        return self._console_has_change

    def get_console_output(self):
        """
        Retuns the string with the log execution of the last task.
        :return: string with the console output.
        """
        self._console_has_change = False
        return self.console_output

    @staticmethod
    def _exec_fuz_list(list_files, exec_path, list_exec_report):
        """
        Execute the fuz operation for all listed files on list_files. The encoder is instantiated using the exec_path
        as reference. A list of report objects BatchCmdReport is returned and is appended (extend) to the mutable list
        list_exec_report.
        :param list_files: list of sound files where are going to be applied the fuz operation.
        :param exec_path: Exec path to instantidate the SkyAudioEncoder object.
        :param list_exec_report: mutable list of execution reports.
        :return: list of execution reports generated in this thread.
        """
        encoder = SkyAudioEncoder(exec_path)
        thread_report = []
        if len(list_files) == 0:
            return thread_report
        for sound_path in list_files:
            report = ReportBatchCmd()
            fuz_file = FileUtils.change_ext(sound_path, Exts.EXT_FUZ)
            ret_val = encoder.fuz(sound_path)
            report.error_code = ret_val
            if ret_val != SkyAudioEncoder.RET_SUCCESS:
                report.error_flag = False
                report.error_message = encoder.get_last_error()
            else:
                report.error_flag = True
            report.command = encoder.get_last_command()
            report.stdout = encoder.get_last_stdout()
            report.process_code = encoder.get_last_encodder_ret_code()
            report.file_name = sound_path
            report.exe_dir = exec_path
            thread_report.append(report)
        AudioLogicLayer.batchReportMutex.acquire()
        try:
            list_exec_report.extend(thread_report)
        finally:
            AudioLogicLayer.batchReportMutex.release()
        return thread_report

    @staticmethod
    def _calc_reading_time(text_str, wpm=110, word_len=5, min_time=2, padding=0):
        """
        Estimate the reading time in seconds.
        :param text_str: Text to be read.
        :param wpm: words per minute.
        :param word_len: length of each word.
        :param min_time: minimum time.
        :param padding: this value will be added to the generated time.
        :return: extimated reading time.
        """
        # split text in words
        text_list = text_str.split()
        # count words
        total_words = 0
        for current_text in text_list:
            total_words += len(current_text) / word_len
        # calc reading time in seconds
        read_time = (total_words * 60) / wpm
        # add padding
        read_time = read_time + padding
        # ensure min time
        read_time = max([read_time, min_time])
        return round(read_time)

    @staticmethod
    def _create_silent_audio(file_name: str, duration_sec: int):
        try:
            silent_audio = AudioSegment.silent(duration=int(duration_sec) * 1000)  # or be explicit
            silent_audio.export(file_name + ".wav", format="wav")
            return True, "SUCCESS"
        except:
            ex_msg = "Error exporting file " + file_name + ".wav\n'" +\
                     "traceback.format_exc(): " + str(traceback.format_exc()) + "\n" +\
                     "sys.exc_info()[2]: " + str(sys.exc_info()[2])
            return False, ex_msg

    def _generate_wav_if_not_exit(self, sound_path, xwm_to_wav=True):
        """
        Try to generate WAV file if it does not exit.
        :param sound_path: Path of the file to be used on the generation of the wav file.
        :param xwm_to_wav: optional flag to tell the method to try or not to generate a wav file from a xwm.
        :return: return a vector [ret_flag: bool, ret_val: int, ret_msg: str, ret_stdout: str], where ret_flag is True
        if the WAV file already exits, or it was rightly generated. Returns False in case some error occurred
        generating the WAV file. ret_val is the code returned by the encoder. ret_str is the error message returned by
        the encoder, in case of error, and the ret_strout is the console output generated (error os success).
        """
        _log = Logger.get()
        _log.debug("-- _generate_wav_if_not_exit()")
        self._console_add("_generate_wav_if_not_exit()")
        ret_flag = False
        ret_val = SkyAudioEncoder.RET_SUCCESS
        ret_msg = ""
        ret_stdout = ""
        wav_file = FileUtils.change_ext(sound_path, Exts.EXT_WAV)
        xwm_file = FileUtils.change_ext(sound_path, Exts.EXT_XWM)
        mp3_file = FileUtils.change_ext(sound_path, Exts.EXT_MP3)
        _log.debug("wav_file:" + wav_file + ", xwm_file:" + xwm_file + ", mp3_file:" + mp3_file)
        self._console_add("wav_file:" + wav_file + ", xwm_file:" + xwm_file + ", mp3_file:" + mp3_file)
        if not exists(wav_file):
            _log.info("WAV file for <" + sound_path + "> was not found. Search for alternatives: XWM and MP3...")
            self._console_add("WAV file for <" + sound_path + "> was not found. Search for alternatives: XWM and MP3...")
            # try to generate file from XWM
            if exists(xwm_file) and xwm_to_wav:
                _log.info("XWM was found for " + sound_path)
                self._console_add("XWM was found for " + sound_path)
                ret_val = self.encoder.xwm_to_wav(sound_path)
                ret_stdout = self.encoder.get_last_stdout()
                _log.error("Last stdout: " + ret_stdout)
                self._console_add("Last stdout: " + ret_stdout)
                if ret_val != SkyAudioEncoder.RET_SUCCESS:
                    ret_msg = self.encoder.get_last_error()
                    _log.error("Last error: " + ret_msg)
                    self._console_add("Last error: " + ret_msg)
            # try mp3 format
            elif exists(mp3_file):
                _log.info("MP3 was found for " + sound_path)
                self._console_add()
                ret_val = self.encoder.mp3_to_wav(sound_path)
                ret_stdout = self.encoder.get_last_stdout()
                _log.error("Last stdout: " + ret_stdout)
                self._console_add("Last stdout: " + ret_stdout)
                if ret_val != SkyAudioEncoder.RET_SUCCESS:
                    ret_msg = self.encoder.get_last_error()
                    _log.error("Last error: " + ret_msg)
                    self._console_add("Last error: " + ret_msg)
            else:
                ret_msg = "WAV file does not exit for track \"" + sound_path + \
                          "\", and no alternative (xwm or mp3) was found.\n Try to use UnFuz first."
                _log.error("ret_msg:" + ret_msg)
                self._console_add("ret_msg:" + ret_msg)
        else:
            ret_flag = True
        if ret_val == SkyAudioEncoder.RET_SUCCESS:
            ret_flag = True
        # print([ret_flag, ret_val, ret_msg, ret_stdout])
        self._console_add(str([ret_flag, ret_val, ret_msg, ret_stdout]))
        return [ret_flag, ret_val, ret_msg, ret_stdout]

    def _console_clear(self):
        self.console_output = ""
        self._console_has_change = True

    def _console_add(self, msg: str):
        self.console_output += msg + "\n"
        self._console_has_change = True


# -*- coding: utf-8 -*-
import logging

import dateutil
import requests
from django.db.models import Q

__author__ = 'mloeks'

from main.models import Game

from django.core.management.base import BaseCommand

LOG = logging.getLogger('rtg.' + __name__)

class Command(BaseCommand):
    args = ''
    help = 'Set OpenLigaDB match IDs on games'

    def handle(self, *args, **options):
        openligadb_games = self.fetch_games()
        for game in list(self.get_games_without_match_id()):
            match_found = False
            for ol_game in openligadb_games:
                if self.game_matches(game, ol_game):
                    LOG.info('Setting Match ID of game %s to %s' % (game, ol_game[0]))
                    match_found = True
                    game.openligadb_match_id = ol_game[0]
                    game.save()
                    break
            if not match_found:
                LOG.warning('No ID found for game %s (ID %s)' % (game, game.pk))

    def fetch_games(self):
        resp = requests.get('https://www.openligadb.de/api/getmatchdata/fifa18/2018').json()
        return [(
            g['MatchID'],
            dateutil.parser.parse(g['MatchDateTimeUTC']),
            g['Team1']['TeamName'],
            g['Team2']['TeamName']
        ) for g in resp]

    def get_games_without_match_id(self):
        return Game.objects\
            .filter(Q(openligadb_match_id__isnull=True) | Q(openligadb_match_id__exact=''))

    def game_matches(self, game, ol_game):
        return game.kickoff == ol_game[1] and game.hometeam.name.lower() == ol_game[2].lower() \
                    and game.awayteam.name.lower() == ol_game[3].lower()


import os, os.path
import time
from loguru import logger
from sys import stderr

logger.remove()
logger.add(stderr, format="<white>{time:HH:mm:ss}</white>"
                          " | <level>{level: <8}</level>"
                          " | <cyan>{line}</cyan>"
                          " - <white>{message}</white>")

def check_images():
    p='media'
    return [os.remove(file) for file in (os.path.join(path, file) for path, _, files in os.walk(p) for file in files) if os.stat(file).st_mtime < time.time() - 7 * 86400]
logger.debug(f"LAUNCHED IMAGE REMOVER")
while (True):
    time.sleep(1*3600)
    logger.debug("STARTING IMAGE REMOVE")
    result=len(check_images())
    logger.debug(f"REMOVED {result} IMAGES")


import pandas as pd
import warnings

warnings.simplefilter(action="ignore", category=FutureWarning)

# Convert fractional year to pandas date format
def fractional_year_to_datetime(year_fraction):
    year = int(year_fraction)
    fraction = year_fraction - year
    delta = pd.Timedelta(fraction * 365.25, unit="d")
    return pd.to_datetime(str(year)) + delta

def load_dataset():
    dataset = {
        "ssd": pd.read_csv(
            "./data/ssd-jcmit.csv", sep=" ", header=None, names=["ds", "y"]
        ),
        "drives": pd.read_csv(
            "./data/drives-jcmit.csv", sep=" ", header=None, names=["ds", "y"]
        ),
        "flash": pd.read_csv(
            "./data/flash-jcmit.csv", sep=" ", header=None, names=["ds", "y"]
        ),
        "memory": pd.read_csv(
            "./data/memory-jcmit.csv", sep=" ", header=None, names=["ds", "y"]
        ),
    }

    for key in dataset:
        dataset[key]["ds"] = dataset[key]["ds"].apply(fractional_year_to_datetime)

    return dataset


import sys
input=sys.stdin.readline

import math

n=1000001
a = [True]*(n+1)
primes=[]

for i in range(2,n+1):
  if a[i]:
    primes.append(i)
    for j in range(2*i, n+1, i):
        a[j] = False

N=int(input())
List=list(map(int,input().split()))
primegroup=[[0 for _ in range(N)] for _ in range(len(primes))]
#primegroup[어떤 수][소수가]=몇개 --> 예를 들면 primegroup[3번째 수, 8][2, 즉 primes인덱스1]=3
for i in range(len(List)):
    for j in range(len(primes)):
        if List[i]%primes[j]==0:
            while (List[i]%primes[j]==0):
                primegroup[j][i]+=1
                List[i]//=primes[j]

#print(primegroup)

ans1=1; ans2=0
for x in range(len(primes)):
    target=sum(primegroup[x])//N
    ans1*=primes[x]**int(target)
    for j in range(N):
        if target>primegroup[x][j]:
            ans2+=primegroup[x][j]-target
    # print(ans1, ans2)

# print(primegroup)

print(ans1, abs(int(ans2)))


"""

Structures and functions for reading a knowledge base of rules for constructing argument networks.

"""

import logging
import copy
import itertools
import functools
from collections import defaultdict

from pyparsing import alphanums, alphas, delimitedList, ParseException
from pyparsing import Word, Group, Optional, Suppress, OneOrMore

from .signals import Signal
from .common import Graph

logger = logging.getLogger('argulib.kb')

class ParseError(Exception):
    """ Thrown when parsing fails. """
    pass

class RuleError(Exception):
    pass

class Literal:
    """ The class represents a (possibly negated) literal.

    The main purpose of the class is to enforce naming conventions.
    Literals follow C naming conventions - start with a letter
    optionally followed by alpha-nums + '_'.

    To create a Literal instance from string use Literal.from_str(x). If
    a literal is to be negated, the identifier should start with '-' (eg, -p).
    The parser is not smart enough to parse "--p" as "p".

    """

    def __init__(self, name, negated=False):
        """ Create a literal with a name.
        @param name: the name of the literal
        @param negated: specifies whether the literal is negated (eg, not a; default false)

        """
        self.name = name
        self.negated = negated

    def __eq__(self, other):
        return (isinstance(other, Literal) and
                self.name == other.name and
                self.negated == other.negated)

    def __lt__(self, other):
        return (self.name, self.negated) < (other.name, other.negated)

    def __neg__(self):
        return Literal(self.name, not self.negated)

    def __hash__(self):
        return hash(str(self))

    def __str__(self):
        return '%s%s' % ('-' if self.negated else '', self.name)

    def __repr__(self):
        return "<Literal: %s>" % str(self)

    @classmethod
    def from_str(cls, data):
        try:
            parsed = _literal.parseString(str(data).strip(), parseAll=True)
            return parsed[0]
        except ParseException as e:
            raise ParseError('"%s" is not a literal; %s' % (data, str(e))) from e

    @classmethod
    def from_parsed(cls, parsed):
        params = parsed[0]
        if len(params) == 1:
            return cls(params[0])
        elif len(params) == 2:
            return cls(params[1], True)

# types of rules

STRICT_RULE = 0
DEFEASIBLE_RULE = 1
ORDERING_RULE = 2

class StrictRule:
    """ The class represents a strict rule (a modus ponens). """

    _hash = None

    type = STRICT_RULE
    is_strict = True
    is_defeasible = False

    def __init__(self, antecedent, consequent, name=''):
        """ A rule has to have antecedent and a consequent.
        antecedent -- a list of Literals or None
        consequent -- a Literal
        name -- an optional name (default = '')

        """
        self.name = name
        # do some error checking to be nice
        self.antecedent = check_list_of_type(antecedent, Literal,
                                             'Antecedent must be a list of Literals')
        self.antecedent.sort()  # it is essential that the list is sorted!
        if consequent is None:
            raise RuleError("Rule has to have a consequence (None provided)")
        else:
            if not isinstance(consequent, Literal):
                raise RuleError("Consequent must be a Literal")
            self.consequent = consequent

    def __eq__(self, other):
        """ Two rules are equal if they are the same type
        and contain the same antecedent and the same consequent.
        Name does not matter.

        """
        return (self.type == other.type and
                self.antecedent == other.antecedent and
                self.consequent == other.consequent)

    def __len__(self):
        """ The length of the rule is given by the number of antecedents. """
        return len(self.antecedent)

    def __hash__(self):
        """ Just like equals, the hash only uses the antecedent and consequent.

        """
        if not self._hash:
            value = hash(self.consequent)
            for a in self.antecedent:
                value ^= hash(a)
            self._hash = value
        return self._hash

    def __lt__(self, other):
        """ self < other if self has fewer antecedents or if they are alphabetically before other. """
        if other.type == DEFEASIBLE_RULE:
            return False
        ls = len(self.antecedent)
        lo = len(other.antecedent)
        if ls != lo:
            return ls < lo
        else:
            return str(self) < str(other)

    def __str__(self):
        name = self.name + ': ' if self.name else ''
        return ('{name}{antecedent} --> {consequent}'
                .format(name=name,
                        antecedent=', '.join(map(str, self.antecedent)),
                        consequent=str(self.consequent)))

    def __repr__(self):
        return '<StrictRule %s>' % str(self)

    @classmethod
    def from_str(cls, data):
        try:
            parsed = _strict_rule.parseString(str(data).strip(), parseAll=True)
            return parsed[0]
        except Exception as e:
            raise ParseError('"%s" is not a strict rule\n\t error: %s'
                             % (data, str(e)))

    @classmethod
    def from_parsed(cls, parsed):
        if 'name' in parsed:
            name = parsed['name']
        else:
            name = ''
        if 'antecedent' in parsed:
            antecedent = list(parsed['antecedent'])
        else:
            antecedent = []
        consequent = parsed['consequent'][0]
        return cls(antecedent, consequent, name)

class DefeasibleRule:
    """ The class represents a defeasible rule.
    The difference between a strict and a defeasible rule is that defeasible
    rule captures "the most frequent case". E.g., A ==> B means that
    A usually implies B.

    Defeasible rules can have exceptions (vulnerabilities) specified in
    parenthesis between the arrow: A =(-C)=> B means that A usually implies B
    unless C is true. (E.g., an object is red if it looks red unless we shine
    on it a red light.)

    """
    _hash = None

    type = DEFEASIBLE_RULE
    is_strict = False
    is_defeasible = True

    def __init__(self, antecedent, consequent, vulnerabilities=None, name=''):
        """ A rule has to have antecedent and a consequent.
        :param antecedent: a list of Literals or None
        :param consequent: a Literal
        :param vulnerabilities: a list of Literals or None (default)
        :param name: an optional name (default = '')

        """
        self.name = name
        # do some error checking to be nice
        self.antecedent = check_list_of_type(antecedent, Literal,
                                             'Antecedent must be a list of Literals')
        self.antecedent.sort()  # it is essential that the list is sorted!

        if not isinstance(consequent, Literal):
            raise RuleError('Consequent must be a Literal but was {}'
                            .format(type(consequent)))
        self.consequent = consequent

        self.vulnerabilities = check_list_of_type(vulnerabilities, Literal,
                                                  'Vulnerabilities must be list of Literals')
        self.vulnerabilities.sort()

    def __eq__(self, other):
        """ Two rules are equal if they are the same type (rule.type ==
        DEFEASIBLE_RULE) and contain the same antecedent, the same consequent
        and the same vulnerabilities. Name does not matter.

        """
        return (self.type == other.type and
                self.antecedent == other.antecedent and
                self.consequent == other.consequent and
                self.vulnerabilities == other.vulnerabilities)

    def __len__(self):
        """ The length of the rule is given by the number of antecedents. """
        return len(self.antecedent)

    def __hash__(self):
        """ Just like equals, the hash only uses the antecedent and consequent
        and the vulnerabilities.

        """
        if not self._hash:
            value = hash(self.consequent)
            for a in self.antecedent:
                value ^= hash(a)
            for a in self.vulnerabilities:
                value ^= hash(a)
            self._hash = value
        return self._hash

    def __lt__(self, other):
        """ self < other if self has fewer antecedents, or
        fewer vulnerabilities or
        if str(self) is alphabetically before str(other).

        """
        if other.type == STRICT_RULE:
            return True
        ls = len(self.antecedent)
        lo = len(other.antecedent)
        if ls != lo:
            return ls < lo
        else:
            ls = len(self.vulnerabilities)
            lo = len(other.vulnerabilities)
            if ls != lo:
                return ls < lo
            else:
                return str(self) < str(other)

    def __str__(self):
        text = '%s: ' % self.name if self.name else ''
        if len(self.antecedent) > 0:
            text += ', '.join(map(str, self.antecedent))
        if len(self.vulnerabilities) > 0:
            text += (' =(%s)=> ' % ', '.join(map(str, self.vulnerabilities)))
        else:
            text += ' ==> '
        text += str(self.consequent)
        return text

    def __repr__(self):
        return '<DefeasibleRule %s>' % str(self)

    @classmethod
    def from_str(cls, data):
        try:
            parsed = _defeasible_rule.parseString(str(data).strip(), parseAll=True)
            return parsed[0]
        except Exception as e:
            raise ParseError('"%s" is not a defeasible rule\n\tError: %s'
                             % (repr(data), str(e)))

    @classmethod
    def from_parsed(cls, parsed):
        if 'name' in parsed:
            name = parsed['name']
        else:
            name = ''
        if 'antecedent' in parsed:
            antecedent = list(parsed['antecedent'])
        else:
            antecedent = []
        consequent = parsed['consequent'][0]
        if 'vulnerabilities' in parsed:
            vulnerabilities = list(parsed['vulnerabilities'])
        else:
            vulnerabilities = list()
        return cls(antecedent, consequent, vulnerabilities, name)

class Ordering:

    type = ORDERING_RULE

    def __init__(self, *data, direction='<'):
        """Initialise orderings from a list of tuples. """
        self.data = check_list_of_type(list(data), tuple)
        self.direction = direction

    @classmethod
    def from_str(cls, data):
        try:
            parsed = _orderings.parseString(str(data).strip(), parseAll=True)
            return parsed[0]
        except Exception as e:
            raise ParseError('"%s" is not a preference rule\n\t error: %s' % (data, str(e)))

    @classmethod
    def from_parsed(cls, parsed, ord):
        tmp = []
        for i in range(len(parsed) - 1):
            tmp.append((list(parsed[i]), list(parsed[i + 1])))
        return cls(*tmp, direction=ord)

    def __str__(self):
        return (' %s ' % self.direction).join(map(str, self.data))

    def __repr__(self):
        return '<Ordering: %s>' % str(self)

    def __eq__(self, other):
        return self.data == other.data

class Proof:
    """ A proof leading to a particular consequent. """

    def __init__(self, name, rule, proofs, kb):
        """ Create an instance of a proof concluding "consequent" given
        the rule and proofs for the antecedents of the rule.

        """
        self._hash = None
        self.name = name
        self.rule = rule
        self._proofs = proofs
        self.is_strict = all(map(lambda x: x.is_strict, self.rules))
        self.is_defeasible = not self.is_strict
        self.weakest_link = self
        if kb:
            self.update_weakest_link(kb)

    def __str__(self):
        s = ' ∧ '.join(map(str, self.subproofs))
        if not self.has_empty_antecedent():
            s = '(' + s + ')' + ' → '
        s += str(self.rule)
        return s.strip()

    def __repr__(self):
        return '<Proof %s>' % str(self)

    def __eq__(self, other):
        """ Two proofs are equal if they have the same top rule and the same sub-proofs. """
        return self.rule == other.rule and self._proofs == other._proofs

    def __hash__(self):
        if not self._hash:
            value = hash(self.rule)
            for p in self._proofs.values():
                value ^= hash(p)
            self._hash = value
        return self._hash

    def __len__(self):
        """ Return the number of rules involved in this proof. """
        max_len = list(map(len, self.subproofs))
        return 1 + max(max_len + [0])

    def __lt__(self, other):
        """ Order by length, name. """
        l1 = len(self)
        l2 = len(other)
        if l1 == l2:
            return self.name < other.name
        else:
            return l1 < l2

    @property
    def antecedents(self):
        """ Yield the antecedents of this proof. """
        return self.rule.antecedents

    @property
    def consequent(self):
        """ Return the consequent of this proof. """
        return self.rule.consequent

    @property
    def conclusion(self):
        """ Return the consequent of this proof. """
        return self.rule.consequent

    @property
    def vulnerabilities(self):
        """ Return the vulnerabilities of this proof. """
        if self.rule.is_strict:
            return []
        return self.rule.vulnerabilities

    @property
    def subproofs(self):
        """ Yield the proofs used by the top rule only. """
        return self._proofs.values()

    @property
    def proofs(self):
        """ Return the set of all proofs used by this proof. """
        tmp = map(lambda x: x.proofs, self.subproofs)  # sets of subproofs
        proofs = functools.reduce(lambda x, y: x | y, tmp, set())  # collect them
        proofs.add(self)  # add self
        return proofs

    @property
    def rules(self):
        """ Return the generator of all rules including the top one. """
        return map(lambda x: x.rule, self.proofs)

    @property
    def strict_rules(self):
        """ Return all strict rules used in this proof and its subproofs. """
        return filter(lambda r: r.type == STRICT_RULE, self.rules)

    @property
    def defeasible_rules(self):
        """ Return all defeasible rules used in this proof and its subproofs."""
        return filter(lambda r: r.type == DEFEASIBLE_RULE, self.rules)

    def has_empty_antecedent(self):
        """ Return True if the proof does not have any antecedents. """
        return len(self._proofs) == 0

    def uses_rule(self, rule):
        """ Returns True if any of the proofs use the given rule. """
        return any(map(lambda x: x == rule, self.rules))

    def uses_consequent(self, consequent):
        """ Returns True if any of the proofs leads to the given consequent. """
        return any(map(lambda x: x.consequent == consequent, self.rules))

    def update_weakest_link(self, kb):
        """ Find the weakest rule based on the preference of the knowledge base. """
        # FIXME: this actually just finds the least preferred rule wrt to itself
        #   the weakest link should really be calculated between two proofs
        #   because preferences might not be defined over every pair of rules
        self.weakest_link = self.rule
        if not self.is_strict:
            for link in [p.weakest_link for p in self.proofs]:
                if self.weakest_link.is_strict and link.is_defeasible:
                    self.weakest_link = link
                elif kb.less_preferred(link, self.weakest_link):
                    self.weakest_link = link
            logger.debug('Weakest link of {0} set to \n\t{1}'
                         .format(self, self.weakest_link))
        return self.weakest_link

class KnowledgeBaseError(Exception):
    """ Thrown when construction of knowledge base fails. """
    pass

class KnowledgeBase:
    """ A class that represents the knowledge base of rules. """

    def __init__(self, name=''):
        self.name = name
        self._rules = defaultdict(set)  # consequent : [rule]
        self._prefs = Graph()  # directed acyclic graph storing partial order
        self._proofs = defaultdict(set)  # consequent : [proofs]
        # working memory -- inferred rules + user rules
        self._wm = defaultdict(set)  # consequent : [rule]
        # signals
        self.rules_added = Signal()
        self.rules_deleted = Signal()
        self.updated = Signal()  # passes proofs (set) and flag added
        self.ordering_changed = Signal()
        # index for creating proofs
        self.proof_idx = 0
        # if True, proofs are not generated -- for batch adding/deleting
        self.batch = False

    @classmethod
    def from_file(cls, file_name):
        if file_name is None:
            return cls()
        kb = cls(file_name)
        kb.read_file(file_name)
        return kb

    def __eq__(self, other):
        # it is easier to compare the stings as the KBs can differ in proofs
        # because removing a proof still leaves the key with an empty set in
        # the `_proofs` dict.
        return str(self) == str(other)

    def __str__(self):
        s = 'Name: "%s"\n' % self.name
        s += ('Strict Rules:\n\t%s\n' %
              '\n\t'.join(map(str, sorted(self.get_strict_rules()))))
        s += ('Defeasible Rules:\n\t%s\n' %
              '\n\t'.join(map(str, sorted(self.get_defeasible_rules()))))
        s += ('Proofs:\n\t%s\n' %
              '\n\t'.join(map(str, sorted(self.proofs))))
        s += ('Preferences:\n\t%s' %
              '\n\t'.join(map(lambda x: '%s > %s' % x, self._prefs.edges)))
        return s

    __repr__ = __str__

    @property
    def rules(self):
        """ Return a generator of rules in the KB (in working memory). """
        for rules in self._wm.values():
            for r in rules:
                yield r

    @property
    def proofs(self):
        """ Return a generator of proofs in the knowledge base. """
        for proofs in self._proofs.values():
            for p in proofs:
                yield p

    def proofs_for(self, conclusion):
        """Return the set of proofs for `conclusion`. """
        return self._proofs[conclusion]

    # TODO: remove the recalc flag? or change to batch?
    def add_rule(self, rule):
        """ Try to add a new rule (possibly a string) in the knowledge base.

        Strict rule might raise KBError if it would
        make the knowledge base inconsistent.
        Ordering rule might raise KBError if it would
        make the knowledge base inconsistent.
        Malformed string can raise ParseError.

        :param rule: str or Stric or Defeasible or Ordering rule
        """
        logger.debug('adding rule "%s"' % str(rule))
        if isinstance(rule, str):
            rule = mk_rule(rule)
        if STRICT_RULE == rule.type:
            self._add_strict_rule(rule)
        elif DEFEASIBLE_RULE == rule.type:
            self._add_defeasible_rule(rule)
        elif ORDERING_RULE == rule.type:
            self.add_ordering(rule)
        else:
            msg = 'Unknown rule type for rule "%s"'
            raise KnowledgeBaseError(msg % str(rule))
        return rule

    def _add_strict_rule(self, rule):
        logger.debug('  _adding strict rule "%s"' % str(rule))
        if not rule.type == STRICT_RULE:
            raise KnowledgeBaseError('Tried to insert a non strict rule.')
        closure = self.contrapositions(rule)
        all_variants = {rule} | closure
        # create new proofs
        new_proofs = self.construct_proofs(self._proofs, all_variants)
        # check that the new proofs are consistent with the current KB
        self.check_consistency(new_proofs)
        # add to the list of rules
        self._rules[rule.consequent].add(rule)
        # add to the working memory
        for r in all_variants:
            self._wm[r.consequent].add(r)
        # add the proofs
        for p in new_proofs:
            self._proofs[p.conclusion].add(p)
        # emit signals
        self.rules_added(all_variants)
        self.updated(new_proofs, added=True)

    def _add_defeasible_rule(self, rule):
        logger.debug('  _adding defeasible rule "%s"' % str(rule))
        if not rule.type == DEFEASIBLE_RULE:
            raise KnowledgeBaseError('Tried to insert a non defeasible rule.')
        self._rules[rule.consequent].add(rule)
        self._wm[rule.consequent].add(rule)
        # create new proofs
        new_proofs = self.construct_proofs(self._proofs, {rule})
        # add the proofs
        for p in new_proofs:
            self._proofs[p.conclusion].add(p)
        # emit signals
        self.rules_added({rule})
        self.updated(new_proofs, added=True)

    def del_rule(self, rule):
        """ Delete the given rule and all proofs that use this rule. """
        # if passed as a string, parse it first
        if isinstance(rule, str):
            rule = mk_rule(rule)
        logger.debug('deleting rule "%s"' % str(rule))
        if STRICT_RULE == rule.type:
            self._del_strict_rule(rule)
        elif DEFEASIBLE_RULE == rule.type:
            self._del_defeasible_rule(rule)
        elif ORDERING_RULE == rule.type:
            self.del_ordering(rule)
        else:
            msg = 'Unknown rule type for rule "%s"'.format(str(rule))
            raise KnowledgeBaseError(msg)
        return rule

    def _del_strict_rule(self, rule):
        logger.debug('  _deleting strict rule "%s"' % str(rule))
        if rule.consequent not in self._rules:
            return
        # if the rule is in _rules, it has to be in _wm as well
        closure = self.contrapositions(rule)
        all_variants = {rule} | closure
        # proofs that use the rule should also be deleted
        proofs = set()
        # delete the rule + contrapositions from working memory
        for r in all_variants:
            if r.consequent in self._wm:
                self._wm[r.consequent].remove(r)
                for p in self.proofs:
                    if p.uses_rule(r):
                        proofs.add(p)
        for p in proofs:
            self._proofs[p.consequent].remove(p)
        # delete the rule
        self._rules[rule.consequent].remove(rule)
        # emit signals
        self.rules_deleted(closure)
        self.updated(proofs, added=False)

    def _del_defeasible_rule(self, rule):
        logger.debug('  _deleting defeasible rule "%s"' % str(rule))
        if rule.consequent not in self._rules:
            return
        # if the rule is in _rules, it has to be in _wm as well
        self._wm[rule.consequent].remove(rule)
        proofs = set()
        for p in self.proofs:
            if p.uses_rule(rule):
                proofs.add(p)
        for p in proofs:
            self._proofs[p.consequent].remove(p)
        self._rules[rule.consequent].remove(rule)
        # emit signals
        self.rules_deleted({rule})
        self.updated(proofs, added=False)

    @staticmethod
    def contrapositions(rule):
        """ Create a set of contraposition rules.
        Every strict rule have corresponding contraposition rules.
        For example:
            a --> b also means that -b --> -a
            p, q --> r has p, -r --> -q and -r, q --> -p

        """
        logger.debug('  contrapositions for rule: %s' % rule)
        rules = set()
        nc = -rule.consequent  # negation of the consequent
        idx = 0
        for a in rule.antecedent:
            idx += 1
            antecedent = [i for i in rule.antecedent if i != a]
            antecedent.append(nc)
            r = StrictRule(antecedent, -a)
            if r.name != '':
                r.name = '%s-%d' % (rule.name, idx)
            rules.add(r)
            logger.debug('\t created contraposition : %s' % r)
        return rules

    def construct_proofs(self, existing_proofs, rules):
        """ Return the set of new proofs given the existing proofs
        and new rules.

        existing_proofs -- a dict of proofs: {conclusion: {proofs} }
        rules -- a set of rules: {rule}
        """
        # if we are batch processing, don't add any proofs
        if self.batch: return set()
        logger.debug('constructing proofs for rules \n\t%s'
                     % '\n\t'.join(map(str, rules)))
        new_proofs = set()
        inferred = set()  # new conclusions
        old_size = -1
        rules = sorted(rules)
        all_proofs = copy.copy(existing_proofs)  # shallow copy is sufficient
        num_steps = 0
        while old_size != len(new_proofs):
            # how many proofs we are starting from in this iteration
            old_size = len(new_proofs)
            num_steps += 1
            for r in rules:
                logger.debug('Current rule %s' % repr(r))
                # can we skip this rule, because no new conclusions affect it?
                if num_steps > 1 and (not inferred & set(r.antecedent)):
                    # none of the inferred conclusions is in the antecedent
                    logger.debug('...this rule has no new proofs')
                    continue
                logger.debug('...this rule might have new proofs')
                # find a proof for each antecedent
                subproofs = dict()
                for a in r.antecedent:
                    if a in all_proofs:
                        subproofs[a] = all_proofs[a]
                    else:
                        break
                # do we have a proof for all antecedents?
                if len(subproofs) == len(r.antecedent):
                    tmp = self._create_proofs(r, subproofs)
                    new_proofs |= tmp
                    inferred |= set(map(lambda p: p.conclusion, new_proofs))
                    all_proofs[r.consequent] |= tmp
            # we started only with the new rules;
            # now add other rules that might be applicable
            if num_steps == 1 and new_proofs:
                rules = sorted((set(rules) | set(self.rules)))
        logger.debug('Constructed proofs in %d iterations.' % num_steps)
        return new_proofs

    def _create_proofs(self, rule, subproofs):
        """ Insert new proofs based on the rule and the subproofs.
        Subproofs have the format of a dictionary with Literals as kees and
        sets of proofs as values. Because there can be many ways to reach
        a consequent (conclusion) a proof for each of the subproofs should
        be created.

        """
        logger.debug('\t adding proofs with rule %s' % str(rule))
        logger.debug('\t\t subproofs: %s' % str(subproofs))
        new_proofs = set()
        # we need a proof for each subproof so create a cartesian product
        # to find the possible combinations
        product = itertools.product(*subproofs.values())
        for combination in product:
            contains_loop = False
            for subproof in combination:
                if subproof.uses_rule(rule):
                    # avoid loops - in case one of the subproofs uses the rule
                    contains_loop = True
                    break
            if contains_loop:
                continue
            proofs = dict()
            for sp in combination:
                proofs[sp.consequent] = sp
            p = Proof('', rule, proofs, self)
            name = self.generate_proof_name()
            p.name = name
            logger.debug('\t\tfound proof "%s"' % str(p))
            new_proofs.add(p)
        return new_proofs

    def recalculate(self):
        """ Recalculate all proofs. """
        # create new proofs
        self._proofs.clear()
        self.proof_idx = 0
        new_proofs = self.construct_proofs(self._proofs, set(self.rules))
        # add the proofs
        for p in new_proofs:
            self._proofs[p.conclusion].add(p)
        self.updated(new_proofs, False)
        return new_proofs

    def get_rules(self):
        """ Return a generator of all rules in working memory.
        These include not only user defined rules but also contrapositions.

        """
        for rule_set in self._wm.values():
            for r in rule_set:
                yield r

    def get_defeasible_rules(self):
        """ Return a generator of defeasible rules. """
        for r in self.get_rules():
            if isinstance(r, DefeasibleRule):
                yield r

    def get_strict_rules(self):
        """ Return a generator of strict rules. """
        for r in self.get_rules():
            if isinstance(r, StrictRule):
                yield r

    def get_rule_with_name(self, name):
        """ Return a rule with given name or None. """
        for r in self.get_rules():
            if r.name == name:
                return r
        return None

    def rules_with_consequent(self, consequent):
        """ Return all rules with the given consequent or None. """
        if isinstance(consequent, str):
            consequent = Literal.from_str(consequent)
        return self._wm[consequent]

    def get_proofs_for_rule(self, rule):
        """ Return a proofs that uses `rule` as the top rule or `set()`."""
        # only look at the proofs with the same consequent
        result = set()
        for p in self._proofs[rule.consequent]:
            if p.rule == rule:
                result.add(p)
        return result

    def add_ordering(self, ordering):
        """ Parse the line containing names of rules and their preferences.
        format: r1, r2 < r3 < r4, r5 ...
        Throws ParseError when the format is wrong and
        KnowledgeBaseError when preferences are inconsistent.

        """
        # TODO: how to best report failure? pass up?
        logger.debug('Adding preferences: %s' % str(ordering))
        for a, b in ordering.data:
            self.add_preference_rule(a, b, direction=ordering.direction)
        self.ordering_changed()
        self.recalculate()

    def del_ordering(self, ordering):
        """ Remove the given orderings. """
        # TODO: how to best report failure? pass up?
        for a, b in ordering.data:
            self.del_preference_rule(a, b, direction=ordering.ord)
        self.ordering_changed()
        self.recalculate()

    def add_preference_rule(self, lower, higher, direction):
        """ Insert preferences for defeasible rules.
        lower, higher - iterable of rule names; higher is preferred over lower.

        """
        # self._prefs stores the preferences (partial order) as a DAG
        # NOTE: the order is specified across defeasible rule names
        # on inserting, check that we are not creating inconsistencies
        #   and raise KBError if we are
        if direction == '<':
            edges = list(itertools.product(higher, lower))
        else:
            edges = list(itertools.product(lower, higher))
        logger.debug('  preference edges: %s' % str(edges))
        # be exception safe - first check for consistency and then add
        tmp = copy.deepcopy(self._prefs)
        for e in edges:
            po = tmp.find_path(e[1], e[0])  # possible pref order (path)
            # if po exists than this edge is inconsistent
            if po is not None:
                # inconsistency - be nice and include extra info
                ps = (' ' + direction + ' ').join(map(str, po))
                msg = ('The preference rule "%s %s %s" is not consistent with'
                       'the existing preference order: %s' %
                       (e[0], direction, e[1], ps))
                raise KnowledgeBaseError(msg)
            # if the rule is consistent, tentatively add it
            tmp.add_edge(*e)
        # all edges are consistent with respect to
        #   the existing prefs and each other
        for e in edges:
            logger.debug('  Adding preference: %s > %s' % e)
            self._prefs.add_edge(*e)

    def del_preference_rule(self, lower, higher, direction):
        """ Delete the pair of names from preferences. """
        if direction == '<':
            edges = list(itertools.product(higher, lower))
        else:
            edges = list(itertools.product(lower, higher))
        logger.debug('Deleting preference rule {0}'.format(repr(edges)))
        try:
            for e in edges:
                logger.debug('Deleting "{0}"'.format(repr(e)))
                self._prefs.del_edge(*e)
            return True
        except KeyError:
            return False

    def more_preferred(self, rule_a, rule_b):
        """ Return True if rule 'a' is more preferred than rule 'b'. """
        # a is preferred over b if there is a path from a to b
        a = rule_a.name if not isinstance(rule_a, str) else rule_a
        b = rule_b.name if not isinstance(rule_b, str) else rule_b
        path = self._prefs.find_path(a, b)
        return path is not None and path != [a]

    def less_preferred(self, rule_a, rule_b):
        """ Return True if rule 'a' is less preferred than rule 'b'. """
        return self.more_preferred(rule_b, rule_a)

    def preference_order(self, rule_a, rule_b):
        """ Return the order of preferences between rule_a and rule_b or None. """
        return self._prefs.find_path(rule_a.name, rule_b.name)

    def has_preference_for(self, rule):
        """ Return True if the rule has any preference weight. """
        return rule.name in self._prefs

    def check_consistency(self, proofs):
        """ Check that none of the strict proofs interferes
        with the existing knowledge base.

        """
        for p in proofs:
            # consistency only applies to strict proofs
            if not p.is_strict:
                continue
            if -p.consequent in self._proofs:
                counterproofs = self._proofs[-p.consequent]
                for cp in counterproofs:
                    if cp.is_strict:
                        # cp is a strict proof with an opposite conclusion
                        # which is not consistent with the proof p
                        msg = ('The proof "%s" is inconsistent with an existing'
                               ' proof "%s"' % (str(p), str(cp)))
                        raise KnowledgeBaseError(msg)

    def generate_proof_name(self):
        """ Return a name for an argument. """
        name = 'P%d' % self.proof_idx
        self.proof_idx += 1
        return name

    def save_into_file(self, file_name):
        with open(file_name, "w") as f:
            for consequent, rules in self._rules.items():
                f.write('# rules with consequent "%s":\n' % str(consequent))
                for r in rules:
                    f.write(str(r) + '\n')
            for k, vs in self._prefs.items():
                if vs:
                    f.write('{k} < {vs}\n'.format(k=k, vs=', '.join(vs)))

    def read_file(self, file_name):
        with open(file_name, "r") as f:
            self.parse_file(f)

    def parse_file(self, file):
        line_no = 0
        self.batch = True
        for line in file:
            line_no += 1
            line = line.partition('#')[0].strip()  # remove comments
            if line == '':
                continue
            try:
                self.add_rule(line)
            except Exception as e:
                msg = 'Exception on line %d: %s'
                logger.exception(msg % (line_no, str(e)))
        self.batch = False
        proofs = self.recalculate()
        self.check_consistency(proofs)

def check_list_of_type(lst, cls, msg=''):
    """ Check that the given list contains only instances of cls.
    Raise TypeError if an element is not of the given type.
    If the list is None, an empty list is returned.

    :param lst: list of items or None
    :param cls: the class of which the elements should be instances
    :param msg: error message used in exception
    :returns: the passed list
    """
    if lst is None:
        return []
    for o in lst:
        if not isinstance(o, cls):
            if not msg:
                msg = ('Elements of the list must be instances of {cls}'
                       .format(cls=str(cls)))
            raise TypeError(msg)
    if not isinstance(lst, list):
        return list(lst)
    return lst

def print_proofs(proofs):
    for c, ps in proofs.items():
        print(str(c) + ':')
        for p in ps:
            print('  ' + str(p))

def mk_rule(rule):
    """ Take a string and create an a Strict or a Defeasible rule. """
    if isinstance(rule, str):
        return _rule_grammar.parseString(rule.strip())[0]
    elif isinstance(rule, StrictRule):
        return rule
    elif isinstance(rule, DefeasibleRule):
        return rule
    elif isinstance(rule, Ordering):
        return rule
    else:
        msg = 'mk_rule expects a string but received "%s"'
        raise TypeError(msg % repr(rule))

# ########################## parsing related functions ######################## #

_literal = Group(Optional(Word('-')) + Word(alphas, alphanums + '_'))
_literal.setParseAction(Literal.from_parsed)

_literals = delimitedList(_literal)
_antecedent = _literals
_vulnerabilities = _literals
_consequent = _literal
_rule_name = Word(alphas + '_', alphanums + '_')
_rule_names = delimitedList(_rule_name)

_strict_rule = (Optional(_rule_name.setResultsName("name") + ':') +
                Optional(Group(_antecedent).setResultsName("antecedent")) +
                "-->" + Group(_consequent).setResultsName("consequent"))

_strict_rule.setParseAction(StrictRule.from_parsed)

_defeasible_rule = (Optional(_rule_name.setResultsName("name") + ':') +
                    Optional(Group(_antecedent).setResultsName("antecedent")) + '=' +
                    Optional('(' + Group(_vulnerabilities).setResultsName("vulnerabilities") + ')') +
                    '=>' + Group(_consequent).setResultsName("consequent"))

_defeasible_rule.setParseAction(DefeasibleRule.from_parsed)

def _mk_l_ordering(_, __, tokens):
    return Ordering.from_parsed(tokens, '<')

def _mk_r_ordering(_, __, tokens):
    return Ordering.from_parsed(tokens, '>')

_lt, _gt = map(Suppress, '<>')
_l_orderings = Group(_rule_names) + OneOrMore(_lt + Group(_rule_names))
_l_orderings.setParseAction(_mk_l_ordering)
_r_orderings = Group(_rule_names) + OneOrMore(_gt + Group(_rule_names))
_r_orderings.setParseAction(_mk_r_ordering)

_orderings = _l_orderings | _r_orderings

_rule_grammar = _strict_rule | _defeasible_rule | _orderings | _literal

# ############################################################################## #


from typing import Any, Dict, Iterable, Optional

import torch
from omegaconf import DictConfig
from torch import Tensor

from eztorch.losses.mocov3_loss import compute_mocov3_loss
from eztorch.models.siamese.momentum_base import MomentumSiameseBaseModel
from eztorch.modules.gather import concat_all_gather_without_backprop

class MoCov3Model(MomentumSiameseBaseModel):
    """MoCov3 that can be configured as in the paper.

    References:
        - MoCov3: https://arxiv.org/abs/2104.02057

    Args:
        trunk: Config to build a trunk.
        optimizer: Config to build optimizers and schedulers.
        projector: Config to build a project.
        predictor: Config to build a predictor.
        train_transform: Config to perform transformation on train input.
        val_transform: Config to perform transformation on val input.
        test_transform: Config to perform transformation on test input.
        normalize_outputs: If ``True``, normalize outputs.
        num_global_crops: Number of global crops which are the first elements of each batch.
        num_local_crops: Number of local crops which are the last elements of each batch.
        num_splits: Number of splits to apply to each crops.
        num_splits_per_combination: Number of splits used for combinations of features of each split.
        mutual_pass: If ``True``, perform one pass per branch per crop resolution.
        initial_momentum: Initial value for the momentum update.
        scheduler_momentum: Rule to update the momentum value.
        temp: Temperature parameter to scale the online similarities.
    """

    def __init__(
        self,
        trunk: DictConfig,
        optimizer: DictConfig,
        projector: Optional[DictConfig] = None,
        predictor: Optional[DictConfig] = None,
        train_transform: Optional[DictConfig] = None,
        val_transform: Optional[DictConfig] = None,
        test_transform: Optional[DictConfig] = None,
        normalize_outputs: bool = True,
        num_global_crops: int = 2,
        num_local_crops: int = 0,
        num_splits: int = 0,
        num_splits_per_combination: int = 2,
        mutual_pass: bool = False,
        initial_momentum: int = 0.99,
        scheduler_momentum: str = "cosine",
        temp: float = 1.0,
    ) -> None:
        super().__init__(
            trunk=trunk,
            optimizer=optimizer,
            projector=projector,
            predictor=predictor,
            train_transform=train_transform,
            val_transform=val_transform,
            test_transform=test_transform,
            normalize_outputs=normalize_outputs,
            num_global_crops=num_global_crops,
            num_local_crops=num_local_crops,
            num_splits=num_splits,
            num_splits_per_combination=num_splits_per_combination,
            mutual_pass=mutual_pass,
            initial_momentum=initial_momentum,
            scheduler_momentum=scheduler_momentum,
        )

        self.save_hyperparameters()

        assert not self.use_split, "Splits not supported for MoCov3"

        self.temp = temp

    def compute_loss(self, q: Tensor, k: Tensor) -> Tensor:
        """Compute the MoCo loss.

        Args:
            q: The representations of the queries.
            k: The representations of the keys.

        Returns:
            The loss.
        """

        k_global = concat_all_gather_without_backprop(k)

        return compute_mocov3_loss(
            q, k_global, self.device, self.temp, self.global_rank
        )

    def on_train_epoch_start(self) -> None:
        super().on_train_epoch_start()

        self.log("pretrain/temp", self.temp, on_step=False, on_epoch=True)

    def training_step(self, batch: Iterable[Any], batch_idx: int) -> Dict[str, Tensor]:
        X = batch["input"]
        X = [X] if isinstance(X, Tensor) else X

        assert len(X) == self.num_crops

        if self.train_transform is not None:
            with torch.no_grad():
                with torch.cuda.amp.autocast(enabled=False):
                    X = self.transform(X)

        outs_online = self.multi_crop_shared_step(X)
        outs_momentum = self.multi_crop_momentum_shared_step(X[: self.num_global_crops])

        tot_loss = 0
        for i in range(self.num_global_crops):
            for j in range(self.num_crops):
                if i == j:
                    continue
                loss = self.compute_loss(outs_online[j]["q"], outs_momentum[i]["z"])
                tot_loss += loss

        outputs = {"loss": tot_loss}
        # Only keep outputs from first computation to avoid unnecessary time and memory cost.
        outputs.update(outs_online[0])
        for name_output, output in outputs.items():
            if name_output != "loss":
                outputs[name_output] = output.detach()

        self.log(
            "pretrain/loss", outputs["loss"], prog_bar=True, on_step=True, on_epoch=True
        )

        return outputs


import os
import sys

from util.excel.resolver import resolve_excel, ExcelResolveInfo, ExcelTable, unwrap_cell_obj
from util.excel.creator import ExcelFileInfo, SheetInfo, create_excel

glass_wall_head = {
    "Project": lambda x: x,
    "Task": lambda x: x[8:],
    "Status": lambda x: f" ({x})"
}

def generate_glass_wall(table: ExcelTable):
    xf_list = table.xf_list
    font_list = table.font_list
    head_used = {}
    for i, head in enumerate(list(map(unwrap_cell_obj, table.head))):
        if isinstance(head, str) and glass_wall_head.get(head):
            head_used[i] = head

    glass_wall_result = []
    for row in table:
        row_text = {}
        for i, cell_obj in enumerate(row):
            cell, xf = cell_obj
            font_index = xf_list[xf].font_index

            header = head_used.get(i)
            if not (font_list[font_index].weight == 700 and header) or not cell.value:
                continue
            row_text[header] = cell.value
        if not row_text.__len__() or row_text.__len__() != 3:
            continue
        glass_wall_result.append(row_text)
    return glass_wall_result

def generate_glass_wall_summary(table: ExcelTable):
    home_address = os.getenv("HOME")
    os.chdir(home_address)
    if not (os.path.exists("generate_xlxs") and os.path.isdir("generate_xlxs")):
        os.mkdir("generate_xlxs")
    data = generate_glass_wall(table)
    print(data)


import os
import re
import sys

def print_tree(top, prefix=""):
    """Print paths for file tree recursively."""
    print(prefix + os.path.basename(top))
    prefix = re.sub(r"[-`]", " ", prefix)
    try:
        children = os.listdir(top)
    except OSError:
        return  # This is a file, not a directory
    if not children:
        return  # Empty directory
    children = sorted(os.path.join(top, name) for name in children)
    for name in children[:-1]:
        print_tree(name, (prefix + "|-- "))
    print_tree(children[-1], (prefix + "`-- "))

if __name__ == "__main__":
    directory = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    print_tree(directory)

exercises/Modern python tips and tricks/tree.py


def dfs(arr, x, y, s, idx):
    if idx == len(s):
        return 1
    count = 0
    if x - 1 >= 0 and arr[x - 1][y] == s[idx]:
        count += dfs(arr, x - 1, y, s, idx + 1)
    if x + 1 < len(arr) and arr[x + 1][y] == s[idx]:
        count += dfs(arr, x + 1, y, s, idx + 1)
    if y - 1 >= 0 and arr[x][y - 1] == s[idx]:
        count += dfs(arr, x, y - 1, s, idx + 1)
    if y + 1 < len(arr[0]) and arr[x][y + 1] == s[idx]:
        count += dfs(arr, x, y + 1, s, idx + 1)
    return count

C = 'CHINA'
def dfs1(arr, x, y, c):
    count = 0
    if not c:
        return 1
    n = len(arr)
    move = [[-1, 0], [1, 0], [0,1], [0,-1]]
    for i, j in move:
        tx, ty = x+i, y+j
        if 0<=tx<n and 0<= ty < n and arr[tx][ty] == c[0]:
            count += dfs1(arr, tx, ty, c[1:])
    return count

if __name__ == '__main__':
    n = int(input())
    arr = []
    for i in range(n):
        a = input()
        tmp = []
        for c in a:
            tmp.append(c)
        arr.append(tmp)

    count = 0
    for i in range(len(arr)):
        for j in range(len(arr[0])):
            if arr[i][j] == 'C':
                count += dfs1(arr, i, j, C[1:])
    print(count)

# 9
# AAAAAAAAA
# AAAANAAAA
# AAANINAAA
# AANIHINAA
# ANIHCHINA
# AANIHINAA
# AAANINAAA
# AAAANAAAA
# AAAAAAAAA
# 7
# NNNNNNN
# NNNINNN
# NNIHINN
# NIHCHIN
# NNIHINN
# NNNINNN
# NNNNNNN


#------------------------------------------------------------------------------
#  Aniket Pratap
#  1825275
#  CSE 30-02 Spring 2021
#  pa4
#  ContinuedFractions.py
#------------------------------------------------------------------------------
from rational import *
from decimal import *
import sys

def CF2R(L):
    if len(L) == 1:
        return Rational(L[0])
    elif len(L) > 1:
        return Rational(L[0]) + (Rational(1) / CF2R(L[1:]))

def usage():
    sys.stderr.write('Usage: $ python3 ContinuedFractions.py <input file> <output file>')

def main():
    if len(sys.argv) != 3:
        usage()
    else:
        try:
            getcontext().prec = 100
            in_file = open(sys.argv[1])
            outfile = open(sys.argv[2], 'w')
            lines = in_file.readlines()
            print('', file=outfile)
            for S in lines:
                L = S.split()
                R = list(map(int, L))
                A = CF2R(R)
                a = Decimal(A._numer) / Decimal(A._denom)
                print(A, file=outfile)
                print(a, file=outfile)
                print('', file=outfile)

        except FileNotFoundError as e:
            print(e, file=sys.stderr)
            usage()

if __name__ == '__main__':
    main()


import fileinput
import numpy as np

grid = np.zeros((1000,1000), 'int32')

for line in fileinput.input():
    data = line.strip()
    _l = data.split()

    if _l[0] == 'turn':
        x,y = _l[2].split(',')
        _x,_y = _l[4].split(',')
        x,y,_x,_y = int(x), int(y), int(_x), int(_y)

        if _l[1] == 'on':
            grid[x:_x+1, y:_y+1] += 1
        else:
            assert _l[1] == 'off'
            grid[x:_x+1,y:_y+1] -= 1
            grid[grid<0] = 0
    else:
        assert _l[0] == 'toggle'
        x,y = _l[1].split(',')
        _x,_y = _l[3].split(',')
        x,y,_x,_y = int(x), int(y), int(_x), int(_y)
        grid[x:_x+1, y:_y+1] += 2

print(np.sum(grid))


import json
from os import remove
from pathlib import Path

from polystar.models.box import Box
from polystar.models.image import load_image
from polystar.utils.path import copy_file, move_file
from research.common.datasets.roco.roco_annotation import ROCOAnnotation
from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder
from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo
from research.constants import TWITCH_DSET_DIR, TWITCH_ROBOTS_VIEWS_DIR
from research.dataset.twitch.mask_detector import has_bonus_icon, is_aerial_view

AERIAL_DIR = TWITCH_DSET_DIR / "v2" / "aerial-views"
RUNES_DIR = TWITCH_DSET_DIR / "v2" / "runes"

def match_on_dataset(builder: ROCODatasetBuilder):
    twitch_id = builder.main_dir.name
    hd_images_directory = TWITCH_ROBOTS_VIEWS_DIR / twitch_id

    dataset_v2_directory = TWITCH_DSET_DIR / "v2" / twitch_id

    _move_images_with_720p_annotations(builder, dataset_v2_directory, hd_images_directory, twitch_id)
    _copy_changes_locks(builder, dataset_v2_directory)
    _move_aerials_views(hd_images_directory)

def _move_images_with_720p_annotations(
    builder: ROCODatasetBuilder, dataset_v2_directory: Path, hd_images_directory: Path, twitch_id: str
):
    dataset = builder.build_lazy()
    missing_images = []
    for image_file, annotation, _ in dataset:
        hd_image_file = hd_images_directory / image_file.name
        if hd_image_file.exists():
            hd_image = load_image(hd_image_file)
            if has_bonus_icon(hd_image):
                remove(hd_image_file)
                continue
            elif is_aerial_view(hd_image):
                directory = AERIAL_DIR
            elif annotation.has_rune:
                directory = RUNES_DIR
            else:
                directory = dataset_v2_directory
            move_file(hd_image_file, directory / "image")
            _scale_annotation(annotation, height=1080, width=1920)
            annotation.save_in_directory(directory / "image_annotation", image_file.stem)
        else:
            missing_images.append(str(image_file))
    print(f"{len(missing_images)} missing images in {twitch_id}")
    (hd_images_directory / "missing.json").write_text(json.dumps(missing_images))

def _scale_annotation(annotation: ROCOAnnotation, height: int, width: int):
    vertical_ratio, horizontal_ratio = height / annotation.h, width / annotation.w

    for obj in annotation.objects:
        obj.box = Box.from_positions(
            x1=int(obj.box.x1 * horizontal_ratio),
            y1=int(obj.box.y1 * vertical_ratio),
            x2=int(obj.box.x2 * horizontal_ratio),
            y2=int(obj.box.y2 * vertical_ratio),
        )

    annotation.w, annotation.h = width, height

def _copy_changes_locks(builder, dataset_v2_directory):
    for task in ("colors", "digits"):
        changes_lock = builder.main_dir / f"{task}/.changes"
        if changes_lock.exists():
            copy_file(changes_lock, dataset_v2_directory / task)

def _move_aerials_views(hd_images_directory):
    for hd_image_file in hd_images_directory.glob("*.jpg"):
        image = load_image(hd_image_file)
        if has_bonus_icon(image):
            remove(hd_image_file)
        elif is_aerial_view(image):
            move_file(hd_image_file, AERIAL_DIR / "unannotated_image")

if __name__ == "__main__":
    for _builder in ROCODatasetsZoo.TWITCH:
        match_on_dataset(_builder)
    for _new_twitch_id in ("470149066", "470152932"):
        _move_aerials_views(TWITCH_ROBOTS_VIEWS_DIR / _new_twitch_id)


s=input()
k=0
out=[0]
while(k<len(s)):
    if(out[len(out)-1]==s[k]):
        out.pop()
    else:
        out.append(s[k])
    k+=1
u=1
if(len(out)==1):
    print("Empty String")
else:
    while(u<len(out)):
        print(out[u],end="")
        u+=1


from typing import Optional

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

import sys
import shutil
import logging

import cv2
import numpy as np
import onnx
from onnx_tf.backend import prepare
import torch
import tensorflow as tf

import numpy as np
import random

def representative_dataset():
  for _ in range(100):
  #data = random.randint(0, 1)
  #yield [data]
    data = np.random.rand(32)*2
    yield [data.astype(np.float32)]

class Torch2TFLiteConverter:
    def __init__(
            self,
            onxx_model_path: str,
            tflite_model_save_path: str,
            sample_file_path: Optional[str] = None,
            target_shape: tuple = (224, 224, 3),
            seed: int = 10,
            normalize: bool = True
    ):
        self.onxx_model_path = onxx_model_path
        self.tflite_model_path = tflite_model_save_path
        self.sample_file_path = sample_file_path
        self.target_shape = target_shape
        self.seed = seed
        self.normalize = normalize

        self.tmpdir = '~/tmp/'
        self.__check_tmpdir()
        self.tf_model_path = os.path.join(self.tmpdir, 'tf_model')
        self.sample_data = self.load_sample_input(sample_file_path, target_shape, seed, normalize)

    def convert(self):
        self.onnx2tf()
        self.tf2tflite()

    def __check_tmpdir(self):
        try:
            if os.path.exists(self.tmpdir) and os.path.isdir(self.tmpdir):
                shutil.rmtree(self.tmpdir)
                logging.info(f'Old temp directory removed')
            os.makedirs(self.tmpdir, exist_ok=True)
            logging.info(f'Temp directory created at {self.tmpdir}')
        except Exception:
            logging.error('Can not create temporary directory, exiting!')
            sys.exit(-1)

    def load_tflite(self):

        interpret = tf.lite.Interpreter(self.tflite_model_path)
        interpret.allocate_tensors()
        logging.info(f'TFLite interpreter successfully loaded from, {self.tflite_model_path}')
        return interpret

    @staticmethod
    def load_sample_input(
            file_path: Optional[str] = None,
            target_shape: tuple = (224, 224, 3),
            seed: int = 10,
            normalize: bool = True
    ):
        if file_path is not None:
            if (len(target_shape) == 3 and target_shape[-1] == 1) or len(target_shape) == 2:
                imread_flags = cv2.IMREAD_GRAYSCALE
            elif len(target_shape) == 3 and target_shape[-1] == 3:
                imread_flags = cv2.IMREAD_COLOR
            else:
                imread_flags = cv2.IMREAD_ANYCOLOR + cv2.IMREAD_ANYDEPTH
            try:
                img = cv2.resize(
                    src=cv2.imread(file_path, imread_flags),
                    dsize=target_shape[:2],
                    interpolation=cv2.INTER_LINEAR
                )
                if len(img.shape) == 3:
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

                if normalize:
                    img = img * 1. / 255
                img = img.astype(np.float32)

                sample_data_np = np.transpose(img, (2, 0, 1))[np.newaxis, :, :, :]
                sample_data_torch = torch.from_numpy(sample_data_np)
                logging.info(f'Sample input successfully loaded from, {file_path}')

            except Exception:
                logging.error(f'Can not load sample input from, {file_path}')
                sys.exit(-1)

        else:
            logging.info(f'Sample input file path not specified, random data will be generated')
            np.random.seed(seed)
            data = np.random.random(target_shape).astype(np.float32)
            sample_data_np = np.transpose(data, (2, 0, 1))[np.newaxis, :, :, :]
            sample_data_torch = torch.from_numpy(sample_data_np)
            logging.info(f'Sample input randomly generated')

        return {'sample_data_np': sample_data_np, 'sample_data_torch': sample_data_torch}

    def onnx2tf(self) -> None:
        onnx_model = onnx.load(self.onxx_model_path)
        onnx.checker.check_model(onnx_model)
        tf_rep = prepare(onnx_model)
        tf_rep.export_graph(self.tf_model_path)

    def tf2tflite(self) -> None:
        converter = tf.lite.TFLiteConverter.from_saved_model(self.tf_model_path)
        converter.optimizations = [tf.lite.Optimize.DEFAULT]
        converter.representative_dataset = representative_dataset
        converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
        converter.target_spec.supported_types = [tf.int8]
        converter.inference_input_type = tf.int8 # or tf.uint8
        converter.inference_output_type = tf.int8 # or tf.uint8

        tflite_model = converter.convert()
        with open(self.tflite_model_path, 'wb') as f:
            f.write(tflite_model)

    def inference_torch(self) -> np.ndarray:
        y_pred = self.torch_model(self.sample_data['sample_data_torch'])
        return y_pred.detach().cpu().numpy()

    def inference_tflite(self, tflite_model) -> np.ndarray:
        input_details = tflite_model.get_input_details()
        output_details = tflite_model.get_output_details()
        tflite_model.set_tensor(input_details[0]['index'], self.sample_data['sample_data_np'])
        tflite_model.invoke()
        y_pred = tflite_model.get_tensor(output_details[0]['index'])
        return y_pred

    @staticmethod
    def calc_error(result_torch, result_tflite):
        mse = ((result_torch - result_tflite) ** 2).mean(axis=None)
        mae = np.abs(result_torch - result_tflite).mean(axis=None)
        logging.info(f'MSE (Mean-Square-Error): {mse}\tMAE (Mean-Absolute-Error): {mae}')

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--onxx-path', type=str, required=True)
    parser.add_argument('--tflite-path', type=str, required=True)
    parser.add_argument('--target-shape', type=tuple, nargs=3, default=(224, 224, 3))
    parser.add_argument('--sample-file', type=str)
    parser.add_argument('--seed', type=int, default=10)
    args = parser.parse_args()

    conv = Torch2TFLiteConverter(
        args.onxx_path,
        args.tflite_path,
        args.sample_file,
        args.target_shape,
        args.seed
    )
    conv.convert()
    sys.exit(0)