/* Copyright (C) 2016 Canonical Ltd.
 * Author: Luke Yelavich <luke.yelavich@canonical.com>
 *
 * 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/>.
 */

/* Loads, sets, and resets profile settings, and provides access to information
 * stored in the profile manifest.
 */

namespace A11yProfileManager
{

/**
 * Errors which can be thrown when attempting to activate an accessibility
 * profile.
 *
 * @since 0.1
 */
public errordomain ProfileError
{
	/**
	 * No profile group in manifest file:
	 */
	NO_PROFILE_GROUP,
	/**
	 * No name entry in profile group.
	 */
	NO_NAME_ENTRY,
	/**
	 * No description entry in profile group.
	 */
	NO_DESCRIPTION_ENTRY,
	/**
	 * Invalid manifest entry.
	 */
	INVALID_ENTRY,
	/**
	 * No such assistive technology monitor schema.
	 */
	NO_SUCH_MONITOR_SCHEMA,
	/**
	 * A schema was specified, but no key.
	 */
	SCHEMA_WITHOUT_KEY,
	/**
	 * No such assistive technology monitor key.
	 */
	NO_SUCH_MONITOR_KEY,
	/**
	 * GSettings schema does not exist.
	 */
	NO_SUCH_SCHEMA,
	/**
	 * Schema is not relocatable.
	 */
	SCHEMA_NOT_RELOCATABLE,
	/**
	 * Key does not exist in schema.
	 */
	NO_SUCH_KEY,
	/**
	 * Invalid schema path.
	 */
	INVALID_SCHEMA_PATH,
	/**
	 * Invalid key type.
	 */
	INVALID_KEY_TYPE
}

/**
 * This object represents an accessibility profile.
 *
 * An accessibility profile represents a group of settings that can be
 * enabled to provide a better experience for a user with a disability. An
 * accessibility profile can be associated with an assistive technology
 * service such as the Orca Screen reader if desired.
 *
 * Only one accessibility profile can be enabled at a time, as the same setting
 * may be present in multiple profiles, and have multiple different values
 * accross different profiles. The accessibility profile manager library
 * currently only supports GSettings for profile configuration at this time.
 *
 * @since 0.1
 */
public class Profile : Object
{
	/**
	 * The directory where the profile files are stored, relative to the
	 * profiles directory.
	 *
	 * @since 0.1
	 */
	public string dir_name { get; private set; }

	/**
	 * The name of the accessibility profile, as per the manifest file.
	 *
	 * @since 0.1
	 */
	public string name { get; private set; }

	/**
	 * The description of the accessibility profile, as per the manifest
	 * file.
	 *
	 * @since 0.1
	 */
	public string description { get; private set; }

	/**
	 * The GSettings schema to subscribe to for change events, as per the
	 * manifest file.
	 *
	 * @since 0.1
	 */
	public string monitor_schema { get; private set; }

	/**
	 * THe GSettings key associated with the GSettings schema to monitor,
	 * as per the manifest file.
	 *
	 * @since 0.1
	 */
	public string monitor_key { get; private set; }

	/**
	 * The command to execute to run an assistive technology that best
	 * works with this profile.
	 *
	 * @since 0.1.2
	 */
	public string at_command { get; private set; }

	/**
	 * The validity of the accessibility profile, whether the GSettings
	 * exist, the manifest is valid, whether settings values are the
	 * correct value type, etc.
	 *
	 * @since 0.1
	 */
	public bool valid { get; private set; }

	private string manifest_file_name;
	private string gsettings_file_name;
	private string dconf_file_path;

	/**
	 * Creates a new accessibility profile object.
	 *
	 * @param profile_dir The profile directory name relative to the
	 * profiles directory.
	 *
	 * @return A new Profile
	 *
	 * @since 0.1
	 */
	public
	Profile (string profile_dir)
	{
		this.dir_name = profile_dir;
		this.manifest_file_name = Config.PROFILEDIR + "/" + profile_dir + "/profile.manifest";
		this.gsettings_file_name = Config.PROFILEDIR + "/" + profile_dir + "/profile.gsettings";
		this.valid = false;
	}

