#!/usr/bin/env python3


# Simple Keyboard Layout switcher for FreeBSD in PyQt5


# Copyright (c) 2020, Simon Peter <probono@puredarwin.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


import os, sys, subprocess, io
import configparser

try:
    from PyQt5 import QtWidgets, QtGui, QtCore
except:
    print("Could not import PyQt5. On FreeBSD, sudo pkg install py37-qt5-widgets")

config_file = os.path.expanduser('~/.config/hello/keyboard.conf')

class Xkbmap(object):

    def __init__(self):
        self.rules = []
        self.model = ""
        self.layout = ""
        self.variant = ""
        self.options = []
        self.update()

    def update(self):
        """
        Parse currently set xkbmap and fill some variables based on this information..
        """

        proc = subprocess.Popen(["setxkbmap", "-query"], stdout=subprocess.PIPE)
        for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
            subjects = ["rules", "model", "layout", "variant", "options"]
            for subject in subjects:
                if line.startswith(subject + ":"):
                    line = line.replace(subject + ":", "").replace("\n", "").strip()
                    if "," in line:
                       contents = line.split(",")
                       for i in range(0, len(contents)):
                           contents[i] = contents[i].strip()
                    else:
                        contents = line

                    # print("subject:", subject)
                    # print("contents:", contents)
                    if subject == "rules":
                        self.rules = contents
                        print("xkbmap rules:", self.rules)
                    elif subject == "model":
                        self.model = contents
                        print("xkbmap model:", self.model)
                    elif subject == "layout":
                        self.layout = contents
                        print("xkbmap layout:", self.layout)
                    elif subject == "variant":
                        self.variant = contents
                        print("xkbmap variant:", self.variant)
                    elif subject == "options":
                        if contents.__class__ == str:
                            self.options = [contents]
                        else:
                            self.options = contents
                        print("xkbmap options:", self.options)

    def setKeyToValue(self, key, value):
        args = ["setxkbmap", '-' + key, value]
        out = subprocess.call(args)
        if out == 0:
            print("Changed xkbmap successfully")
        else:
            print("Error changing xkbmap")
        self.update()

    def clearOptions(self):
        print ("Clearing xkbmap options")
        args = ["setxkbmap", '-option'] # This is needed for resetting Options
        # To replace all previously specified options, set the 'option' key with no value
        out = subprocess.call(args)
        if out == 0:
            print("Cleared xkbmap options successfully")
        else:
            print("Error clearing xkbmap options")
        self.update()

    def removeOption(self, option_to_be_removed):
        print("option_to_be_removed:", option_to_be_removed)
        if self.options == None or option_to_be_removed == None:
            # Nothing to do. Return now in order not to crash later
            return
        new_options = []
        options_to_be_removed = []
        if "," in option_to_be_removed:
            options_to_be_removed = option_to_be_removed.split(",")
        else:
            options_to_be_removed = [option_to_be_removed]
        print("options_to_be_removed:", options_to_be_removed)
        if options_to_be_removed == None:
            return
        for old_option in self.options:
            if old_option not in options_to_be_removed:
                new_options.append(old_option)
        print("new_options:", new_options)
        self.clearOptions() # Clear existing options
        self.setKeyToValue("option", ",".join(new_options)) # Set the new options in which option is removed
        self.options = new_options

    def setOption(self, option_to_be_set):
        if "," in option_to_be_set:
            options_to_be_set = option_to_be_set.split(",")
        else:
            options_to_be_set = [option_to_be_set]
        # print("options_to_be_set:", options_to_be_set)
        for option in options_to_be_set:
            if option not in self.options:
                print("Setting option", option)
                self.setKeyToValue("option", option) # In fact, this just adds the option in setxkbmap by default

    def exportToXkbcomp(self):
        # Currently used for information only. This output can be piped directly to the xkbcomp input
        proc = subprocess.Popen(["setxkbmap", "-print"], stdout=subprocess.PIPE)
        for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
            print(line)

