/*
 * Seahorse
 *
 * Copyright (C) 2006 Stefan Walter
 * Copyright (C) 2011 Collabora Ltd.
 *
 * 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.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

namespace Seahorse {
namespace Gkr {

public enum Use {
	OTHER,
	NETWORK,
	WEB,
	PGP,
	SSH,
}

private struct DisplayInfo {
	string? item_type;
	string? label;
	string? details;
	string? description;
    GLib.Icon? icon;

    public void query_installed_apps(string item_type) {
        DesktopAppInfo? app_info = new DesktopAppInfo("%s.desktop".printf(item_type));
        if (app_info == null)
            return;

        this.icon = app_info.get_icon();
    }
}

[CCode (has_target = false)]
private delegate void DisplayCustom(string label,
                                    GLib.HashTable<string, string> item_attrs,
                                    ref DisplayInfo info);

private struct DisplayEntry {
	string item_type;
	string description;
	DisplayCustom? custom_func;
}

public class Item : Secret.Item, Deletable, Viewable {
	public string description {
		owned get {
			ensure_display_info ();
			return this._info.description;
		}
	}

	public Use use {
		get {
			var schema_name = get_attribute("xdg:schema");
			if (schema_name != null && schema_name == NETWORK_PASSWORD)
				return Use.NETWORK;
			return Use.OTHER;
		}
	}

	public bool has_secret {
		get { return _item_secret != null; }
	}

	public Keyring place {
		owned get { return (Keyring)_place.get(); }
		set { _place.set(value); }
	}

	public Flags object_flags {
		get { return Flags.DELETABLE | Flags.PERSONAL; }
	}

    public GLib.Icon icon {
        owned get {
            ensure_display_info();
            return this._info.icon ?? new GLib.ThemedIcon (ICON_PASSWORD);
        }
    }

	public new string label {
		owned get {
			ensure_display_info ();
			return this._info.label;
		}
	}

    public string markup {
        owned get {
            ensure_display_info ();
            var result = new StringBuilder("");
            result.append(Markup.escape_text(this._info.label));
            if (this._info.details != null && this._info.details != "") {
                result.append("<span size='small' rise='0' foreground='#555555'>\n");
                result.append(this._info.details);
                result.append("</span>");
            }
            return result.str;
        }
    }

	public Usage usage {
		get { return Usage.CREDENTIALS; }
	}

	public Gtk.ActionGroup? actions {
		get { return null; }
	}

	public bool deletable {
		get { return true; }
	}

	private Secret.Value? _item_secret = null;
	private DisplayInfo? _info = null;
	private GLib.WeakRef _place;
	private GLib.Cancellable? _req_secret = null;

	construct {
		g_properties_changed.connect((changed_properties, invalidated_properties) => {
			this._info = null;
			freeze_notify();
			notify_property("has-secret");
			notify_property("use");
			notify_property("label");
			notify_property("icon");
			notify_property("markup");
			notify_property("description");
			notify_property("object-flags");
			thaw_notify();
		});
	}

	public override void dispose() {
		if (this._req_secret != null)
			this._req_secret.cancel();
		this._req_secret = null;
		base.dispose();
	}

	public Deleter create_deleter() {
		return new ItemDeleter(this);
	}

	public Gtk.Window? create_viewer(Gtk.Window? parent) {
		return new ItemProperties(this, parent);
	}

    private void ensure_display_info() {
        if (this._info != null)
            return;

        this._info = DisplayInfo();

        var attrs = this.attributes;
        var item_type = get_attribute_string (attrs, "xdg:schema");
        item_type = map_item_type_to_specific (item_type, attrs);
        assert (item_type != null);
        this._info.item_type = item_type;

        var label = base.get_label();
        bool found_custom = false;
        foreach (var entry in DISPLAY_ENTRIES) {
            if (entry.item_type == item_type) {
                found_custom = true;
                if (entry.custom_func != null)
                    entry.custom_func(label, attrs, ref this._info);
                if (this._info.description == null)
                    this._info.description = _(entry.description);
                break;
            }
        }

        // If we look at the schema, we might be able to deduce the app
        if (!found_custom)
            this._info.query_installed_apps (item_type);

        if (this._info.label == null)
            this._info.label = label;
        if (this._info.label == null)
            this._info.label = "";
        if (this._info.details == null)
            this._info.details = "";
    }

	private void load_item_secret() {
		if (this._req_secret == null) {
			this._req_secret = new GLib.Cancellable();
			load_secret.begin(this._req_secret, (obj, res) => {
				try {
					this._req_secret = null;
					load_secret.end(res);
					this._item_secret = base.get_secret();
					notify_property("has-secret");
				} catch (GLib.Error err) {
					GLib.warning("Couldn't retrieve secret: %s", err.message);
				}
			});
		}
	}


	public new void refresh() {
		base.refresh();
		if (this._item_secret != null)
			load_item_secret ();
	}

	public new Secret.Value? get_secret() {
		if (this._item_secret == null)
			load_item_secret ();
		return this._item_secret;
	}

	public string? get_attribute(string name) {
		if (this.attributes == null)
			return null;
		return this.attributes.lookup(name);
	}

	public new async bool set_secret(Secret.Value value,
	                                 GLib.Cancellable? cancellable) throws GLib.Error {
		yield base.set_secret(value, cancellable);
		_item_secret = value;
		notify_property("has-secret");
		return true;
	}
}

private const string GENERIC_SECRET = "org.freedesktop.Secret.Generic";
private const string NETWORK_PASSWORD = "org.gnome.keyring.NetworkPassword";
private const string KEYRING_NOTE = "org.gnome.keyring.Note";
private const string CHAINED_KEYRING = "org.gnome.keyring.ChainedKeyring";
private const string ENCRYPTION_KEY = "org.gnome.keyring.EncryptionKey";
private const string PK_STORAGE = "org.gnome.keyring.PkStorage";
private const string CHROME_PASSWORD = "x.internal.Chrome";
private const string EPIPHANY_PASSWORD = "x.internal.Epiphany";
private const string GOA_PASSWORD = "org.gnome.OnlineAccounts";
private const string TELEPATHY_PASSWORD = "org.freedesktop.Telepathy";
private const string EMPATHY_PASSWORD = "org.freedesktop.Empathy";
private const string NETWORK_MANAGER_SECRET = "org.freedesktop.NetworkManager";
private const string NETWORK_MANAGER_CONNECTION = "org.freedesktop.NetworkManager.Connection";

private string? get_attribute_string(GLib.HashTable<string, string>? attrs,
                                     string name) {
	if (attrs == null)
		return null;
	return attrs.lookup(name);
}

private int get_attribute_int(GLib.HashTable<string, string>? attrs,
                              string name)
{
	if (attrs == null)
		return 0;

	var value = attrs.lookup(name);
	if (value != null)
		return int.parse(value);

	return 0;
}

private bool is_network_item(GLib.HashTable<string, string>? attrs,
                             string match)
{
	var protocol = get_attribute_string (attrs, "protocol");
	return protocol != null && protocol.ascii_casecmp(match) == 0;
}

private bool is_custom_network_label(string? server,
                                     string? user,
                                     string? object,
                                     int port,
                                     string? display)
{
	/*
	 * For network passwords gnome-keyring generates in a funky looking display
	 * name that's generated from login credentials. We simulate that generating
	 * here and return FALSE if it matches. This allows us to display user
	 * customized display names and ignore the generated ones.
	 */

	if (server == null)
		return true;

	var generated = new GLib.StringBuilder("");
	if (user != null)
		generated.append_printf("%s@", user);
	generated.append(server);
	if (port != 0)
		generated.append_printf(":%d", port);
	if (object != null)
		generated.append_printf ("/%s", object);

	return (generated.str == display);
}