	/**
	 * Validates the profile manifest and gsettings files found in the
	 * profile directory given at object creation time. If required
	 * manifest data is not present, or the GSettings data is invalid,
	 * #error will be set to #ProfileError.
	 *
	 * The validity of the profile can be checked with
	 * #a11y_profile_manager_profile_get_valid().
	 *
	 * @since 0.1
	 */
	public void
	validate() throws Error
	{
		var manifest = new KeyFile();
		try
		{
			manifest.load_from_file(this.manifest_file_name, KeyFileFlags.NONE);
		}
		catch (Error e)
		{
			this.valid = false;
			throw e;
		}

		if (!manifest.has_group("profile"))
		{
			this.valid = false;
			throw new ProfileError.NO_PROFILE_GROUP
				(N_("The group named 'profile' cannot be found in the manifest file."));
		}

		try
		{
			this.name = manifest.get_locale_string("profile", "name", null);
		}
		catch (KeyFileError e)
		{
			this.valid = false;
			throw new ProfileError.NO_NAME_ENTRY
				(N_("The key named 'name' cannot be found in the group 'profile' in the profile manifest file."));
		}

		try
		{
			this.description = manifest.get_locale_string("profile", "description", null);
		}
		catch (KeyFileError e)
		{
			this.valid = false;
			throw new ProfileError.NO_DESCRIPTION_ENTRY
				(N_("The profile description cannot be found in the manifest file."));
		}

		SettingsSchemaSource schema_source = SettingsSchemaSource.get_default();

		/* The schema fields in the manifest file are optional */
		try
		{
			this.monitor_schema = manifest.get_string("profile", "monitor-schema");
			this.monitor_key = manifest.get_string("profile", "monitor-key");
		}
		catch (KeyFileError e)
		{
			this.monitor_schema = null;
			this.monitor_key = null;
		}

		if (this.monitor_schema != null && this.monitor_schema != "")
		{
			string[] keys;
			SettingsSchema gsettings_schema = schema_source.lookup(this.monitor_schema, true);
			if (gsettings_schema == null)
			{
				this.valid = false;
				throw new ProfileError.NO_SUCH_MONITOR_SCHEMA
					(N_("Cannot find the schema specified in the manifest file."));
			}

			if (this.monitor_key == null)
			{
				this.valid = false;
				throw new ProfileError.SCHEMA_WITHOUT_KEY
					(N_("A GSettings schema was specified in the manifest file, but with no key."));
			}

			keys = gsettings_schema.list_keys();
			bool key_found = false;

			foreach (string key in keys)
			{
				if (key == this.monitor_key)
				{
					key_found = true;
				}
			}

			if (!key_found)
			{
				this.valid = false;
				throw new ProfileError.NO_SUCH_MONITOR_KEY
					(N_("The GSettings key specified in the manifest file cannot be found."));
			}
		}

		try
		{
			this.at_command = manifest.get_string("profile", "at-command");
		}
		catch (Error e)
		{
			this.at_command = null;
		}

		try
		{
			this.process_gsettings(true, false);
		}
		catch (Error e)
		{
			throw e;
		}

		return;
	}

	/**
	 * Sets the settings as per the profile GSettings file.
	 *
	 * @since 0.1
	 */
	public void
	set_settings() throws Error
	{
		try
		{
			if (!this.valid)
			{
				this.validate();
			}

			this.process_gsettings(false, true);
		}
		catch (Error e)
		{
			throw e;
		}
		return;
	}

	/**
	 * Resets the settings given in the profile GSettings file to
	 * defaults.
	 *
	 * @since 0.1
	 */
	public void
	reset_settings() throws Error
	{
		try
		{
			if (!this.valid)
			{
				this.validate();
			}

			this.process_gsettings(false, false);
		}
		catch (Error e)
		{
			throw e;
		}
		return;
	}

	/**
	 * Writes the gsettings data in the profile to a format that is
	 * consumable by the dconf command-line utility. The file can
	 * be used with 'dconf compile' to create a dconf database with
	 * profile settings.
	 *
	 * @param file A file to write the dconf data to.
	 *
	 * @since 0.1.2
	 */
	public void
	write_dconf_file(string file) throws Error
	{
		if (file != null)
		{
			this.dconf_file_path = file;
		}

		try
		{
			if (!this.valid)
			{
				this.validate();
			}

			this.process_gsettings(false, true);
		}
		catch (Error e)
		{
			throw e;
		}
		return;
        }

