#!/usr/bin/env python3
#
# nxt_filemgr program -- Updated from nxt_filer
# Based on: nxt_filer program -- Simple GUI to manage files on a LEGO MINDSTORMS NXT
# Copyright (C) 2006  Douglas P Lau
# Copyright (C) 2010  rhn
# Copyright (C) 2017  TC Wan
# Copyright (C) 2018 David Lechner <david@lechnology.com>
#
# 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.

import os.path
import shutil
import sys
import urllib.request

import gi
from usb.core import USBError

import nxt.locator
from nxt.error import FileExistsError, SystemProtocolError
from nxt.locator import BrickNotFoundError

gi.require_version("GLib", "2.0")
gi.require_version("Gio", "2.0")
gi.require_version("Gtk", "3.0")

from gi.repository import Gio, GLib, Gtk  # noqa: E402

VERSION = "2.0"
PROTOCOL_VER = (1, 124)

COPYRIGHT = "2006-2018"
AUTHORS = ["Douglas P. Lau", "rhn", "TC Wan", "David Lechner"]

FILENAME_MINWIDTH = 100
FILENAME_MAXWIDTH = 300
FILENAME_WIDTH = 200
FILESIZE_MINWIDTH = 40
FILESIZE_MAXWIDTH = 120
FILESIZE_WIDTH = 80
FILELIST_HEIGHT = 300

WIN_WIDTH = 800
WIN_HEIGHT = 600

FILELIST_MINWIDTH = 1.0 * WIN_WIDTH
FILELIST_MAXWIDTH = 1.0 * WIN_WIDTH


def play_soundfile(b, fname):
    b.play_sound_file(0, fname)


def run_program(b, fname):
    b.start_program(fname)


def delete_file(b, fname):
    b.file_delete(fname)


def read_file(b, fname):
    with b.open_file(fname, "rb") as r:
        with open(fname, "wb") as f:
            shutil.copyfileobj(r, f)


def file_exists_dialog(win, fname):
    fileexists_str = "Cannot add %s!" % fname
    dialog = Gtk.MessageDialog(
        win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "File Exists"
    )
    dialog.format_secondary_text(fileexists_str)
    dialog.run()
    dialog.destroy()


def system_error_dialog(win):
    dialog = Gtk.MessageDialog(
        win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "System Error"
    )
    dialog.format_secondary_text(
        "Insufficient Contiguous Space!\n\n"
        "Please delete files to create contiguous space."
    )
    dialog.run()
    dialog.destroy()


def usb_error_dialog(win):
    dialog = Gtk.MessageDialog(
        win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "USB Error"
    )
    dialog.format_secondary_text("Can't connect to NXT!\n\nIs the NXT powered off?")
    dialog.run()
    dialog.destroy()


def write_file(win, b, fname, data):
    try:
        with b.open_file(fname, "wb", len(data)) as w:
            w.write(data)
    except FileExistsError:
        file_exists_dialog(win, fname)
    except SystemProtocolError:
        system_error_dialog(win)
    except USBError:
        raise


def write_files(win, b, names):
    for fname in names.split("\r\n"):
        if fname:
            bname = os.path.basename(fname)
            url = urllib.request.urlopen(fname)
            try:
                data = url.read()
            finally:
                url.close()
            write_file(win, b, bname, data)


class NXTListing(Gtk.ListStore):
    def __init__(self):
        "Create an empty file list"
        Gtk.ListStore.__init__(self, str, str)
        self.set_sort_column_id(0, Gtk.SortType(Gtk.SortType.ASCENDING))

    def populate(self, brick, pattern):
        for fname, size in brick.find_files(pattern):
            self.append((fname, str(size)))


