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

namespace Arugula.Games
{
    [CreateAssetMenu(fileName = "Scoreboard.asset", menuName = "Arugula/Scoreboard")]
    public class Scoreboard : ScriptableObject
    {
        public enum StorageMode { None, PlayerPrefs, LocalFile, Database, WebService }
        static DateTime Epoch = new DateTime(1970, 1, 1);
        static Dictionary<string, Scoreboard> scoreboards = new Dictionary<string, Scoreboard>();
        public static Scoreboard Get(string name) => scoreboards.ContainsKey(name) ? scoreboards[name] : null;
        public static IEnumerable<Scoreboard> All => scoreboards.Values.AsEnumerable();

        [Serializable]
        public class Collection : IEnumerable<Entry>
        {
            [SerializeField]
            public List<Entry> entries = new List<Entry>();

            public Entry this[int index] => index < entries.Count ? entries[index] : null;
            public Entry this[string name] => entries.FirstOrDefault(x => x.name == name);
            public int Count => entries.Count;

            public int IndexOf(Entry e) => entries.IndexOf(e);

            public Entry First => scoreboard.descending ? entries.OrderByDescending(x => x.Value).FirstOrDefault() : entries.OrderBy((x) => x.Value).FirstOrDefault();
            public Entry Last => !scoreboard.descending ? entries.OrderByDescending(x => x.Value).FirstOrDefault() : entries.OrderBy((x) => x.Value).FirstOrDefault();
            public Entry MostRecent => entries.OrderByDescending(x => x.time).FirstOrDefault();

            [NonSerialized]
            internal Scoreboard scoreboard;

            public Collection() { }

            public Collection(Collection src)
            {
                foreach (var e in src.entries)
                    entries.Add(new Entry(e));
            }

            public void Sort()
            {

                if (Count <= 1)
                    return;

                if (scoreboard.descending)
                    entries.Sort((a, b) => b.Value.CompareTo(a.Value));
                else
                    entries.Sort((a, b) => a.Value.CompareTo(b.Value));
            }

            public IEnumerator<Entry> GetEnumerator()
            {
                return entries.GetEnumerator();
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }

        [Serializable]
        public class Entry
        {
            public event Action<Entry> OnUpdated = delegate { };

            public string name;
            [SerializeField]
            private int value;
            [TimeStamp]
            public double startTime;
            [TimeStamp]
            public double time;
            public string meta;
            public DateTime dateTime => (Epoch + TimeSpan.FromSeconds(time)).ToLocalTime();
            [NonSerialized]
            internal Scoreboard scoreboard;

            public Entry(Entry src)
            {
                name = src.name;
                value = src.value;
                startTime = src.startTime;
                time = src.time;
                meta = src.meta;
                scoreboard = src.scoreboard;
            }

            public Entry(string name, int value, string meta)
            {
                this.name = name;
                this.value = value;
                this.meta = meta;
                this.time = (DateTime.UtcNow - Epoch).TotalSeconds;
                this.startTime = this.time;
            }

            public int Value
            {
                get
                {
                    return value;
                }
                set
                {
                    this.value = value;
                    time = (DateTime.UtcNow - Epoch).TotalSeconds;
                    OnUpdated(this);
                }

            }

            public void Archive(bool serialize = false)
            {
                if (scoreboard.archived.Contains(this))
                    throw new Exception("Entry already archived!");

                scoreboard.Archive(this);
                if (serialize)
                    scoreboard.SaveToPrefs();
            }

            public void Purge(bool serialize = false)
            {
                scoreboard.Remove(this);
                if (serialize)
                    scoreboard.SaveToPrefs();
            }

            public float DisplayValue => value * scoreboard.multiplier;

            public static bool operator >(Entry x, Entry y)
            {
                if (x.scoreboard.descending)
                    return x.value > y.value;
                else
                    return x.value < y.value;
            }

            public static bool operator <(Entry x, Entry y)
            {
                if (x.scoreboard.descending)
                    return x.value > y.value;
                else
                    return x.value < y.value;
            }

            public static Entry operator +(Entry x, int value)
            {
                if (!x.scoreboard.negative && x.Value + value < 0)
                {
                    x.Value = 0;
                }
                else
                {
                    x.Value = (x.Value + value);
                }

                return x;
            }

            public static Entry operator -(Entry x, int value)
            {
                return x + (-value);
            }
        }

