using inutralia.Abstractions;
using inutralia.Models;
using Newtonsoft.Json;
using Plugin.Settings;
using Plugin.Settings.Abstractions;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;

namespace inutralia.API
{
    /// <summary>
    /// Servicio de datos que utiliza el plugin de Settings para almacenar modelos
    /// </summary>
    public class LocalDataService : IDataPersistenceService
    {
        // Accesor a los settings
        private static ISettings AppSettings => CrossSettings.Current;

#pragma warning disable CS1998 // Sabemos que no vamos a poner await en los métodos async, por lo que deshabilitamos el warning

        public async Task<bool> DeleteItemAsync<T>(T item) where T : IIdentifiableEntity
        {
            Type type = item.GetType();
            string path = GetItemPath(item);
            if (AppSettings.Contains(path))
            {
                // Eliminar elemento
                AppSettings.Remove(path);

                // Eliminar de la lista de elementos
                var list = GetIdList(type);
                list.Remove(item.Id);
                SetIdList(type, list);
            } //endif

            return true;
        }

        public async Task<T> GetItemAsync<T>(int id) where T : IIdentifiableEntity
        {
            string path = string.Format("Models/{0}/{1}", GetDataPath(typeof(T)), id);
            if (AppSettings.Contains(path))
            {
                var content = AppSettings.GetValueOrDefault<string>(path);
                return JsonConvert.DeserializeObject<T>(content);
            } //endif

            return default(T);
        }

        public async Task<bool> RefreshItemAsync<T>(T item) where T : IIdentifiableEntity
        {
            string path = GetItemPath(item);
            if (AppSettings.Contains(path))
            {
                var content = AppSettings.GetValueOrDefault<string>(path);
                JsonConvert.PopulateObject(content, item);
                return true;
            } //endif

            return false;
        }

        public async Task<List<T>> RefreshListAsync<T>() where T : IIdentifiableEntity
        {
            Type type = typeof(T);
            string basePath = GetDataPath(type);
            List<int> idList = GetIdList(type);
            List<T> retVal = new List<T>();
            List<int> idsToRemove = new List<int>();

            foreach (int id in idList)
            {
                string path = string.Format("Models/{0}/{1}", basePath, id);
                if (AppSettings.Contains(path))
                {
                    var content = AppSettings.GetValueOrDefault<string>(path);
                    retVal.Add(JsonConvert.DeserializeObject<T>(content));
                }
                else
                {
                    idsToRemove.Add(id);
                } //endif
            } //endforeach

            if (idsToRemove.Count > 0)
            {
                // Borrar de la lista inicial los que haya que elilminar, y guardar la lista
                idList.RemoveAll((i) => { return idsToRemove.Contains(i); });
                SetIdList(type, idList);
            } //endif

            return retVal;
        }

        public async Task<int?> UpdateItemAsync<T>(T item, bool isNew = false) where T : IIdentifiableEntity
        {
            if (isNew)
            {
                // Elemento nuevo: asignarle Id. Cogemos la lista de ids ...
                List<int> idList = GetIdList(item.GetType());

                // ... buscamos el máximo y le sumamos 1
                //item.Id = (idList.Count < 1) ? 1 : idList.Max () + 1;

                // ... añadimos el nuevo id a la lista
                idList.Add(item.Id);

                // ... y la guardamos
                SetIdList(item.GetType(), idList);
            }
            else if (!AppSettings.Contains(GetItemPath(item)))
            {
                // El elemento no es nuevo, pero no existe en los datos guardados
                return null;
            } //endif

            // Guardar elemento
            AppSettings.AddOrUpdateValue(GetItemPath(item), JsonConvert.SerializeObject(item));

            // Retornar su id
            return item.Id;
        }

#pragma warning restore CS1998 // Sabemos que no vamos a poner await en los métodos async, por lo que deshabilitamos el warning

        private static List<int> GetIdList(Type t, List<int> listToFill = null)
        {
            // Crear lista si no nos pasan ninguna. Vaciarla.
            List<int> retVal = listToFill ?? new List<int>();
            retVal.Clear();

            // Leer lista de ids (sólo si existe)
            string path = string.Format("ModelIdList/{0}", GetDataPath(t));
            if (AppSettings.Contains(path))
            {
                // Deserializar valor json
                string value = AppSettings.GetValueOrDefault<string>(path);
                JsonConvert.PopulateObject(value, retVal);
            } //endif

            return retVal;
        }

        private static void SetIdList(Type t, List<int> idList)
        {
            string path = string.Format("ModelIdList/{0}", GetDataPath(t));

            if ((idList == null) || (idList.Count < 1))
            {
                // Si la lista está vacía, borrar la entrada de settings
                AppSettings.Remove(path);
            }
            else
            {
                // Serializar con JSON y guardar
                string value = JsonConvert.SerializeObject(idList);
                AppSettings.AddOrUpdateValue(path, value);
            } //endif
        }

        /// <summary>
        /// Retorna el path dentro del api para acceder a cierto tipo de entidad
        /// </summary>
        /// <param name="t">Tipo de dato de la entidad</typeparam>
        /// <returns>El path correspondiente dentro del API</returns>
        private static string GetDataPath(Type t)
        {
            // Si tiene el atributo DataPath, lo utilizamos
            DataPathAttribute attr = t.GetTypeInfo().GetCustomAttribute<DataPathAttribute>();
            if (attr != null)
                return attr.DataPath;

            // En caso contrario devolvemos el nombre de la clase en minúsculas y en plural
            return t.Name.ToLowerInvariant() + "s";
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item"></param>
        /// <returns></returns>
        private static string GetItemPath<T>(T item) where T : IIdentifiableEntity
        {
            return string.Format("Models/{0}/{1}", GetDataPath(item.GetType()), item.Id);
        }
    }
}