class ApplicationWindow(Gtk.ApplicationWindow):
    # FIXME
    # TARGETS = Gtk.target_list_add_uri_targets()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_default_size(WIN_WIDTH, WIN_HEIGHT)
        self.set_resizable(False)
        self.set_border_width(6)
        self.brick = None
        self.selected_file = None
        self.brick_info_str = "Disconnected"
        self.nxt_filelist = NXTListing()

        self.status_bar = Gtk.Statusbar()
        self.connect_status_id = self.status_bar.get_context_id("connect")
        self.connect_msg_id = None

        h = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
        self.brick_button = Gtk.Button(label="Connect")
        self.brick_button.connect("clicked", self.reload_filelist)
        h.pack_start(self.brick_button, False, True, 0)
        h.pack_start(self.make_file_panel(), True, True, 0)
        h.pack_start(self.make_button_panel(), False, True, 0)
        h.pack_end(self.status_bar, False, True, 0)
        self.add(h)
        self.reload_filelist(self.nxt_filelist)
        self.show_all()

    def make_file_view(self):
        tv = Gtk.TreeView()
        tv.set_headers_visible(True)
        tv.set_property("fixed_height_mode", True)
        r = Gtk.CellRendererText()
        c = Gtk.TreeViewColumn("File name", r, text=0)
        c.set_fixed_width(FILENAME_WIDTH)
        c.set_min_width(FILENAME_MINWIDTH)
        c.set_max_width(FILENAME_MAXWIDTH)
        c.set_resizable(True)
        c.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        tv.append_column(c)
        r = Gtk.CellRendererText()
        c = Gtk.TreeViewColumn("Bytes", r, text=1)
        c.set_resizable(True)
        c.set_fixed_width(FILESIZE_WIDTH)
        c.set_min_width(FILESIZE_MINWIDTH)
        c.set_max_width(FILESIZE_MAXWIDTH)
        c.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        tv.append_column(c)

        # FIXME
        # tv.enable_model_drag_source(Gtk.gdk.BUTTON1_MASK, self.TARGETS,
        # Gtk.gdk.ACTION_DEFAULT | Gtk.gdk.ACTION_MOVE)
        # tv.enable_model_drag_dest(self.TARGETS, Gtk.gdk.ACTION_COPY)
        # tv.connect("drag_data_get", self.drag_data_get_data)

        tv.connect("drag_data_received", self.drag_data_received_data)
        tv.connect("row_activated", self.row_activated_action)
        return tv

    def make_file_panel(self):
        v = Gtk.Box(homogeneous=Gtk.Orientation.VERTICAL, spacing=0)
        tv = self.make_file_view()
        tv.set_model(self.nxt_filelist)
        select = tv.get_selection()
        select.connect("changed", self.on_tree_selection_changed)

        s = Gtk.ScrolledWindow()
        s.set_min_content_width(FILELIST_MINWIDTH)
        s.set_min_content_height(FILELIST_HEIGHT)
        s.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        s.add(tv)
        s.set_border_width(2)
        v.pack_start(s, True, False, 0)
        return v

    def make_button_panel(self):
        vb = Gtk.Box(homogeneous=Gtk.Orientation.HORIZONTAL, spacing=0)

        self.delete_button = Gtk.Button(label="Remove")
        self.delete_button.connect("clicked", self.remove_file)
        vb.pack_start(self.delete_button, True, True, 10)

        self.add_button = Gtk.Button(label="Add")
        self.add_button.connect("clicked", self.add_file)
        vb.pack_start(self.add_button, True, True, 10)

        self.exec_button = Gtk.Button(label="Execute")
        self.exec_button.connect("clicked", self.execute_file)
        vb.pack_start(self.exec_button, True, True, 10)
        return vb

    def do_housekeeping(self):
        self.brick.close()
        self.brick = None
        self.selected_file = None

    def reload_filelist(self, widget):
        if self.brick:
            self.do_housekeeping()
        try:
            self.brick = nxt.locator.find()
        except BrickNotFoundError:
            print("Brick not found!")
        except USBError:
            usb_error_dialog(self)
            self.do_housekeeping()
        if self.brick:
            self.update_view()

    def update_view(self):
        self.get_brick_info()
        if self.connect_msg_id:
            self.status_bar.remove(self.connect_status_id, self.connect_msg_id)
        self.connect_msg_id = self.status_bar.push(
            self.connect_status_id, self.brick_info_str
        )
        self.nxt_filelist.clear()
        if self.brick:
            self.nxt_filelist.populate(self.brick, "*.*")

    def get_brick_info(self):
        if self.brick:
            print("Reading from NXT..."),
            prot_version, fw_version = self.brick.get_firmware_version()
            print("Protocol version: %s.%s" % prot_version)
            if prot_version == PROTOCOL_VER:
                (
                    brick_name,
                    brick_hostid,
                    brick_signal_strengths,
                    brick_user_flash,
                ) = self.brick.get_device_info()
                connection_type = str(self.brick._sock)
                self.brick_info_str = (
                    "Connected via %s to %s [%s]\tFirmware: v%s.%s\tFree space: %s"
                    % (
                        connection_type,
                        brick_name,
                        brick_hostid,
                        fw_version[0],
                        fw_version[1],
                        brick_user_flash,
                    )
                )
            else:
                print("Invalid Protocol version! Closing connection.")
                self.do_housekeeping()
        else:
            self.brick_info_str = "Disconnected"
        sys.stdout.flush()

    def choose_files(self):
        dialog = Gtk.FileChooserNative()
        dialog.new("Add File", self, Gtk.FileChooserAction.OPEN, None, None)
        dialog.set_transient_for(self)
        dialog.set_select_multiple(False)
        res = dialog.run()
        if res == Gtk.ResponseType.ACCEPT:
            # Handle single file download for now
            uri = dialog.get_uri()
            try:
                write_files(self, self.brick, uri)
            except USBError:
                # Cannot recover, close connection
                usb_error_dialog(self)
                self.do_housekeeping()
        dialog.destroy()

    def on_tree_selection_changed(self, selection):
        model, treeiter = selection.get_selected()
        if treeiter:
            self.selected_file = model[treeiter][0]
        else:
            self.selected_file = None

    def drag_data_get_data(self, treeview, context, selection, target_id, etime):
        treeselection = treeview.get_selection()
        model, iter = treeselection.get_selected()
        data = model.get_value(iter, 0)
        print(data)
        selection.set(selection.target, 8, data)

    def drag_data_received_data(self, treeview, context, x, y, selection, info, etime):
        if context.action == Gtk.gdk.ACTION_COPY:
            write_files(self, self.brick, selection.data)
        # FIXME: update file listing after writing files
        # FIXME: finish context

    def row_activated_action(self, treeview, context, selection):
        self.execute_file(selection)

    def execute_file(self, widget):
        if self.selected_file:
            name = ""
            ext = ""
            try:
                name, ext = self.selected_file.split(".")
                if ext == "rso":
                    play_soundfile(self.brick, self.selected_file)
                elif ext == "rxe":
                    run_program(self.brick, self.selected_file)
                    self.do_housekeeping()
                    self.update_view()
                else:
                    print("Can't execute '*.%s' files" % ext)
            except ValueError:
                print("No file extension (unknown file type)")
            except USBError:
                # Cannot recover, close connection
                usb_error_dialog(self)
                self.do_housekeeping()
                self.update_view()

    def remove_file(self, widget):
        if self.selected_file:
            warning_str = "Do you really want to remove %s?" % self.selected_file
            dialog = Gtk.MessageDialog(
                self,
                0,
                Gtk.MessageType.WARNING,
                Gtk.ButtonsType.OK_CANCEL,
                "Remove File",
            )
            dialog.format_secondary_text(warning_str)
            response = dialog.run()
            dialog.destroy()
            if response == Gtk.ResponseType.OK:
                try:
                    delete_file(self.brick, self.selected_file)
                except USBError:
                    # Cannot recover, close connection
                    usb_error_dialog(self)
                    self.do_housekeeping()
                self.selected_file = None
                self.update_view()

    def add_file(self, widget):
        if self.brick:
            self.choose_files()
            self.selected_file = None
            self.update_view()