class KeyboardSwitcher(QtWidgets.QMainWindow):

    def __init__(self, app):
        super().__init__()
        self.app = app
        self.config = configparser.RawConfigParser()
        self.config.optionxform = str
        self.config.read(config_file)
        self.supported_layouts = self.getSupportedLayouts()
        self.has_apple_keyboard = self.checkHasAppleKeyboard()
        print("self.has_apple_keyboard: %s" % self.has_apple_keyboard)
        self.xkbmap = Xkbmap() # The xkbmap currently active in the system. No root needed to change this.
        self.basicWindow()
        # self._readConfig()

    def saveConfig(self):
        directory = os.path.dirname(config_file)
        print(directory)
        if not os.path.exists(directory):
            os.makedirs(directory)
        with open(config_file, 'w') as configfile:
            self.config.write(configfile, space_around_delimiters=False)

    def checkHasAppleKeyboard(self):
        """
        Check whether this machine has an Apple keyboard.
        TODO: Do this per keyboard, if there are multiple ones connected.

        Return True or False.
        """
        proc = subprocess.Popen(["sysctl", "kern.evdev.input"], stdout=subprocess.PIPE)
        for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
            if "Apple" in line and "Keyboard" in line:
                return True
        return False

    def getSupportedLayouts(self):
        candidates = os.listdir("/usr/local/share/X11/xkb/symbols/")
        # supported_layouts = []
        # for candidate in candidates:
        #     if len(candidate) < 4:
        #         supported_layouts.append(candidate)
        # supported_layouts.sort()
        # supported_layouts.insert(0, "en")
        # return supported_layouts

        # Only show the main ones in order to keep the list short and tidy, let's see how long
        # it takes for the first person to complain that exotic ones are missing...
        # Let's not end up like https://www.youtube.com/watch?v=4nR4HP2x1iU
        supported_layouts = ["us", "gb", "fr", "es", "de", "it", "jp", "pt", "no", "se", "fi", "ru", "tr", "il"]

        return supported_layouts

    def basicWindow(self):
        self.window = QtWidgets.QMainWindow()
        self.window.setFixedSize(300, 450)
        self._showMenu()

        self.window.setWindowTitle('Keyboard Layout')

        layout = QtWidgets.QVBoxLayout()

        self.list_widget = QtWidgets.QListWidget()
        self.list_widget.setAlternatingRowColors(True)
        print(self.supported_layouts)
        cleartext = ["English (United States)", "English (United Kingdom)", "Français", "Español",
                     "Deutsch", "Italiano", "日本語", "Português", "Norsk",
                     "Svenska", "Suomalainen", "Pусский", "Türkçe", "עִברִית"]
        prev_lang_kbds = ["en_US:0", "en_GB:2", "fr:1", "es:8", "de:3", "it:4", "ja:16384", 
                     "pl:30788", "nb:12", "sv:7", "fi:17", "ru:19456", "tr:-24", "he:-18432"]
        i = 0
        selected_index = -1
        for supported_layout in self.supported_layouts:
            item = QtWidgets.QListWidgetItem(cleartext[i])
            i = i+1
            setattr(item, "layout", supported_layout)
            setattr(item, "prev_lang_kbd", prev_lang_kbds[i-1])
            print("%s = %s = %s" % (supported_layout, cleartext[i-1], prev_lang_kbds[i-1]))
            self.list_widget.addItem(item)
            if self.xkbmap.layout == supported_layout:
                print( "Layout in xkbmap matches this layout")
                item.setSelected(True)
                selected_index = i
                self.updateVariantsMenu(self.xkbmap.layout)
        if selected_index > -1:
            print("selected_index, %i" % selected_index)
            self.list_widget.setFocus()

        self.list_widget.itemSelectionChanged.connect(self.onSomethingChanged)
        self.list_widget.setUniformItemSizes(True)
        self.list_widget.setItemAlignment(QtCore.Qt.AlignVCenter)
        layout.addWidget(self.list_widget)

        self.checkbox_keyswap = QtWidgets.QCheckBox()
        if self.has_apple_keyboard == True:
            # For Apple keyboards
            self.checkbox_keyswap.setText(self.tr("Use the ⌘ key for menu shortcuts") + "\n" + self.tr("(this swaps alt and cmd)"))
            self.checkbox_keyswap.__setattr__("setxkbmap_option", "ctrl:swap_lwin_lctl,ctrl:swap_rwin_rctl")
        else:
            # For PC keyboards
            self.checkbox_keyswap.setText(self.tr("Use the Alt key as ⌘") + "\n" + self.tr("(this swaps Ctrl and Alt)"))
            self.checkbox_keyswap.__setattr__("setxkbmap_option", "ctrl:swap_lalt_lctl")
        if("ctrl:swap_lwin_lctl" in self.xkbmap.options \
                or "ctrl:swap_rwin_rctl" in self.xkbmap.options \
                or "ctrl:swap_lalt_lctl" in self.xkbmap.options):
                self.checkbox_keyswap.setChecked(True)
        self.checkbox_keyswap.clicked.connect(self.onSomethingChanged)
        layout.addWidget(self.checkbox_keyswap)

        self.checkbox_disablecapslock = QtWidgets.QCheckBox()
        self.checkbox_disablecapslock.setText("Disable the Caps Lock key")
        if "caps:none" in self.xkbmap.options:
            self.checkbox_disablecapslock.setChecked(True)
        self.checkbox_disablecapslock.clicked.connect(self.onSomethingChanged)
        layout.addWidget(self.checkbox_disablecapslock)

        widget = QtWidgets.QWidget()
        widget.setLayout(layout)
        self.window.setCentralWidget(widget)
        self.window.show()
        sys.exit(app.exec_())

    def onSomethingChanged(self):
        # Need to ensure that users can't do another action before this one has been completed
        self.checkbox_keyswap.setEnabled(False)
        self.checkbox_disablecapslock.setEnabled(False)
        self.list_widget.setEnabled(False)
        self.app.setOverrideCursor(
            QtGui.QCursor(QtCore.Qt.WaitCursor))

        print("Selected layout changed")
        print(self.list_widget.selectedItems()[0].text())
        selected_layout = getattr(self.list_widget.selectedItems()[0], "layout")
        selected_prev_lang_kbd = getattr(self.list_widget.selectedItems()[0], "prev_lang_kbd")
        print("selected_layout", selected_layout)

        self.updateVariantsMenu(selected_layout)

        self.xkbmap.setKeyToValue("layout", selected_layout)
        try:
            self.xkbmap.setKeyToValue("variant", "nodeadkeys")
        except:
            pass

        if not self.config.has_section("Keyboard"):
            self.config.add_section("Keyboard")
        option = getattr(self.checkbox_keyswap, "setxkbmap_option")
        if(self.checkbox_keyswap.isChecked()):
            self.xkbmap.removeOption("ctrl:swap_lwin_lctl,ctrl:swap_rwin_rctl,ctrl:swap_lalt_lctl")
            self.xkbmap.setOption(option)
            self.config.remove_option("Keyboard", "DoNotSwapCommandKey")
        else:
            self.xkbmap.removeOption("ctrl:swap_lwin_lctl,ctrl:swap_rwin_rctl,ctrl:swap_lalt_lctl")
            self.config.set("Keyboard", "DoNotSwapCommandKey", "true")

        if self.checkbox_disablecapslock.isChecked():
            self.xkbmap.setOption("caps:none")
            self.config.remove_option("Keyboard", "DoNotDisableCapslockKey")
        else:
            self.xkbmap.removeOption("caps:none")
            self.config.set("Keyboard", "DoNotDisableCapslockKey", "true")

        self.saveConfig()
        self.updateVariantsMenu(self.xkbmap.layout)
        self.app.setOverrideCursor(
            QtGui.QCursor(QtCore.Qt.ArrowCursor))
        self.checkbox_keyswap.setEnabled(True)
        self.checkbox_disablecapslock.setEnabled(True)
        self.list_widget.setEnabled(True)
        
        # Save to EFI NVRAM variable 7C436110-AB2A-4BBB-A880-FE41995C9F82-prev-lang:kbd
        selected_prev_lang_kbd = getattr(self.list_widget.selectedItems()[0], "prev_lang_kbd")
        proc = subprocess.Popen( "/usr/local/bin/sudo -A -E efivar -w -n 7C436110-AB2A-4BBB-A880-FE41995C9F82-prev-lang:kbd", stdin=subprocess.PIPE, shell=True )
        bytes = bytearray(selected_prev_lang_kbd.encode())
        bytes.append(00)
        proc.communicate(bytes)
        print(proc.returncode)


    def updateVariantsMenu(self, layout):
        self.variantsMenu.clear()
        file1 = open('/usr/local/share/X11/xkb/symbols/' + layout, 'r')
        lines = file1.readlines()
        file1.close()
        self.variantsGroup = QtWidgets.QActionGroup(self.variantsMenu) # Only one of the actions added to this group can be active
        for line in lines:
            if "xkb_symbols" in line and ('"' in line or "'" in line):
                variant = line.replace("'", '"').split('"')[1].strip()
                # print(variant)
                varAct = QtWidgets.QAction(variant, self.window)
                varAct.triggered.connect(self.applyVariant)
                varAct.setCheckable(True)
                if self.xkbmap.variant == variant:
                    varAct.setChecked(True)
                varAct.__setattr__("variant", variant)
                self.variantsGroup.addAction(varAct)
                self.variantsMenu.addAction(varAct)

    def applyVariant(self):
        print("applyVariant called")
        variant = getattr(self.variantsGroup.checkedAction(), "variant")
        # print(variant)
        try:
            self.xkbmap.setKeyToValue("variant", variant)
        except:
            pass
 
    def _showMenu(self):
        menubar = self.window.menuBar()
        fileMenu = menubar.addMenu('&File')
        
        saveAct = QtWidgets.QAction('&Save', self.window)
        saveAct.setShortcut('Ctrl+S')
        saveAct.setStatusTip('Save settings to persist reboots')
        saveAct.triggered.connect(self.save)
        fileMenu.addAction(saveAct)
        
        fileMenu.addSeparator()

        exitAct = QtWidgets.QAction('&Quit', self.window)
        exitAct.setShortcut('Ctrl+Q')
        exitAct.setStatusTip('Exit application')
        exitAct.triggered.connect(QtWidgets.QApplication.quit)
        fileMenu.addAction(exitAct)
        
        self.variantsMenu = menubar.addMenu('&Variants')
        
        helpMenu = menubar.addMenu('&Help')
        
        aboutAct = QtWidgets.QAction('&About', self.window)
        aboutAct.setStatusTip('About this application')
        aboutAct.triggered.connect(self._showAbout)
        
        helpMenu.addAction(aboutAct)


    def save(self):
        print("save called")
        print("TODO: To be implemented")
        conf_file_path = "/tmp/00-keyboard.conf"
        conf_file_template = """Section "InputClass"
        Identifier "system-keyboard"
        MatchIsKeyboard "on"
        Option "XkbLayout" "@layout@"
        Option "XkbModel" "@model@"
        Option "XkbVariant" ",@variant@"
        Option "XkbOptions" "@options@"
EndSection"""
        conf_file_content = conf_file_template.replace("@layout@", self.xkbmap.layout).replace("@model@", self.xkbmap.model).replace("@variant@", self.xkbmap.variant).replace("@options@", ",".join(self.xkbmap.options))
        print("conf_file_content:")
        print(conf_file_content)
        
        f = open(conf_file_path, "w")
        f.write(conf_file_content)
        f.close()

        # Run a helper tool as root that puts the config file into the system-wide Xorg configuration and/or writes the information to EFI
        # TODO: Implement error handling with an error dialog
        proc = subprocess.Popen(["/usr/local/bin/sudo", "-A", "-E", os.path.dirname(__file__) + "/Resources/save.py"], stdout=subprocess.PIPE, env=os.environ)
        for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
            print(line)

    def _showAbout(self):
        print("showDialog")
        msg = QtWidgets.QMessageBox()
        msg.setWindowTitle("About")
        msg.setIconPixmap(QtGui.QPixmap(os.path.dirname(__file__) + "/Resources/Keyboard.png"))
        candidates = ["COPYRIGHT", "COPYING", "LICENSE"]
        for candidate in candidates:
            if os.path.exists(os.path.dirname(__file__) + "/" + candidate):
                with open(os.path.dirname(__file__) + "/" + candidate, 'r') as file:
                    data = file.read()
                msg.setDetailedText(data)
        msg.setText("<h3>Keyboard</h3>")
        msg.setInformativeText(
            "A simple preferences application to set the keyboard layout using <a href='https://www.freebsd.org/cgi/man.cgi?setxkbmap'>setxkbmap</a><br><br><a href='https://github.com/helloSystem/Utilities'>https://github.com/helloSystem/Utilities</a>")
        msg.exec()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ks = KeyboardSwitcher(app)