	private void
	process_gsettings(bool validate_only, bool apply) throws Error
	{
		var gsettings_file = new KeyFile();
		var dconf_file = new KeyFile();
		try
		{
                        gsettings_file.load_from_file(this.gsettings_file_name, KeyFileFlags.NONE);
		}
		catch (Error e)
		{
			this.valid = false;
			throw e;
		}

		string[] gsettings_file_groups = gsettings_file.get_groups();

		foreach (string group in gsettings_file_groups)
		{
			string[] parts = group.split(":", 2);
			string schema_name = parts[0];
			string schema_path = parts[1];
			SettingsSchemaSource schema_source = SettingsSchemaSource.get_default();
			SettingsSchema gsettings_schema = schema_source.lookup(schema_name, true);
			Settings settings;
			string[] keys = null;

			if (gsettings_schema == null)
			{
				this.valid = false;
				throw new ProfileError.NO_SUCH_SCHEMA
					(N_("The GSettings schema %s in the gsettings file cannot be found."), schema_name);
			}

			if (schema_path != null && schema_path != "")
			{
				if (gsettings_schema.get_path() != null)
				{
					this.valid = false;
					throw new ProfileError.SCHEMA_NOT_RELOCATABLE
						(N_("The GSettings schema %s in the gsettings file is not a relocatable schema."), schema_name);
				}

				if (!schema_path.has_prefix("/"))
				{
					this.valid = false;
					throw new ProfileError.INVALID_SCHEMA_PATH
						(N_("The GSettings schema path %s associated with the relocatable schema %s must begin with a slash (/)."),
						schema_path, schema_name);
				}

				if (!schema_path.has_suffix("/"))
				{
					this.valid = false;
					throw new ProfileError.INVALID_SCHEMA_PATH
						(N_("The GSettings schema path %s associated with the relocatable schema %s must end with a slash (/)."),
						schema_name, schema_path);
				}

				if (schema_path.index_of("//") > -1)
				{
					this.valid = false;
					throw new ProfileError.INVALID_SCHEMA_PATH
						(N_("The GSettings schema path %s associated with the relocatable schema %s must not contain two adjacent slashes. (//)"),
						schema_path, schema_name);
				}

				settings = new Settings.full(gsettings_schema, null, schema_path);
			}
			else
			{
				settings = new Settings.full(gsettings_schema, null, null);
			}

			try
			{
				keys = gsettings_file.get_keys(group);
			}
			catch (Error e)
			{
				this.valid = false;
				throw e;
			}

			foreach (string key in keys)
			{
				bool key_found = false;

				foreach (string cur_key in gsettings_schema.list_keys())
				{
					if (cur_key == key)
					{
						key_found = true;
					}
				}

				if (!key_found)
				{
					this.valid = false;
					throw new ProfileError.NO_SUCH_KEY
						(N_("The GSettings schema %s does not contain the key %s."),
						schema_name, key);
				}

				SettingsSchemaKey settings_key = gsettings_schema.get_key(key);
				string value = null;

				try
				{
					value = gsettings_file.get_value(group, key);
				}
				catch (Error e)
				{
					this.valid = false;
					throw e;
				}

				var key_variant_type = settings_key.get_value_type();
				Variant value_variant = null;

				try
				{
					value_variant = Variant.parse(key_variant_type, value);
				}
				catch (Error e)
				{
					this.valid = false;
					throw e;
				}

				if (!validate_only)
				{
					if (this.dconf_file_path != null && this.dconf_file_path != "")
					{
						if (schema_path == null || schema_path == "")
						{
							schema_path = gsettings_schema.get_path();
						}

						string[] dconf_path_elements = schema_path.split("/", -1);

						/* We don't want / at the beginning, so we add the second
						 * element directly, and concatinate the rest with /
						 * separators
						 */
						string dconf_path = dconf_path_elements[1];

						/* THe first and last elements of the array are empty strings */
						for (int pos = 2; pos < dconf_path_elements.length - 1; pos++)
						{
							dconf_path += "/" + dconf_path_elements[pos];
						}

						dconf_file.set_value(dconf_path, key, value);
					}
					else if (apply == true)
					{
						settings.set_value(key, value_variant);
					}
					else
					{
						settings.reset(key);
					}

					Settings.sync();
				}
			}
		}

		if (this.dconf_file_path != null && this.dconf_file_path != "")
		{

			dconf_file.set_value("com/canonical/a11y-profile-manager", "active-profile", this.dir_name);
			try
			{
				dconf_file.save_to_file(this.dconf_file_path);
			}
			catch (Error e)
			{
				throw e;
			}
		}

		this.valid = true;
		return;
	}

}

} // namespace A11yProfileManager
