# Copyright (c) 2017-2018 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
# 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 3 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.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from gi.repository import Gtk, GLib, Gdk, Secret

import json
from gettext import gettext as _
from random import choice

from passbook.logger import Logger
from passbook.widgets_secrets_listbox_row import SecretsListBoxRow
from passbook.helper_secrets import SecretsHelper
from passbook.define import Type, App, COLORS, LoadingType
from passbook.widgets_collection_row import CollectionRow


class WindowContent:
    """
        Main window content
    """
    def __init__(self):
        """
            Init indirect callbacks
        """
        self.__default_collection = None
        self.__loading_type = LoadingType.SECRETS
        self.__applications = {}
        self.__application_items = {}
        self.__colors = json.loads(
                App().settings.get_value("colors").get_string())
        self._secrets_helper = SecretsHelper()
        self.__collections = []
        self.set_initial_placeholder()
        self.connect("realize", self.__on_realize)
        self.applications_listbox.set_sort_func(self.__apps_sort)

    def add_collection(self, collection):
        """
            Add a new collection
            @param collection as Secret.Collection
        """
        self.collections_separator.show()
        if collection not in self.__collections:
            self.__collections.append(collection)
        self.add_password_button.set_sensitive(True)
        app_name = collection.get_label()
        row = CollectionRow(app_name,
                            None,
                            collection,
                            Type.KEYRING)
        color = self.get_color(row.uid)
        row.set_rgba_color(color)
        self.collections_listbox.add(row)

    def add_keepass_collection(self, uri):
        """
            Add a keepass collection
            @param uri as str
        """
        try:
            from passbook.keepass_collection import KeepassCollection
            collection = KeepassCollection(uri)
            self.keepass_separator.show()
            name = collection.get_label()
            row = CollectionRow(name, None, collection, Type.KEEPASS)
            if collection.get_object_path() is None:
                row.set_sensitive(False)
            elif collection not in self.__collections:
                self.__collections.append(collection)
            color = self.get_color(row.uid)
            row.set_rgba_color(color)
            self.keepass_listbox.add(row)
        except Exception as e:
            Logger.error("WindowContent::add_keepass_collection: %s" % e)

    def add_password(self, collection, item):
        """
            Add a new password
            @param item as Secret.Item
        """
        row = self.selected_row
        if row is not None:
            if row.collection.get_object_path() ==\
                    collection.get_object_path():
                secrets_row = SecretsListBoxRow(item, collection)
                secrets_row.show()
                self.secrets_listbox.add(secrets_row)
                self.__setup_collection_header_bar(row.collection_type)

    def delete_password(self, item):
        """
            Delete password from cache
            @param item as Secret.Item
        """
        row = self.selected_row
        if row is not None:
            self.__setup_collection_header_bar(row.collection_type)
        for key in self.__application_items.keys():
            for _item in self.__application_items[key]:
                if _item == item:
                    self.__application_items[key].remove(item)
                    break

    def get_color(self, value="default"):
        """
            Get color for app
            param value as str
        """
        if value not in self.__colors.keys():
            color = choice(COLORS)
            self.__add_color_to(color, value)
            return color
        else:
            return self.__colors[value]

    def load_css_color(self, color):
        """
            Set color on listbox
            @param color as str
        """
        if not App().settings.get_value("enable-colors"):
            return
        try:
            default = ".mycolor {background-color: %s;}" % color
            rgba = Gdk.RGBA()
            rgba.parse(color)
            self.color_button.set_rgba(rgba)
            App().css_provider.load_from_data(default.encode("utf-8"))
        except Exception as e:
            Logger.error("WindowContent::load_css_color(): %s" % e)

    def update_color(self):
        """
            Update colors for current row
        """
        if App().settings.get_value("enable-colors"):
            self.color_button.show()
        else:
            self.color_button.hide()
        row = self.selected_row
        if row is not None:
            color = self.get_color(row.uid)
            self.load_css_color(color)

    def load_all(self):
        """
            Load all rows
        """
        self.__loading_type = LoadingType.SEARCH
        App().css_provider.load_from_data(b"")
        self.__clear_listbox(self.secrets_listbox)
        for listbox in [self.collections_listbox,
                        self.keepass_listbox,
                        self.applications_listbox]:
            listbox.unselect_all()
        for row in self.applications_listbox.get_children():
            self.__load_secrets_row(row)

    def set_initial_placeholder(self):
        """
            Set initial placeholder with a welcome message
        """
        self.placeholder_label.set_markup(
            _("<big>Welcome to <b>Passbook</b>, a password manager helping to "
              "keep your information secure on your computer.</big>"))
        self.placeholder_image.set_from_icon_name(
            "org.gnome.Passbook-symbolic", Gtk.IconSize.DIALOG)

    @property
    def default_collection(self):
        """
            Get default collection
            @return Secret.Collection
        """
        return self.__default_collection

    @property
    def collections(self):
        """
            Get collections
            @return [Secret.Collection]
        """
        return self.__collections

    @property
    def selected_row(self):
        """
            Get selected row
            @return Gtk.ListBoxRow
        """
        for listbox in [self.collections_listbox,
                        self.keepass_listbox,
                        self.applications_listbox]:
            row = listbox.get_selected_row()
            if row is not None:
                return row
        return None