private string? calc_network_label(GLib.HashTable<string, string>? attrs,
                                   bool always)
{
	/* HTTP usually has a the realm as the "object" display that */
	if (is_network_item (attrs, "http") && attrs != null) {
		var val = get_attribute_string (attrs, "object");
		if (val != null && val != "")
			return val;

		/* Display the server name as a last resort */
		if (always) {
			val = get_attribute_string (attrs, "server");
			if (val != null && val != "")
				return val;
		}
	}

	return null;
}

private void network_custom(string? display,
                            GLib.HashTable<string, string>? attrs,
                            ref DisplayInfo info)
{
	var server = get_attribute_string (attrs, "server");
	var protocol = get_attribute_string (attrs, "protocol");
	var object = get_attribute_string (attrs, "object");
	var user = get_attribute_string (attrs, "user");
	var port = get_attribute_int (attrs, "port");

	if (protocol == null)
		return;

	/* If it's customized by the application or user then display that */
	if (!is_custom_network_label (server, user, object, port, display))
		info.label = calc_network_label (attrs, true);
	if (info.label == null)
		info.label = display;

	string symbol = "@";
	if (user == null) {
		user = "";
		symbol = "";
	}

	if (object == null)
		object = "";

	if (server != null && protocol != null) {
		info.details = GLib.Markup.printf_escaped("%s://%s%s%s/%s",
		                                          protocol, user, symbol,
		                                          server, object);
	}
}

