#!/usr/bin/env python3

#    Copyright (C) 2013-2016 Christian T. Jacobs.

#    This file is part of PyQSO.

#    PyQSO 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 3 of the License, or
#    (at your option) any later version.
#
#    PyQSO 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 PyQSO.  If not, see <http://www.gnu.org/licenses/>.

from gi.repository import Gtk, GObject
import logging
import telnetlib
import unittest
import unittest.mock
import configparser
import os.path

from pyqso.telnet_connection_dialog import *

BOOKMARKS_FILE = os.path.expanduser('~/.config/pyqso/bookmarks.ini')


class DXCluster(Gtk.VBox):

    """ A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """

    def __init__(self, parent):
        """ Set up the DX cluster's Gtk.VBox, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds.

        :arg parent: The parent Gtk window.
        """
        logging.debug("Setting up the DX cluster...")
        Gtk.VBox.__init__(self, spacing=2)

        self.connection = None
        self.parent = parent

        # Set up the menubar
        self.menubar = Gtk.MenuBar()

        self.items = {}

        # CONNECTION ######
        mitem_connection = Gtk.MenuItem(label="Connection")
        self.menubar.append(mitem_connection)
        subm_connection = Gtk.Menu()
        mitem_connection.set_submenu(subm_connection)

        # Connect
        mitem_connect = Gtk.ImageMenuItem(label="Connect to Telnet Server")
        icon = Gtk.Image()
        icon.set_from_stock(Gtk.STOCK_CONNECT, Gtk.IconSize.MENU)
        mitem_connect.set_image(icon)
        subm_connection.append(mitem_connect)
        self.items["CONNECT"] = mitem_connect

        subm_connect = Gtk.Menu()

        # New
        mitem_new = Gtk.MenuItem(label="New...")
        mitem_new.connect("activate", self.new_server)
        subm_connect.append(mitem_new)

        # From Bookmark
        mitem_bookmark = Gtk.MenuItem(label="From Bookmark")
        self.subm_bookmarks = Gtk.Menu()
        mitem_bookmark.set_submenu(self.subm_bookmarks)
        self._populate_bookmarks()
        subm_connect.append(mitem_bookmark)

        mitem_connect.set_submenu(subm_connect)

        # Disconnect
        mitem_disconnect = Gtk.ImageMenuItem(label="Disconnect from Telnet Server")
        icon = Gtk.Image()
        icon.set_from_stock(Gtk.STOCK_DISCONNECT, Gtk.IconSize.MENU)
        mitem_disconnect.set_image(icon)
        mitem_disconnect.connect("activate", self.telnet_disconnect)
        subm_connection.append(mitem_disconnect)
        self.items["DISCONNECT"] = mitem_disconnect

        self.pack_start(self.menubar, False, False, 0)

        # A TextView object to display the output from the Telnet server.
        self.renderer = Gtk.TextView()
        self.renderer.set_editable(False)
        self.renderer.set_cursor_visible(False)
        sw = Gtk.ScrolledWindow()
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        sw.add(self.renderer)
        self.buffer = self.renderer.get_buffer()
        self.pack_start(sw, True, True, 0)

        # Set up the command box.
        self.commandbox = Gtk.HBox(spacing=2)
        self.command = Gtk.Entry()
        self.commandbox.pack_start(self.command, True, True, 0)
        self.send = Gtk.Button(label="Send Command")
        self.send.connect("clicked", self.telnet_send_command)
        self.commandbox.pack_start(self.send, False, False, 0)
        self.pack_start(self.commandbox, False, False, 0)

        self.set_items_sensitive(True)

        self.show_all()

        logging.debug("DX cluster ready!")

        return

    def new_server(self, widget=None):
        """ Get Telnet server host and login details specified in the Gtk.Entry boxes in the TelnetConnectionDialog and attempt a connection. """
        dialog = TelnetConnectionDialog(self.parent)
        response = dialog.run()

        if(response == Gtk.ResponseType.OK):
            connection_info = dialog.get_connection_info()
            host = connection_info["HOST"].get_text()
            port = connection_info["PORT"].get_text()
            username = connection_info["USERNAME"].get_text()
            password = connection_info["PASSWORD"].get_text()

            # Save the server details in a new bookmark, if desired.
            if(connection_info["BOOKMARK"].get_active()):
                try:
                    config = configparser.ConfigParser()
                    config.read(BOOKMARKS_FILE)

                    # Use the host name as the bookmark's identifier.
                    try:
                        config.add_section(host)
                    except configparser.DuplicateSectionError:
                        # If the hostname already exists, assume the user wants to update the port number, username and/or password.
                        logging.warning("A server with hostname '%s' already exists. Over-writing existing details..." % (host))
                    config.set(host, "host", host)
                    config.set(host, "port", port)
                    config.set(host, "username", username)
                    config.set(host, "password", password)

                    # Write the bookmarks to file.
                    if not os.path.exists(os.path.expanduser('~/.config/pyqso')):
                        os.makedirs(os.path.expanduser('~/.config/pyqso'))
                    with open(BOOKMARKS_FILE, 'w') as f:
                        config.write(f)

                    self._populate_bookmarks()

                except IOError:
                    # Maybe the bookmarks file could not be written to?
                    logging.error("Bookmark could not be saved. Check bookmarks file permissions? Going ahead with the server connection anyway...")

            dialog.destroy()

            try:
                # Convert port (currently of type str) into an int.
                port = int(port)
                # Attempt a connection with the server.
                self.telnet_connect(host, port, username, password)
            except ValueError as e:
                logging.error("Could not convert the server's port information to an integer.")
                logging.exception(e)

        else:
            dialog.destroy()
        return

    def _populate_bookmarks(self):
        """ Populate the list of bookmarked Telnet servers in the menu. """
        config = configparser.ConfigParser()
        have_config = (config.read(BOOKMARKS_FILE) != [])

        if(have_config):
            try:
                # Clear the menu of all current bookmarks.
                for i in self.subm_bookmarks.get_children():
                    self.subm_bookmarks.remove(i)

                # Add all bookmarks in the config file.
                for bookmark in config.sections():
                    mitem = Gtk.MenuItem(label=bookmark)
                    mitem.connect("activate", self.bookmarked_server, bookmark)
                    self.subm_bookmarks.append(mitem)

            except Exception as e:
                logging.error("An error occurred whilst populating the DX cluster bookmarks menu.")
                logging.exception(e)

            self.show_all()  # Need to do this to update the bookmarks list in the menu.

        return

    def bookmarked_server(self, widget, name):
        """ Get Telnet server host and login details from an existing bookmark and attempt a connection.

        :arg str name: The name of the bookmark. This is the same as the server's hostname.
        """

        config = configparser.ConfigParser()
        have_config = (config.read(BOOKMARKS_FILE) != [])
        try:
            if(not have_config):
                raise IOError("The bookmark's details could not be loaded.")

            host = config.get(name, "host")
            port = int(config.get(name, "port"))
            username = config.get(name, "username")
            password = config.get(name, "password")
            self.telnet_connect(host, port, username, password)

        except ValueError as e:
            # This exception may occur when casting the port (which is a str) to an int.
            logging.exception(e)
        except IOError as e:
            logging.exception(e)
        except Exception as e:
            logging.error("Could not connect to Telnet server '%s'" % name)
            logging.exception(e)

        return

    def telnet_connect(self, host, port=23, username=None, password=None):
        """ Connect to a user-specified Telnet server.

        :arg str host: The Telnet server's hostname.
        :arg int port: The Telnet server's port number. If no port is specified, the default Telnet server port of 23 will be used.
        :arg str username: The user's username. This is an optional argument.
        :arg str password: The user's password. This is an optional argument.
        """

        if(host == "" or host is None):
            logging.error("No Telnet server specified.")
            return
        if(port == "" or port is None):
            port = 23  # Use the default Telnet port

        try:
            self.connection = telnetlib.Telnet(host, port)

            if(username):
                self.connection.read_until("login: ".encode())
                self.connection.write((username + "\n").encode())
            if(password):
                self.connection.read_until("password: ".encode())
                self.connection.write((password + "\n").encode())
        except Exception as e:
            logging.error("Could not create a connection to the Telnet server")
            logging.exception(e)
            self.connection = None
            return

        self.set_items_sensitive(False)

        self.check_io_event = GObject.timeout_add(1000, self._on_telnet_io)

        return

    def telnet_disconnect(self, widget=None):
        """ Disconnect from a Telnet server and remove the I/O timer. """
        if(self.connection):
            self.connection.close()
        self.buffer.set_text("")
        self.connection = None
        self.set_items_sensitive(True)

        # Stop checking for server output once disconnected.
        try:
            GObject.source_remove(self.check_io_event)
        except AttributeError:
            # This may happen if a connection hasn't yet been established.
            pass

        return

    def telnet_send_command(self, widget=None):
        """ Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
        if(self.connection):
            self.connection.write((self.command.get_text() + "\n").encode())
            self.command.set_text("")
        return

    def _on_telnet_io(self):
        """ Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget.

        :returns: Always returns True to satisfy the GObject timer.
        :rtype: bool
        """
        if(self.connection):
            text = self.connection.read_very_eager().decode()
            try:
                text = text.replace("\u0007", "")  # Remove the BEL Unicode character from the end of the line
            except UnicodeDecodeError:
                pass

            # Allow auto-scrolling to the new text entry if the focus is already at
            # the very end of the Gtk.TextView. Otherwise, don't auto-scroll
            # in case the user is reading something further up.
            # Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
            end_iter = self.buffer.get_end_iter()
            end_mark = self.buffer.create_mark(None, end_iter)
            self.renderer.move_mark_onscreen(end_mark)
            at_end = self.buffer.get_iter_at_mark(end_mark).equal(end_iter)
            self.buffer.insert(end_iter, text)
            if(at_end):
                end_mark = self.buffer.create_mark(None, end_iter)
                self.renderer.scroll_mark_onscreen(end_mark)

        return True

    def set_items_sensitive(self, sensitive):
        """ Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected.

        :arg bool sensitive: If True, enable the Connect button and disable the Disconnect button. If False, vice versa.
        """
        self.items["CONNECT"].set_sensitive(sensitive)
        self.items["DISCONNECT"].set_sensitive(not sensitive)
        self.send.set_sensitive(not sensitive)
        return


class TestDXCluster(unittest.TestCase):

    """ The unit tests for the DXCluster class. """

    def setUp(self):
        """ Set up the objects needed for the unit tests. """
        self.dxcluster = DXCluster(parent=None)

    def tearDown(self):
        """ Destroy any unit test resources. """
        pass

    def test_on_telnet_io(self):
        """ Check that the response from the Telnet server can be correctly decoded. """

        telnetlib.Telnet = unittest.mock.Mock(spec=telnetlib.Telnet)
        connection = telnetlib.Telnet("hello", "world")
        connection.read_very_eager.return_value = b"Test message from the Telnet server."
        self.dxcluster.connection = connection
        result = self.dxcluster._on_telnet_io()
        assert(result)

if(__name__ == '__main__'):
    unittest.main()
