"""
EDSM-RSE a plugin for EDMC
Copyright (C) 2017 Sebastian Bauer

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

import sys
import os
import time
import logging
import semantic_version

from queue import Queue
from urllib.parse import quote
from typing import Dict, Union

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.messagebox as tkMessageBox
import myNotebook as nb
from ttkHyperlinkLabel import HyperlinkLabel
from config import config, appname, appversion
from l10n import Locale

from RseData import RseData, EliteSystem
from Backgroundworker import BackgroundWorker
import BackgroundTask as BackgroundTask

logger = logging.getLogger(f"{appname}.{os.path.basename(os.path.dirname(__file__))}")
this = sys.modules[__name__]  # For holding module globals

this.edmc_has_logging_support = True

if not logger.hasHandlers():
    level = logging.INFO  # So logger.info(...) is equivalent to print()

    logger.setLevel(level)
    logger_channel = logging.StreamHandler()
    logger_formatter = logging.Formatter(f"%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d:%(funcName)s: %(message)s")
    logger_formatter.default_time_format = "%Y-%m-%d %H:%M:%S"
    logger_formatter.default_msec_format = "%s.%03d"
    logger_channel.setFormatter(logger_formatter)
    logger.addHandler(logger_channel)


this.CONFIG_IGNORED_PROJECTS = "EDSM-RSE_ignoredProjects"
this.CONFIG_MAIN = "EDSM-RSE"

this.rseData = None  # type: Union[RseData, None]
this.systemCreated = False  # initialize with false in case someone uses an older EDMC version that does not call edsm_notify_system()
this.enabled = False  # plugin configured correctly and therefore enabled
this.currentSystem = None  # type: Union[EliteSystem, None] # current system
this.commander = None  # name of current commander

this.worker = None  # type: Union[BackgroundWorker, None]
this.queue = None  # type: Union[Queue, None] # queue used by the background worker

# ui elements in options
this.debug = None  # Type: Union[tk.BooleanVar, None] # toggle debug messages to eddb log
this.clipboard = None  # type: Union[tk.BooleanVar, None] # copy system name to clipboard
this.overwrite = None  # type: Union[tk.BooleanVar, None] # overwrite disabled state (EDSM/EDDN disabled)
this.edsmBodyCheck = None  # type: Union[tk.BooleanVar, None] # in settings; compare total number of bodies to the number known to EDSM
this.systemScanned = False  # variable to prevent spamming the EDSM API
this.ignoredProjectsCheckboxes = dict()  # type: Dict[int, tk.BooleanVar]

# ui elements in main window
this.errorLabel = None  # type: Union[tk.Label, None] # show if plugin can't work (EDSM/EDDN disabled)
this.distanceValue = None  # type: Union[tk.Label, None] # distance to system
this.actionText = None  # type: Union[tk.Label, None] # task to do
this.edsmBodyFrame = None  # type: Union[tk.Frame, None] # frame containing all UI elements for EDSM body count
this.edsmBodyCountText = None  # type: Union[tk.Label, None] # text of information about bodies known to EDSM
this.unconfirmedSystem = None  # type: Union[RseHyperlinkLabel, None] # display name of system that needs checking
this.updateNotificationLabel = None  # type: Union[HyperlinkLabel, None]


class RseHyperlinkLabel(HyperlinkLabel):

    def __init__(self, master=None, **kw):
        super(RseHyperlinkLabel, self).__init__(master, **kw)
        if self.get_edmc_version() < semantic_version.Version('5.11.0'):
            self.menu.add_command(label=_("Ignore once"), command=self.ignore_once)
            self.menu.add_command(label=_("Ignore this session"), command=self.ignore_temporarily)
            self.menu.add_command(label=_("Ignore for 24 hours"), command=self.ignore_for24)
            self.menu.add_command(label=_("Ignore indefinitely"), command=self.ignore_indefinitely)

    @staticmethod
    def get_edmc_version():
        if isinstance(appversion, str):
            return semantic_version.Version(appversion)

        elif callable(appversion):
            # From 5.0.0-beta1 it's a function, returning semantic_version.Version
            return appversion()

    def _contextmenu(self, event: tk.Event) -> None:
        if self.get_edmc_version() < semantic_version.Version('5.11.0'):
            super()._contextmenu(event)
        else:
            menu = tk.Menu(tearoff=tk.FALSE)
            menu.add_command(label=_("Copy"), command=self.copy)  # As in Copy and Paste

            menu.add_separator()
            menu.add_command(label=_("Ignore once"), command=self.ignore_once)
            menu.add_command(label=_("Ignore this session"), command=self.ignore_temporarily)
            menu.add_command(label=_("Ignore for 24 hours"), command=self.ignore_for24)
            menu.add_command(label=_("Ignore indefinitely"), command=self.ignore_indefinitely)

            menu.post(event.x_root, event.y_root)

    def copy(self) -> None:
        """Copy the current text to the clipboard."""
        self.clipboard_clear()
        self.clipboard_append(self['text'])

    def ignore_once(self):
        this.queue.put(BackgroundTask.IgnoreSystemTask(this.rseData, self["text"], True))

    def ignore_temporarily(self):
        this.queue.put(BackgroundTask.IgnoreSystemTask(this.rseData, self["text"]))

    def ignore_for24(self):
        this.queue.put(BackgroundTask.IgnoreSystemTask(this.rseData, self["text"], False, int(time.time() + 24 * 3600)))

    def ignore_indefinitely(self):
        this.queue.put(BackgroundTask.IgnoreSystemTask(this.rseData, self["text"], False, 2 ** 31 - 1))


def check_transmission_options():
    if RseHyperlinkLabel.get_edmc_version() < semantic_version.Version('5.6.0-beta1'):
        eddn = (config.get_int("output") & config.OUT_SYS_EDDN) == config.OUT_SYS_EDDN
        edsm = config.get_int("edsm_out") and 1
    else:
        eddn = (config.get_int("output") & config.OUT_EDDN_SEND_NON_STATION) == config.OUT_EDDN_SEND_NON_STATION
        edsm = config.get_int("edsm_out") and 1
    return eddn or edsm


def plugin_start(plugin_dir):
    this.rseData = RseData(plugin_dir)
    settings = config.get_int(this.CONFIG_MAIN) or 0  # default setting
    this.rseData.ignored_projects_flags = config.get_int(this.CONFIG_IGNORED_PROJECTS)
    this.clipboard = tk.BooleanVar(value=((settings >> 5) & 0x01))
    this.overwrite = tk.BooleanVar(value=((settings >> 6) & 0x01))
    this.edsmBodyCheck = tk.BooleanVar(value=not ((settings >> 7) & 0x01))  # invert to be on by default
    this.debug = tk.BooleanVar(value=((settings >> 8) & 0x01))
    if this.debug.get():
        level = logging.DEBUG
        logger.setLevel(level)
        for handler in logger.handlers:
            handler.setLevel(level)
    
    this.enabled = check_transmission_options()

    this.queue = Queue()
    this.worker = BackgroundWorker(this.queue, this.rseData)
    this.worker.name = "EDSM-RSE Background Worker"
    this.worker.daemon = True
    this.worker.radiusExponent = RseData.DEFAULT_RADIUS_EXPONENT
    this.worker.start()

    logger.debug("Debug messages are enabled.")
    logger.debug("Python Version: {0}.".format(sys.version))
    return RseData.PLUGIN_NAME


def plugin_start3(plugin_dir):
    plugin_start(plugin_dir)


def update_ui_unconfirmed_system(event=None):
    elite_system = this.rseData.last_event_info.get(RseData.BG_RSE_SYSTEM, None)  # type: EliteSystem
    message = this.rseData.last_event_info.get(RseData.BG_RSE_MESSAGE, None)
    if (this.enabled or this.overwrite.get()) and elite_system:
        this.errorLabel.grid_remove()
        this.unconfirmedSystem.grid(row=0, column=1, sticky=tk.W)
        this.unconfirmedSystem["text"] = elite_system.name
        this.unconfirmedSystem["url"] = "https://www.edsm.net/show-system?systemName={}".format(quote(elite_system.name))
        this.unconfirmedSystem["state"] = "enabled"
        distance_text = u"{distance} Ly".format(distance=Locale.string_from_number(elite_system.distance, 2))
        if elite_system.uncertainty > 0:
            distance_text = distance_text + u" (\u00B1{uncertainty})".format(uncertainty=elite_system.uncertainty)
        this.distanceValue["text"] = distance_text
        this.actionText["text"] = elite_system.get_action_text()
        if this.clipboard.get():
            this.frame.clipboard_clear()
            this.frame.clipboard_append(elite_system.name)
    else:
        this.unconfirmedSystem.grid_remove()
        this.errorLabel.grid(row=0, column=1, sticky=tk.W)
        this.distanceValue["text"] = "?"
        this.actionText["text"] = "?"
        if not this.enabled and not this.overwrite.get():
            this.errorLabel["text"] = "EDSM/EDDN is disabled"
        else:
            this.errorLabel["text"] = message or "?"


def update_ui_edsm_body_count(event=None):
    message = this.rseData.last_event_info.get(RseData.BG_EDSM_BODY, None)
    if this.edsmBodyCheck.get():
        if message:
            this.edsmBodyCountText["text"] = message
        else:
            this.edsmBodyCountText["text"] = "?"
        this.edsmBodyFrame.grid(row=11, columnspan=2, sticky=tk.EW)
    else:
        this.edsmBodyFrame.grid_remove()


def plugin_close():
    # Signal thread to close and wait for it
    this.queue.put(None)
    this.worker.join()
    this.worker = None


def clear_scanned_systems_cache_callback(cache_type: int, name: str):
    # called when clicked on the clear cache of scanned systems button in settings
    result = tkMessageBox.askquestion("Delete " + name, "Do you really want to delete all {}?\nThis cannot be undone.".format(name), icon='warning')
    if result == tkMessageBox.YES:
        this.queue.put(BackgroundTask.DeleteSystemsFromCacheTask(this.rseData, cache_type))


def plugin_prefs(parent, cmdr, is_beta):
    PADX = 5

    frame = nb.Frame(parent)
    frame.columnconfigure(0, weight=1)

    nb.Checkbutton(frame, variable=this.edsmBodyCheck,
                   text="Display number of bodies known to EDSM in current system").grid(padx=PADX, sticky=tk.W)

    # enable projects
    ttk.Separator(frame, orient=tk.HORIZONTAL).grid(padx=PADX * 2, pady=8, sticky=tk.EW)
    nb.Label(frame, text="Please choose which projects to enable").grid(padx=PADX, sticky=tk.W)
    for rseProject in this.rseData.projects_dict.values():
        invertedFlag = not (this.rseData.ignored_projects_flags & rseProject.project_id == rseProject.project_id)
        variable = this.ignoredProjectsCheckboxes.setdefault(rseProject.project_id, tk.BooleanVar(value=invertedFlag))
        text = rseProject.name
        if not rseProject.enabled:
            text += " (globally disabled)"
        nb.Checkbutton(frame, variable=variable, text=text).grid(padx=PADX, sticky=tk.W)
        nb.Label(frame, text=rseProject.explanation).grid(padx=PADX * 4, sticky=tk.W)

    # overwrite disabled state when EDDN/EDSM is off in EDMC
    ttk.Separator(frame, orient=tk.HORIZONTAL).grid(padx=PADX * 2, pady=8, sticky=tk.EW)
    nb.Checkbutton(frame, variable=this.clipboard,
                   text="Copy system name to clipboard after jump").grid(padx=PADX, sticky=tk.W)
    nb.Checkbutton(frame, variable=this.overwrite,
                   text="I use another tool to transmit data to EDSM/EDDN").grid(padx=PADX, sticky=tk.W)

    # clear caches
    ttk.Separator(frame, orient=tk.HORIZONTAL).grid(padx=PADX * 2, pady=8, sticky=tk.EW)
    nb.Label(frame, text="Clear caches").grid(padx=PADX, sticky=tk.W)
    clearCachesFrame = nb.Frame(frame)
    clearCachesFrame.grid(padx=PADX * 2, pady=8, sticky=tk.EW)
    frame.columnconfigure(2, weight=1)
    nb.Button(clearCachesFrame, text="Fully scanned systems", command=lambda: clear_scanned_systems_cache_callback(RseData.CACHE_FULLY_SCANNED_BODIES, "fully scanned systems"))\
        .grid(padx=PADX, sticky=tk.W, row=0, column=0)
    nb.Button(clearCachesFrame, text="Ignored systems", command=lambda: clear_scanned_systems_cache_callback(RseData.CACHE_IGNORED_SYSTEMS, "ignored systems")) \
        .grid(padx=PADX, sticky=tk.W, row=0, column=1)

    # links
    ttk.Separator(frame, orient=tk.HORIZONTAL).grid(padx=PADX * 2, pady=8, sticky=tk.EW)
    nb.Label(frame, text="Plugin Version: {}".format(RseData.VERSION)).grid(padx=PADX, sticky=tk.W)
    if not this.edmc_has_logging_support:
        nb.Checkbutton(frame, variable=this.debug,
                       text="Verbose Logging").grid(padx=PADX, sticky=tk.W)
    HyperlinkLabel(frame, text="Open the Github page for this plugin", background=nb.Label().cget("background"),
                   url="https://github.com/Thurion/EDSM-RSE-for-EDMC", underline=True).grid(padx=PADX, sticky=tk.W)
    HyperlinkLabel(frame, text="A big thanks to EDTS for providing the coordinates.", background=nb.Label().cget("background"),
                   url="http://edts.thargoid.space/", underline=True).grid(padx=PADX, sticky=tk.W)
    return frame


def prefs_changed(cmdr, is_beta):
    # bits are as follows:
    # 0-3 radius # not used anymore
    # 4-5 interval, not used anymore
    # 6: copy to clipboard
    # 7: overwrite enabled status
    # 8: EDSM body check, value inverted
    # 9: Debug
    settings = (this.clipboard.get() << 5) | (this.overwrite.get() << 6) | ((not this.edsmBodyCheck.get()) << 7) | (this.debug.get() << 8)
    config.set(this.CONFIG_MAIN, settings)
    this.enabled = check_transmission_options()
    this.rseData.radius_exponent = RseData.DEFAULT_RADIUS_EXPONENT

    old_flags = this.rseData.ignored_projects_flags
    for k, v in this.ignoredProjectsCheckboxes.items():
        if not v.get():  # inverted, user wants to ignore this project
            this.rseData.ignored_projects_flags = this.rseData.ignored_projects_flags | k
        else:
            this.rseData.ignored_projects_flags = this.rseData.ignored_projects_flags & (0xFFFFFFFF - k)  # DWord = 32 bit

    if old_flags != this.rseData.ignored_projects_flags:
        this.rseData.radius = RseData.DEFAULT_RADIUS_EXPONENT  # reset radius just in case
        if this.currentSystem:
            this.rseData.system_list = list()  # clear list in case there is no system nearby
            this.queue.put(BackgroundTask.JumpedSystemTask(this.rseData, this.currentSystem))

    config.set(this.CONFIG_IGNORED_PROJECTS, this.rseData.ignored_projects_flags)

    if this.debug.get():
        level = logging.DEBUG
    else:
        level = logging.INFO
    logger.setLevel(level)
    for handler in logger.handlers:
        handler.setLevel(level)
    logger.debug("RSE Debug messages are enabled.")

    update_ui_unconfirmed_system()
    update_ui_edsm_body_count()


def show_update_notification(event=None):
    updateVersionInfo = this.rseData.last_event_info.get(RseData.BG_UPDATE_JSON, None)
    if updateVersionInfo:
        url = updateVersionInfo["url"]
        text = "Plugin update to {version} available".format(version=updateVersionInfo["version"])
    else:
        url = "https://github.com/Thurion/EDSM-RSE-for-EDMC/releases"
        text = "Plugin update available"
    this.updateNotificationLabel["url"] = url
    this.updateNotificationLabel["text"] = text
    this.updateNotificationLabel.grid(row=99, column=0, columnspan=2, sticky=tk.W)  # always put in last row


def plugin_app(parent):
    this.frame = tk.Frame(parent)
    this.frame.bind_all(RseData.EVENT_RSE_BACKGROUNDWORKER, update_ui_unconfirmed_system)
    this.frame.bind_all(RseData.EVENT_RSE_UPDATE_AVAILABLE, show_update_notification)
    this.frame.bind_all(RseData.EVENT_RSE_EDSM_BODY_COUNT, update_ui_edsm_body_count)

    this.rseData.set_frame(this.frame)

    this.frame.columnconfigure(1, weight=1)
    tk.Label(this.frame, text="Target:").grid(row=0, column=0, sticky=tk.W)
    this.unconfirmedSystem = RseHyperlinkLabel(this.frame, compound=tk.RIGHT, popup_copy=True)
    this.errorLabel = tk.Label(this.frame)
    tk.Label(this.frame, text="Distance:").grid(row=1, column=0, sticky=tk.W)
    this.distanceValue = tk.Label(this.frame)
    this.distanceValue.grid(row=1, column=1, sticky=tk.W)
    tk.Label(this.frame, text="Todo:").grid(row=2, column=0, sticky=tk.W)
    this.actionText = tk.Label(this.frame)
    this.actionText.grid(row=2, column=1, sticky=tk.W)

    this.edsmBodyFrame = tk.Frame(this.rseData.frame)
    this.edsmBodyFrame.columnconfigure(1, weight=1)
    tk.Frame(this.edsmBodyFrame, highlightthickness=1).grid(row=0, pady=3, columnspan=2, sticky=tk.EW)  # separator
    tk.Label(this.edsmBodyFrame, text="EDSM Bodies:").grid(row=1, column=0, sticky=tk.W)
    this.edsmBodyCountText = tk.Label(this.edsmBodyFrame)
    this.edsmBodyCountText["text"] = "?"
    this.edsmBodyCountText.grid(row=1, column=1, sticky=tk.W)

    this.updateNotificationLabel = HyperlinkLabel(this.frame, text="Plugin update available", background=nb.Label().cget("background"),
                                                  url="https://github.com/Thurion/EDSM-RSE-for-EDMC/releases", underline=True)
    update_ui_unconfirmed_system()
    update_ui_edsm_body_count()

    # start update check after frame is initialized to avoid any possible race conditions when generating the event
    this.queue.put(BackgroundTask.VersionCheckTask(this.rseData))

    return this.frame


def journal_entry(cmdr, is_beta, system, station, entry, state):
    if not this.enabled and not this.overwrite.get() or is_beta:
        return  # nothing to do here

    if this.commander != cmdr:
        # user switched commanders, reset the list of systems
        logger.debug("New commander detected: {cmdr}; resetting radius and clearing nearby systems.".format(cmdr=cmdr))
        this.commander = cmdr
        this.rseData.system_list = list()
        this.rseData.radius_exponent = RseData.DEFAULT_RADIUS_EXPONENT

    if entry["event"] in ["FSDJump", "Location", "CarrierJump", "StartUp"]:
        if entry["SystemAddress"] in this.rseData.get_cached_set(RseData.CACHE_FULLY_SCANNED_BODIES):
            this.edsmBodyCountText["text"] = "System complete"
            this.systemScanned = True
        else:
            this.edsmBodyCountText["text"] = "Use discovery scanner"
            this.systemScanned = False
        if "StarPos" in entry:
            this.currentSystem = EliteSystem(entry["SystemAddress"], entry["StarSystem"], *entry["StarPos"])
            this.queue.put(BackgroundTask.JumpedSystemTask(this.rseData, this.currentSystem))

    if entry["event"] == "Resurrect":
        # reset radius in case someone died in an area where there are not many available stars (meaning very large radius)
        this.rseData.system_list = list()
        this.rseData.radius = RseData.DEFAULT_RADIUS_EXPONENT

    if entry["event"] == "NavBeaconScan":
        this.queue.put(BackgroundTask.NavbeaconTask(this.rseData, entry["SystemAddress"]))

    if entry["event"] == "FSSDiscoveryScan" and this.edsmBodyCheck.get():
        if not this.systemScanned:
            if this.systemCreated:
                this.edsmBodyCountText["text"] = "0/{}".format(entry["BodyCount"])
            else:
                this.queue.put(BackgroundTask.FSSDiscoveryScanTask(this.rseData, system, entry["BodyCount"], entry["Progress"]))
        this.systemScanned = True

    if entry["event"] == "FSSAllBodiesFound":
        this.systemScanned = True
        this.queue.put(BackgroundTask.FSSAllBodiesFoundTask(this.rseData, entry["SystemAddress"], this.edsmBodyCheck.get()))


def edsm_notify_system(reply):
    if reply.get("systemCreated"):
        this.systemCreated = True
    else:
        this.systemCreated = False