private void epiphany_custom(string? display,
                             HashTable<string, string>? attrs,
                             ref DisplayInfo info) {
    /* Set the Epiphany icon */
    info.query_installed_apps("org.gnome.Epiphany");

    var uri = get_attribute_string(attrs, "uri");
    if (uri != null)
        info.label = uri;

    var username = get_attribute_string(attrs, "username");
    if (username != null)
        info.details = Markup.escape_text(username);
}

private void chrome_custom(string? display,
                           GLib.HashTable<string, string>? attrs,
                           ref DisplayInfo info)
{
	var origin = get_attribute_string (attrs, "origin_url");

	/* A brain dead url, parse */
	if (display != null && display == origin) {
		try {
			var regex = new GLib.Regex("[a-z]+://([^/]+)/", GLib.RegexCompileFlags.CASELESS, 0);
			GLib.MatchInfo match;
			if (regex.match(display, GLib.RegexMatchFlags.ANCHORED, out match)) {
				if (match.matches())
					info.label = match.fetch(1);
			}
		} catch (GLib.RegexError err) {
			GLib.critical("%s", err.message);
			return;
		}

	}

	if (info.label == null)
		info.label = display;
	if (origin != null)
		info.details = GLib.Markup.escape_text(origin);
	else
		info.details = "";
}

private string? decode_telepathy_id(string account) {
	account.replace("_", "%");
	return GLib.Uri.unescape_string(account);
}

private void empathy_custom(string? display,
                            GLib.HashTable<string, string>? attrs,
                            ref DisplayInfo info)
{
	var account = get_attribute_string(attrs, "account-id");

	/* Translators: This should be the same as the string in empathy */
	var prefix = _("IM account password for ");

	if (display != null && display.has_prefix(prefix)) {
		var len = prefix.length;
		var pos = display.index_of_char ('(', (int)len);
		if (pos != -1)
			info.label = display.slice(len, pos);

		try {
			var regex = new GLib.Regex("^.+/.+/(.+)$", GLib.RegexCompileFlags.CASELESS, 0);
			GLib.MatchInfo match;
			if (regex.match(account, GLib.RegexMatchFlags.ANCHORED, out match)) {
				if (match.matches())
					info.details = decode_telepathy_id (match.fetch(1));
			}
		} catch (GLib.RegexError err) {
			GLib.critical("%s", err.message);
			return;
		}
	}

	if (info.label == null)
		info.label = display;
	if (info.details == null)
		info.details = GLib.Markup.escape_text (account);
}

private void telepathy_custom(string? display,
                              GLib.HashTable<string, string>? attrs,
                              ref DisplayInfo info)
{
	var account = get_attribute_string (attrs, "account");
	if (account != null && display != null && display.index_of(account) != -1) {
		try {
			var regex = new GLib.Regex("^.+/.+/(.+)$", GLib.RegexCompileFlags.CASELESS, 0);
			GLib.MatchInfo match;
			if (regex.match(account, GLib.RegexMatchFlags.ANCHORED, out match)) {
				if (match.matches()) {
					var identifier = match.fetch(1);
					info.label = decode_telepathy_id(identifier);
				}
			}
		} catch (GLib.RegexError err) {
			GLib.critical("%s", err.message);
			return;
		}
	}

	if(info.label == null)
		info.label = display;
	if(account != null)
		info.details = GLib.Markup.escape_text(account);
}

private void network_manager_connection_custom(string? display,
                                               HashTable<string, string>? attrs,
                                               ref DisplayInfo info) {
    var setting_name = get_attribute_string(attrs, "setting-name");
    if (setting_name == "802-11-wireless-security") {
        info.icon = new ThemedIcon("network-wireless-symbolic");
        info.description = _("Wi-Fi password");
    }
}