class AboutDialog(Gtk.AboutDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_version(VERSION)
        self.set_copyright(COPYRIGHT)
        self.set_comments("LEGO® MINDSTORMS NXT File Manager")
        self.set_license("Released under GPL v2 or later")
        self.set_website("https://sr.ht/~ni/nxt-python/")
        self.set_website_label("Wiki")
        self.set_authors(AUTHORS)

        markup = "<span size='x-small'>{}</span>".format(
            "LEGO® is a trademark of the LEGO Group of companies which does not "
            "sponsor, authorize or endorse this software."
        )
        disclaimer = Gtk.Label()
        disclaimer.set_line_wrap(True)
        disclaimer.set_max_width_chars(10)
        disclaimer.set_markup(markup)
        disclaimer.show()
        self.get_content_area().pack_end(disclaimer, False, False, 6)

    def do_response(self, response):
        self.close()


class Application(Gtk.Application):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.window = None

        self.set_option_context_summary(
            "Simple GUI to manage files on a LEGO MINDSTORMS NXT"
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

        action = Gio.SimpleAction.new("about", None)
        action.connect("activate", self.on_about)
        self.add_action(action)

        action = Gio.SimpleAction.new("quit", None)
        action.connect("activate", self.on_quit)
        self.add_action(action)

        # on macOS, the menu is created automatically, but still uses the
        # actions above
        if app.prefers_app_menu():
            menu1 = Gio.Menu.new()
            menu1.append("_About", "app.about")

            menu2 = Gio.Menu.new()
            menu2.append("_Quit", "app.quit")
            app.set_accels_for_action("app.quit", ["<Control>q"])

            appMenu = Gio.Menu.new()
            appMenu.append_section(None, menu1)
            appMenu.append_section(None, menu2)
            app.set_app_menu(appMenu)

    def do_activate(self):
        if not self.window:
            self.window = ApplicationWindow(application=self)
        self.window.present()

    def on_about(self, action, param):
        about_dialog = AboutDialog(transient_for=self.window, modal=True)
        about_dialog.present()

    def on_quit(self, action, param):
        self.quit()


if __name__ == "__main__":
    GLib.set_prgname("nxt_filer")
    GLib.set_application_name("NXT File Manager")
    app = Application()
    app.run(sys.argv)