#############
# CALLBACKS #
#############
    def on_row_activated(self, listbox, row):
        """
            Load passwords in view
            @param listbox as Gtk.ListBox
            @param row as CollectionRow
        """
        self.__loading_type = LoadingType.SECRETS
        self.filter_bar.set_search_mode(False)
        self.__clear_listbox(self.secrets_listbox)
        for l in [self.collections_listbox,
                  self.keepass_listbox,
                  self.applications_listbox]:
            if l != listbox:
                l.unselect_all()
        if row.collection_type == Type.KEEPASS:
            # We want unselect_all() to run
            GLib.idle_add(self.__load_keepass_row, row)
        else:
            self.__load_secrets_row(row)

    def on_color_set(self, button):
        """
            Set color on current view
            @param button as Gtk.ColorButton
        """
        color = button.get_rgba().to_string()
        row = self.selected_row
        if row is None:
            value = "default"
        else:
            value = row.uid
            row.set_rgba_color(color)
        self.__add_color_to(color, value)

############
# PRIVATE  #
############
    def __add_keepass_collections(self):
        """
            Add keepass collections
        """
        def add_keepass_collections(uris):
            if uris:
                uri = uris.pop(0)
                self.add_keepass_collection(uri)
                GLib.idle_add(add_keepass_collections, uris)
            else:
                self.paned_stack.set_visible_child_name("paned_child")
                self.paned_spinner.stop()
        uris = list(App().settings.get_value("keepass-uris"))
        add_keepass_collections(uris)

    def __set_empty_placeholder(self):
        """
            Set placeholder for empty collections
        """
        self.placeholder_label.set_markup(
            _("<big>No passwords !</big>"))
        self.placeholder_image.set_from_icon_name(
            "dialog-password-symbolic", Gtk.IconSize.DIALOG)

    def __load_keepass_row(self, row):
        """
            Load keepass row
            @param row as Gtk.ListBoxRow
            @param password as str
        """
        if row.collection.kp is None:
            from passbook.dialog_unlock import UnlockDialog
            dialog = UnlockDialog(row.collection.uri, self)
            kp = dialog.run()
            row.collection.set_kp(kp)
        if row.collection.kp is not None:
            self.__load_collection(row)

    def __load_secrets_row(self, row):
        """
            Load secrets row
            @param row as Gtk.ListBoxRow
        """

        def on_unlock(collection):
            """
                Replace locked collection by unlocked one
                @param collection as Secret.Collection
            """
            # Works but don't know why :-/
            # How to properly reload a collection after an unlock?
            # Help wanted
            self._secrets_helper.get_collection(
                                    "",
                                    lambda x: self.__load_collection(row))
        self.collection_header_bar.hide()
        if row.collection.get_locked():
            self._secrets_helper.unlock(row.collection, on_unlock)
        else:
            self.__load_collection(row)

    def __load_collection(self, row):
        """
            Load collection for row
            @param row as CollectionRow
        """
        def add_rows(items, collection_type):
            if items:
                item = items.pop(0)
                secrets_row = SecretsListBoxRow(item, row.collection)
                secrets_row.show()
                self.secrets_listbox.add(secrets_row)
                if self.__loading_type == LoadingType.SECRETS:
                    self.__setup_collection_header_bar(collection_type)
                GLib.idle_add(add_rows, items, collection_type)

        if self._adaptive_stack:
            self.emit("can-go-back-changed", True)
        self.header_bar.set_subtitle(row.name)
        if self.__loading_type == LoadingType.SECRETS:
            self.search_button.set_active(False)
        if row.collection_type in [Type.KEYRING, Type.KEEPASS]:
            items = row.collection.get_items()
        else:
            items = list(self.__application_items[row.name])
        if items:
            add_rows(items, row.collection_type)
            self.stack.set_visible_child_name("secrets")
            if self.__loading_type == LoadingType.SECRETS:
                self.update_color()
        elif self.__loading_type == LoadingType.SECRETS:
            self.__setup_collection_header_bar(row.collection_type)
            self.__set_empty_placeholder()
            self.color_button.hide()
            self.stack.set_visible_child_name("placeholder")

    def __add_color_to(self, color, value):
        """
            Add color to current view
            @param color as str
            @param value as str
        """
        self.__colors[value] = color
        variant = GLib.Variant("s", json.dumps(self.__colors))
        App().settings.set_value("colors", variant)
        self.load_css_color(color)

    def __apps_sort(self, row1, row2):
        """
            Sort apps
            @param row1 as Gtk.ListBoxRow
            @param row2 as Gtk.ListBoxRow
        """
        return row1.name > row2.name

    def __add_application(self, collection, item):
        """
            Add a new application if needed
            @param item as Secret.Item
        """
        # First get application name from schema
        (app_name, icon_name) = CollectionRow.app_for_schema(
                item.get_schema_name())
        if app_name is None:
            # Then from label
            (app_name, icon_name) = CollectionRow.app_for_label(
                item.get_label())
            if app_name is None:
                app_name = _("Others")
                icon_name = "dialog-password"
        if app_name not in self.__application_items.keys():
            self.__application_items[app_name] = []
        self.__application_items[app_name].append(item)
        self.__applications[app_name] = (collection, icon_name)

    def __add_application_rows(self, apps):
        """
            Add application rows
            @param apps as [str]
        """
        if apps:
            app_name = apps.pop(0)
            (collection, icon_name) = self.__applications[app_name]
            row = CollectionRow(app_name, icon_name, collection, Type.APPS)
            color = self.get_color(row.uid)
            row.set_rgba_color(color)
            self.applications_listbox.add(row)
            GLib.idle_add(self.__add_application_rows, apps)
        else:
            self._secrets_helper.get_collections(self.__on_get_collections)

    def __add_collections(self,):
        """
            Add collections
        """
        for collection in self.__collections:
            self.add_collection(collection)

    def __clear_listbox(self, listbox):
        """
            Remove children from listbox
            @param listbox as Gtk.ListBox
        """
        for child in listbox.get_children():
            child.destroy()

    def __setup_collection_header_bar(self, collection_type):
        """
            Set headerbar content
            @param count as int
        """
        if self.filter_bar.get_search_mode():
            return
        count = len(self.secrets_listbox.get_children())
        self.collection_header_bar.show()
        if collection_type == Type.APPS:
            self.collection_delete_button.hide()
            self.groups_button.hide()
        elif collection_type == Type.KEEPASS:
            self.groups_button.show()
            self.collection_delete_button.show()
        else:
            self.groups_button.hide()
            self.collection_delete_button.show()
        if count > 1:
            self.collection_count_label.set_text(_("%s passwords") % count)
        else:
            self.collection_count_label.set_text(_("%s password") % count)

    def __on_realize(self, widget):
        """
            Init window content
            @param widget as Gtk.Widget
        """
        self._secrets_helper.get_collection(Secret.COLLECTION_DEFAULT,
                                            self.__on_get_default_collection)

    def __on_get_default_collection(self, collection):
        """
            Load collection items to extract applications
            @param collection as Secret.Collection
        """
        def add_applications():
            """
                Cache applications and add rows
            """
            self.applications_separator.show()
            for item in self.__default_collection.get_items():
                self.__add_application(collection, item)
            apps = list(self.__application_items.keys())
            self.__add_application_rows(apps)

        def on_unlock(collection):
            """
                Replace locked collection by unlocked one
                @param collection as Secret.Collection
            """
            self._secrets_helper.get_collection(
                                              Secret.COLLECTION_DEFAULT,
                                              self.__on_get_default_collection)
        self.__default_collection = collection
        if collection is None:
            self._secrets_helper.get_collections(self.__on_get_collections)
        elif collection.get_locked():
            self._secrets_helper.unlock(collection, on_unlock)
        else:
            add_applications()

    def __on_get_collections(self, collections):
        """
            Set collections and get passwords
            @param collections as [Secret.Collection]
        """
        for collection in collections:
            if collection.get_label() and (
                    self.__default_collection is None or
                    self.__default_collection.get_object_path() !=
                    collection.get_object_path()):
                self.__collections.append(collection)
        if self.__collections:
            self.add_password_button.set_sensitive(True)
            self.__add_collections()
        if App().keepass:
            self.__add_keepass_collections()
        else:
            self.paned_stack.set_visible_child_name("paned_child")
            self.paned_spinner.stop()
