#!/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
from nxt.telegram import OPCODES, Telegram


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,
                         flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
                         **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)
