using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace UnityEditor.SettingsManagement
{
    /// <summary>
    /// Settings manages a collection of <see cref="ISettingsRepository"/>.
    /// </summary>
    public sealed class Settings
    {
        ISettingsRepository[] m_SettingsRepositories;

        /// <value>
        /// An event that is raised prior to an `ISettingsRepository` serializing it's current state.
        /// </value>
        public event Action beforeSettingsSaved;

        /// <value>
        /// An event that is raised after an `ISettingsRepository` has serialized it's current state.
        /// </value>
        public event Action afterSettingsSaved;

        Settings()
        {
        }

        /// <summary>
        /// Create a new Settings instance with a <see cref="UserSettingsRepository"/> and <see cref="PackageSettingsRepository"/>.
        /// </summary>
        /// <param name="package">The package name. Ex, `com.unity.my-package`.</param>
        /// <param name="settingsFileName">The name of the settings file. Defaults to "Settings."</param>
        public Settings(string package, string settingsFileName = "Settings")
        {
            m_SettingsRepositories = new ISettingsRepository[]
            {
                new PackageSettingsRepository(package, settingsFileName),
                new UserSettingsRepository()
            };
        }

        /// <summary>
        /// Create a new Settings instance with a collection of <see cref="ISettingsRepository"/>.
        /// </summary>
        public Settings(IEnumerable<ISettingsRepository> repositories)
        {
            m_SettingsRepositories = repositories.ToArray();
        }

        /// <summary>
        /// Find a settings repository that matches the requested scope.
        /// </summary>
        /// <param name="scope">The scope of the settings repository to match.</param>
        /// <returns>
        /// An ISettingsRepository instance that is implementing the requested scope. May return null if no
        /// matching repository is found.
        /// </returns>
        public ISettingsRepository GetRepository(SettingsScope scope)
        {
            foreach (var repo in m_SettingsRepositories)
                if (repo.scope == scope)
                    return repo;
            return null;
        }

        /// <summary>
        /// Find a settings repository that matches the requested scope and name.
        /// </summary>
        /// <param name="scope">The scope of the settings repository to match.</param>
        /// <param name="name">The name of the <see cref="ISettingsRepository"/> to match.</param>
        /// <returns>
        /// An <see cref="ISettingsRepository"/> instance that is implementing the requested scope, and matches name.
        /// May return null if no matching repository is found.
        /// </returns>
        public ISettingsRepository GetRepository(SettingsScope scope, string name)
        {
            foreach (var repo in m_SettingsRepositories)
                if (repo.scope == scope && string.Equals(repo.name, name))
                    return repo;
            return null;
        }

        /// <summary>
        /// Serialize the state of all settings repositories.
        /// </summary>
        public void Save()
        {
            if (beforeSettingsSaved != null)
                beforeSettingsSaved();

            foreach (var repo in m_SettingsRepositories)
                repo.Save();

            if (afterSettingsSaved != null)
                afterSettingsSaved();
        }

        /// <summary>
        /// Set a value for key of type T.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <param name="value">The value to set. Must be serializable.</param>
        /// <param name="scope">Which scope this settings should be saved in.</param>
        /// <typeparam name="T">Type of value.</typeparam>
        public void Set<T>(string key, T value, SettingsScope scope = SettingsScope.Project)
        {
            bool foundScopeRepository = false;

            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope)
                {
                    repo.Set<T>(key, value);
                    foundScopeRepository = true;
                }
            }

            if (!foundScopeRepository)
                Debug.LogWarning("No repository for scope " + scope + " found!");
        }

        /// <summary>
        /// Set a value for key of type T.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <param name="value">The value to set. Must be serializable.</param>
        /// <param name="repositoryName">The repository name to match.</param>
        /// <param name="scope">Which scope this settings should be saved in.</param>
        /// <typeparam name="T">Type of value.</typeparam>
        public void Set<T>(string key, T value, string repositoryName, SettingsScope scope = SettingsScope.Project)
        {
            bool foundScopeRepository = false;

            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope && string.Equals(repo.name, repositoryName))
                {
                    repo.Set<T>(key, value);
                    foundScopeRepository = true;
                }
            }

            if (!foundScopeRepository)
                Debug.LogWarning("No repository matching name \"" + repositoryName + "\" and scope " + scope + " found in settings.");
        }

        /// <summary>
        /// Get a value with key of type T, or return the fallback value if no matching key is found.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <param name="scope">Which scope this settings should be retrieved from.</param>
        /// <param name="fallback">If no key with a value of type T is found, this value is returned.</param>
        /// <typeparam name="T">Type of value to search for.</typeparam>
        public T Get<T>(string key, SettingsScope scope = SettingsScope.Project, T fallback = default(T))
        {
            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope)
                    return repo.Get<T>(key, fallback);
            }

            Debug.LogWarning("No repository for scope " + scope + " found!");
            return fallback;
        }

        /// <summary>
        /// Get a value with key of type T, or return the fallback value if no matching key is found.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <param name="repositoryName">The repository name to match.</param>
        /// <param name="scope">Which scope this settings should be retrieved from.</param>
        /// <param name="fallback">If no key with a value of type T is found, this value is returned.</param>
        /// <typeparam name="T">Type of value to search for.</typeparam>
        public T Get<T>(string key, string repositoryName, SettingsScope scope = SettingsScope.Project, T fallback = default(T))
        {
            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope && string.Equals(repo.name, repositoryName))
                    return repo.Get<T>(key, fallback);
            }

            Debug.LogWarning("No repository matching name \"" + repositoryName + "\" and scope " + scope + " found in settings.");
            return fallback;
        }

        /// <summary>
        /// Does the repository contain a setting with key and type.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <typeparam name="T">The type of value to search for.</typeparam>
        /// <param name="scope">Which scope should be searched for matching key.</param>
        /// <returns>True if a setting matching both key and type is found, false if no entry is found.</returns>
        public bool ContainsKey<T>(string key, SettingsScope scope = SettingsScope.Project)
        {
            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope)
                    return repo.ContainsKey<T>(key);
            }

            Debug.LogWarning("No repository for scope " + scope + " found!");
            return false;
        }

        /// <summary>
        /// Does the repository contain a setting with key and type.
        /// </summary>
        /// <param name="key">The settings key.</param>
        /// <param name="repositoryName">The repository name to match.</param>
        /// <typeparam name="T">The type of value to search for.</typeparam>
        /// <param name="scope">Which scope should be searched for matching key.</param>
        /// <returns>True if a setting matching both key and type is found, false if no entry is found.</returns>
        public bool ContainsKey<T>(string key, string repositoryName, SettingsScope scope = SettingsScope.Project)
        {
            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope && string.Equals(repo.name, repositoryName))
                    return repo.ContainsKey<T>(key);
            }

            Debug.LogWarning("No repository matching name \"" + repositoryName + "\" and scope " + scope + " found in settings.");
            return false;
        }

        /// <summary>
        /// Remove a key value pair from a settings repository.
        /// </summary>
        /// <param name="key">The key to remove.</param>
        /// <param name="scope">Which scope should be searched for matching key.</param>
        /// <typeparam name="T">The type that this key is pointing to.</typeparam>
        public void DeleteKey<T>(string key, SettingsScope scope = SettingsScope.Project)
        {
            bool foundScopeRepository = false;

            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope)
                {
                    foundScopeRepository = true;
                    repo.Remove<T>(key);
                }
            }

            if (!foundScopeRepository)
                Debug.LogWarning("No repository for scope " + scope + " found!");
        }

        /// <summary>
        /// Remove a key value pair from a settings repository.
        /// </summary>
        /// <param name="key">The key to remove.</param>
        /// <param name="repositoryName">The repository name to match.</param>
        /// <param name="scope">Which scope should be searched for matching key.</param>
        /// <typeparam name="T">The type that this key is pointing to.</typeparam>
        public void DeleteKey<T>(string key, string repositoryName, SettingsScope scope = SettingsScope.Project)
        {
            bool foundScopeRepository = false;

            foreach (var repo in m_SettingsRepositories)
            {
                if (repo.scope == scope && string.Equals(repo.name, repositoryName))
                {
                    foundScopeRepository = true;
                    repo.Remove<T>(key);
                }
            }

            if (!foundScopeRepository)
                Debug.LogWarning("No repository matching name \"" + repositoryName + "\" and scope " + scope + " found in settings.");
        }
    }
}
