from base64 import b32decode
from collections import namedtuple

import keyring
import json

import gi
from keyring.errors import KeyringLocked

from numberstation.otpurl import OTPUrl

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, GObject, Gio, Gdk, GLib

gi.require_version('Handy', '1')
from gi.repository import Handy


class NumberstationWindow:
    def __init__(self, application, args, version):
        self.application = application
        self.args = args
        self.version = version

        Handy.init()

        builder = Gtk.Builder()
        builder.add_from_resource('/org/postmarketos/Numberstation/ui/numberstation.glade')
        builder.connect_signals(self)
        css = Gio.resources_lookup_data("/org/postmarketos/Numberstation/ui/style.css", 0)
        self.provider = Gtk.CssProvider()
        self.provider.load_from_data(css.get_data())

        self.provider = Gtk.CssProvider()
        self.provider.load_from_data(css.get_data())

        self.window = builder.get_object("main_window")
        self.window.set_application(self.application)
        self.error = builder.get_object("error")
        self.mainstack = builder.get_object("mainstack")

        self.add_name = builder.get_object("add_name")
        self.add_secret = builder.get_object("add_secret")
        self.add_totp = builder.get_object("add_totp")
        self.add_counter = builder.get_object("add_counter")
        self.add_length_adj = builder.get_object("add_length_adj")
        self.add_timer_adj = builder.get_object("add_timer_adj")
        self.add_save = builder.get_object("add_save")

        self.hamburgermenu = builder.get_object("hamburgermenu")

        self.apply_css(self.window, self.provider)

        self.codes_box = builder.get_object("codes_box")

        self.tokens = []
        self.gestures = []

        self.init_actions()

        try:
            all_keys = keyring.get_password('numberstation', 'totp')
            if all_keys is not None:
                for url in json.loads(all_keys):
                    try:
                        temp = OTPUrl(url)
                        temp.get_token()
                        self.tokens.append(temp)
                    except:
                        print("Invalid key: {}, skipping".format(url))

        except KeyringLocked:
            self.show_error("The keyring could not be opened")
            print("Could not unlock the keyring")

        self.timers = []
        if args.url:
            self.import_url(args.url)

        self.build_code_list()
        GLib.timeout_add(1000, self.update_codes)

        self.window.show()

        Gtk.main()

    def apply_css(self, widget, provider):
        Gtk.StyleContext.add_provider(widget.get_style_context(),
                                      provider,
                                      Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

        if isinstance(widget, Gtk.Container):
            widget.forall(self.apply_css, provider)

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

        action = Gio.SimpleAction.new("import", None)
        action.connect("activate", self.on_import)
        self.application.add_action(action)

        action = Gio.SimpleAction.new("export", None)
        action.connect("activate", self.on_export)
        self.application.add_action(action)

    def on_main_window_destroy(self, widget):
        Gtk.main_quit()

    def show_error(self, message):
        self.error.set_text(message)
        self.error.show()

    def import_url(self, url):
        try:
            url = OTPUrl(url)
        except ValueError as e:
            self.show_error(str(e))
            print(e)
            return

        existing = False
        for token in self.tokens:
            if token.secret == url.secret:
                self.show_error("The code you tried to add already exists in the database")
                print("The code you tried to add already exists in the database, skipping...")
                existing = True

        if not existing:
            self.tokens.append(url)
            self.save_keyring()

    def update_code_label(self, label, progressbar, token):
        code, validity = token.get_token()
        if code is None:
            return
        user_code = code
        paste_code = code
        if len(code) % 4 == 0:
            user_code = ' '.join([code[i:i + 4] for i in range(0, len(code), 4)])
        elif len(code) % 3 == 0:
            user_code = ' '.join([code[i:i + 3] for i in range(0, len(code), 3)])
        elif len(code) % 2 == 0:
            user_code = ' '.join([code[i:i + 2] for i in range(0, len(code), 2)])
        if token.type == 'totp':
            label.set_text(user_code)
        else:
            label.set_label(user_code)
        label.paste_code = paste_code
        if validity:
            progressbar.validity = int(validity)
        else:
            if progressbar:
                progressbar.validity = None

    def update_codes(self):
        GLib.timeout_add(1000, self.update_codes)
        for timer in self.timers:
            if timer.validity is None:
                continue
            timer.validity -= 1
            if timer.validity < 1:
                timer.validity = int(timer.period)
                self.update_code_label(timer.display, timer, timer.token)

            timer.set_fraction(1.0 / timer.period * timer.validity)

    def on_long_press(self, gesture, x, y, *args):
        eb = gesture.eb
        Buttonevent = namedtuple('ButtonEvent', 'button')

        self.on_context_menu(eb, Buttonevent(3))

    def on_context_menu(self, eventbox, event):
        # Only right click
        if event.button != 3:
            return

        popup = Gtk.Popover()
        popup.set_relative_to(eventbox)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.set_spacing(6)
        box.set_margin_top(12)
        box.set_margin_bottom(12)
        box.set_margin_start(12)
        box.set_margin_end(12)
        label = Gtk.Entry()
        label.set_text(eventbox.token.label)
        label.index = eventbox.index
        label.connect("activate", self.on_update_label)
        box.pack_start(label, True, True, 0)

        icon_del = Gtk.Image(icon_name="user-trash-symbolic")
        btn_del = Gtk.Button()
        btn_del.set_image(icon_del)
        btn_del.get_style_context().add_class("destructive-action")
        btn_del.index = eventbox.index
        btn_del.connect('clicked', self.on_delete)
        box.pack_start(btn_del, False, False, 0)

        popup.add(box)
        popup.show_all()
        popup.popup()

    def on_delete(self, widget, *args):
        index = widget.index
        del self.tokens[index]

        self.save_keyring()
        self.build_code_list()

    def on_update_label(self, widget, *args):
        new_label = widget.get_text()
        self.tokens[widget.index].label = new_label
        self.save_keyring()
        self.build_code_list()

    def build_code_list(self):
        for child in self.codes_box:
            child.destroy()

        for index, token in enumerate(self.tokens):
            eb = Gtk.EventBox()
            grid = Gtk.Grid()

            grid.set_hexpand(True)
            display_label = token.label
            if ':' in token.label:
                part = token.label.split(':', maxsplit=1)
                display_label = f"{part[0]} ({part[1]})"
            label = Gtk.Label(display_label)
            label.get_style_context().add_class('token-label')
            grid.attach(label, 0, 0, 1, 1)

            if token.type == 'totp':
                code = Gtk.Label("000000")
            elif token.type == 'hotp':
                code = Gtk.Button("000000")
                code.set_hexpand(True)
                code.connect('clicked', self.on_hotp_click)

            code.get_style_context().add_class('token-code')
            code.token = token
            grid.attach(code, 0, 1, 1, 1)
            timer = Gtk.ProgressBar()
            timer.set_hexpand(True)
            timer.period = token.period

            self.update_code_label(code, timer, token)

            timer.token = token
            timer.display = code
            if not hasattr(timer, 'validity'):
                continue
            if timer.validity is not None:
                timer.show()
                timer.set_no_show_all(False)
                timer.set_fraction(1.0 / token.period * timer.validity)
            else:
                timer.hide()
                timer.set_no_show_all(True)
            self.timers.append(timer)
            grid.attach(timer, 0, 2, 1, 1)
            eb.add(grid)
            eb.connect('button-release-event', self.on_context_menu)
            eb.token = token
            eb.index = index
            self.num_tokens = index
            longpress = Gtk.GestureLongPress.new(eb)
            longpress.eb = eb
            longpress.connect('pressed', self.on_long_press)
            self.gestures.append(longpress)
            self.codes_box.pack_start(eb, True, True, False)

        self.apply_css(self.codes_box, self.provider)
        self.codes_box.show_all()

    def save_keyring(self):
        print("Saving keyring")
        key_urls = []
        for token in self.tokens:
            key_urls.append(token.get_url())
        password = json.dumps(key_urls)
        keyring.set_password('numberstation', 'totp', password)

    def on_add_entry_clicked(self, widget, *args):
        self.mainstack.set_visible_child_name('add')

    def on_back_clicked(self, widget, *args):
        self.mainstack.set_visible_child_name('codes')

    def on_save_clicked(self, widget, *args):
        name = self.add_name.get_text().strip()
        type = 'totp' if self.add_totp.get_active() else 'hotp'
        secret = self.add_secret.get_text().strip()
        duration = self.add_timer_adj.get_value()
        length = self.add_length_adj.get_value()
        counter = self.add_counter.get_text().strip()
        if counter == '':
            counter = None
        else:
            counter = int(counter)

        url = OTPUrl.create(name=name, type=type, secret=secret, duration=duration, length=length, counter=counter)
        existing = False
        for token in self.tokens:
            if token.secret == url.secret:
                self.show_error("The code you tried to add already exists in the database")
                print("The code you tried to add already exists in the database, skipping...")
                existing = True

        if not existing:
            self.tokens.append(url)
            self.save_keyring()

        self.build_code_list()
        self.mainstack.set_visible_child_name('codes')

    def on_add_secret_changed(self, widget, *args):
        value = self.add_secret.get_text()
        try:
            b32decode(value, casefold=True)
            self.add_secret.get_style_context().remove_class('error')
            self.add_save.set_sensitive(True)
        except:
            self.add_secret.get_style_context().add_class('error')
            self.add_save.set_sensitive(False)

    def on_hamburger_clicked(self, widget, *args):
        self.hamburgermenu.popup()

    def on_about(self, *args):
        dialog = Gtk.AboutDialog(transient_for=self.window)
        dialog.set_logo_icon_name('org.postmarketos.Numberstation')
        dialog.set_program_name('Numberstation')
        dialog.set_version(self.version)
        dialog.set_website('https://git.sr.ht/~martijnbraam/numberstation')
        dialog.set_authors(['Martijn Braam'])
        gtk_version = '{}.{}.{}'.format(Gtk.get_major_version(),
                                        Gtk.get_minor_version(), Gtk.get_micro_version())
        comment = "Mobile friendly TOTP application\n\n"
        comment += 'Gtk: {}'.format(gtk_version)
        dialog.set_comments(comment)
        text = "Distributed under the GNU GPL(v3) license.\n"
        dialog.set_license(text)
        dialog.run()
        dialog.destroy()

    def on_import(self, *args):
        dialog = Gtk.FileChooserDialog(title="Select a file to import", parent=self.window,
                                       action=Gtk.FileChooserAction.OPEN)
        dialog.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN,
            Gtk.ResponseType.OK
        )

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
            with open(filename, 'r') as handle:
                for line in handle.readlines():
                    if '://' in line:
                        self.import_url(line.strip())
                self.build_code_list()
        dialog.destroy()

    def on_export(self, *args):
        dialog = Gtk.FileChooserDialog(title="Export secrets to a file", parent=self.window,
                                       action=Gtk.FileChooserAction.SAVE)
        dialog.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_SAVE,
            Gtk.ResponseType.OK
        )
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
            raw = []
            for token in self.tokens:
                raw.append(token.get_url() + "\n")
            with open(filename, 'w') as handle:
                handle.writelines(raw)
        dialog.destroy()

    def on_hotp_click(self, widget, *args):
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(widget.get_label().replace(' ', ''), -1)
        widget.token.initial_count += 1
        self.save_keyring()
        self.update_code_label(widget, None, widget.token)
