#!/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.

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

import gi
gi.require_version("GLib", "2.0")
from gi.repository import GLib
gi.require_version("GObject", "2.0")
from gi.repository import GObject
gi.require_version("Gio", "2.0")
from gi.repository import Gio
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

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

import nxt.locator

from usb.core import USBError
from nxt.locator import BrickNotFoundError
from nxt.error import FileExistsError, SystemProtocolError


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 Exsts")
    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\nPlease 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://github.com/schodet/nxt-python/wiki")
        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)