private const DisplayEntry[] DISPLAY_ENTRIES = {
	{ GENERIC_SECRET, N_("Password or secret"), null},
	{ NETWORK_PASSWORD, N_("Network password"), network_custom },
	{ KEYRING_NOTE, N_("Stored note"), null },
	{ CHAINED_KEYRING, N_("Keyring password"), null },
	{ ENCRYPTION_KEY, N_("Encryption key password"), null },
	{ PK_STORAGE, N_("Key storage password"), null },
	{ EPIPHANY_PASSWORD, N_("GNOME Web password"), epiphany_custom },
	{ CHROME_PASSWORD, N_("Google Chrome password"), chrome_custom },
	{ GOA_PASSWORD, N_("GNOME Online Accounts password"), null },
	{ TELEPATHY_PASSWORD, N_("Telepathy password"), telepathy_custom },
	{ EMPATHY_PASSWORD, N_("Instant messaging password"), empathy_custom },
	{ NETWORK_MANAGER_SECRET, N_("Network Manager secret"), null },
    { NETWORK_MANAGER_CONNECTION, N_("Network connection secret"), network_manager_connection_custom },
};

private struct MappingEntry {
    unowned string item_type;
    unowned string mapped_type;
    unowned string? match_attribute;
    unowned string? match_pattern;
}

private const MappingEntry[] MAPPING_ENTRIES = {
    /* Map some known schema's to their application IDs */
    { "org.gnome.Polari.Identify", "org.gnome.Polari" },
    { "_chrome_dummy_schema_for_unlocking", "google-chrome" },
    { "chrome_libsecret_os_crypt_password_v2", "chromium-browser" },

    /* Browsers */
    { "org.epiphany.FormPassword", EPIPHANY_PASSWORD },
    { "chrome_libsecret_password_schema", CHROME_PASSWORD },
    { GENERIC_SECRET, CHROME_PASSWORD, "application", "chrome*" },

    { GENERIC_SECRET, GOA_PASSWORD, "goa-identity", null },
    { GENERIC_SECRET, TELEPATHY_PASSWORD, "account", "*/*/*" },
    { GENERIC_SECRET, EMPATHY_PASSWORD, "account-id", "*/*/*" },
    /* Network secret for Auto anna/802-11-wireless-security/psk */
    { GENERIC_SECRET, NETWORK_MANAGER_SECRET, "connection-uuid", null },
};

private string map_item_type_to_specific(string? item_type,
                                         GLib.HashTable<string, string>? attrs)
{

    if (item_type == null)
        return GENERIC_SECRET;
    if (attrs == null)
        return item_type;

    foreach (var mapping in MAPPING_ENTRIES) {
        if (item_type != mapping.item_type)
            continue;

        if (mapping.match_attribute == null)
            return mapping.mapped_type;

        var value = get_attribute_string (attrs, mapping.match_attribute);
        if (value != null && mapping.match_pattern != null) {
            if (GLib.PatternSpec.match_simple(mapping.match_pattern, value))
                return mapping.mapped_type;
        } else if (value != null) {
            return mapping.mapped_type;
        }
    }

    return item_type;
}

class ItemDeleter : Deleter {
	private GLib.List<Item> _items;

	public override Gtk.Dialog create_confirm(Gtk.Window? parent) {
		var num = this._items.length();
		if (num == 1) {
			var label = ((Secret.Item)_items.data).label;
			return new DeleteDialog(parent, _("Are you sure you want to delete the password “%s”?"), label);
		} else {
			return new DeleteDialog(parent, GLib.ngettext ("Are you sure you want to delete %d password?",
			                                               "Are you sure you want to delete %d passwords?", num), num);
		}
	}

	public ItemDeleter(Item item) {
		if (!add_object(item))
			GLib.assert_not_reached();
	}

	public override unowned GLib.List<GLib.Object> get_objects() {
		return this._items;
	}

	public override bool add_object (GLib.Object obj) {
		if (obj is Item) {
			this._items.append((Item)obj);
			return true;
		}
		return false;
	}

	public override async bool delete(GLib.Cancellable? cancellable) throws GLib.Error {
		var items = _items.copy();
		foreach (var item in items)
			yield item.delete(cancellable);
		return true;
	}
}

}
}