        public event Action<Entry> OnEntryAdded = delegate { };
        public event Action<Entry> OnEntryArchived = delegate { };
        public event Action OnClearedArchive = delegate { };
        public event Action OnClearedActive = delegate { };
        public event Action<Scoreboard> OnLoaded = delegate { };

        public StorageMode storageMode = StorageMode.PlayerPrefs;
        public string displayName;
        public Sprite icon;
        public string info = null;
        public bool descending = true;
        public bool negative = false;
        public float multiplier = 1;
        [Min(0)]
        public int size = 10;

        public Collection Active => active;
        public Collection Archived => archived;

        [SerializeField]
        private Collection Default;

        [NonSerialized]
        public bool Initialized;

        Collection archived;
        Collection active;

        public void Initialize(bool autoLoad = true)
        {
            if (Initialized)
                return;

            archived = new Collection(Default);
            archived.scoreboard = this;

            active = new Collection();
            active.scoreboard = this;

            scoreboards[name] = this;

            Initialized = true;

            if (autoLoad)
                Load();
        }

        public Entry CreateActiveEntry(string name = "Player", int value = 0, string metaData = null)
        {
            return Add(new Entry(name, value, metaData));
        }

        public void Remove(Entry e)
        {
            active.entries.Remove(e);
            archived.entries.Remove(e);
        }

        public Entry Add(Entry e)
        {
            e.scoreboard = this;
            active.entries.Add(e);

            OnEntryAdded?.Invoke(e);
            return e;
        }

        public Entry Archive(Entry e)
        {
            if (active.entries.Contains(e))
            {
                active.entries.Remove(e);
                archived.entries.Add(e);
                archived.Sort();
                Truncate();

                OnEntryArchived?.Invoke(e);
                return e;
            }

            return null;
        }

        //public Scoreboard(string id, string name, string info = null, bool desc = false, float multiplier = 1, int size = 10)
        //{
        //    if (PlayerPrefs.HasKey(id))
        //    {
        //        JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(id), this);

        //        archived.scoreboard = this;
        //        foreach (var e in archived)
        //            e.scoreboard = this;
        //    }
        //    else
        //    {
        //        this.displayName = name;
        //        this.info = info;
        //        this.descending = desc;
        //        this.multiplier = multiplier;
        //        this.size = size;
        //        archived = new Collection();
        //        active = new Collection();
        //    }
        //}

        //public Scoreboard(string json)
        //{
        //    JsonUtility.FromJsonOverwrite(json, archived);
        //    archived.scoreboard = this;
        //    foreach (var e in archived)
        //        e.scoreboard = this;
        //}

        string GetJson(bool prettyPrint = true)
        {
            return JsonUtility.ToJson(archived, prettyPrint);
        }

        public void Truncate()
        {
            if (size <= 0)
                return;

            while (archived.Count > size)
            {
                archived.entries.RemoveAt(archived.Count - 1);
            }
        }

        public void ClearArchive()
        {
            archived.entries.Clear();
            OnClearedArchive?.Invoke();
        }

        public void ClearActive()
        {
            if (active == null) return;
            active.entries.Clear();
            OnClearedActive?.Invoke();
        }

        public void Save()
        {
            switch (storageMode)
            {
                case StorageMode.None:
                    break;
                case StorageMode.PlayerPrefs:
                    SaveToPrefs();
                    break;
                default:
                    Debug.LogWarning("Storage Mode: " + storageMode + " Not Implemented yet!");
                    break;
            }
        }

        public void Load()
        {
            switch (storageMode)
            {
                case StorageMode.None:
                    break;
                case StorageMode.PlayerPrefs:
                    LoadFromPrefs();
                    break;
                default:
                    Debug.LogWarning("Storage Mode: " + storageMode + " Not Implemented yet!");
                    break;
            }

            foreach (var e in archived)
                e.scoreboard = this;
        }

        void LoadFromPrefs()
        {
            if (PlayerPrefs.HasKey("SB_" + name))
            {
                JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString("SB_" + name), archived);

                archived.scoreboard = this;
                foreach (var e in archived)
                    e.scoreboard = this;
            }

            if (OnLoaded != null)
                OnLoaded(this);
        }

        void ClearPrefs()
        {
            PlayerPrefs.DeleteKey("SB_" + name);
        }

        void SaveToPrefs()
        {
            PlayerPrefs.SetString("SB_" + name, GetJson(false));
        }
    }
}
