using System;
using System.Collections;
using System.Collections.Generic;

namespace SlatedGameToolkit.Framework.Utilities.Collections
{
    public class LRUCache<K, V> : ICollection
    {
        public V this[K key]
        {
            get
            {
                return Get(key);
            }

            set
            {
                Set(key, value);
            }
        }
        private Node first, last;
        private int maxLength;
        public int MaxLength {
            get {
                return maxLength;
            }

            set {
                maxLength = value;
            }
        }
        Dictionary<K, Node> hashmap;

        public int Count => hashmap.Count;

        public bool IsSynchronized => false;

        public object SyncRoot => this;

        public LRUCache(int initialSize = 1024) {
            this.maxLength = initialSize;
            if (initialSize < 1) throw new ArgumentException("size cannot be less than 1.");
            this.hashmap = new Dictionary<K, Node>(maxLength + 1);
        }

        /// <summary>
        /// Sets the value given the key. If the key already registered with a value, the previous value is replaced.
        /// If the key did not exist, it is registered with the value.
        /// </summary>
        /// <param name="key">The key to associate the value with.</param>
        /// <param name="value">The value to cache.</param>
        public void Set(K key, V value) {
            if (!hashmap.ContainsKey(key)) {
                Node node = new Node(key, value);
                hashmap.Add(key, node);
                AddToStack(node);
                if (hashmap.Count > maxLength) {
                    hashmap.Remove(RemoveLastFromStack().key);
                }
            } else {
                Node node = hashmap[key];
                node.value = value;
                MoveToFrontOfStack(node);
            }
            hashmap[key].value = value;
        }

        /// <summary>
        /// Retrieves the key, and increases the pair's usage ranking.
        /// If the key doesn't exist, a KeyNotFoundException is thrown.
        /// </summary>
        /// <param name="key">The key to retrieve the value for.</param>
        /// <returns>The value.</returns>
        public V Get(K key) {
            if (ContainsKey(key)) {
                Node node = hashmap[key];
                MoveToFrontOfStack(node);
                return node.value;
            }
            throw new KeyNotFoundException();
        }

        /// <summary>
        /// Checks if the key exists and returns it if it does, otherwise,
        /// uses the function to calculate the value and cache it.
        /// </summary>
        /// <param name="key">The key to retrieve the value for.</param>
        /// <param name="func">The function to calculate the value in the case the key doesn't have a associated with it.</param>
        /// <returns>The value.</returns>
        public V ComputeIfNonExistent(K key, Func<K, V> func) {
            if (ContainsKey(key)) {
                return hashmap[key].value;
            }
            V val = func(key);
            Set(key, val);
            return val;
        }

        /// <summary>
        /// If the cache currently holds a value for the key.
        /// The value for the key can be null.
        /// </summary>
        /// <param name="key">The key to verify for a value.</param>
        /// <returns>True if there does exist a value for the key in the cache.</returns>
        public bool ContainsKey(K key) {
            return hashmap.ContainsKey(key);
        }

        private void AddToStack(Node node) {
            node.left = null;
            if (first != null) {
                node.right = first;
                first.left = node;
            } else {
                last = node;
            }
            first = node;
        }

        private Node RemoveLastFromStack() {
            if (last != null) {
                Node last = this.last;
                RemoveFromStack(last);
                return last;
            }
            return null;
        }

        private void MoveToFrontOfStack(Node node) {
            RemoveFromStack(node);
            AddToStack(node);
        }

        private void RemoveFromStack(Node node) {
            if (node.left == null) {
                first = node.right;
            } else {
                node.left.right = node.right;
            }
            if (node.right == null) {
                last = node.left;
            } else {
                node.right.left = node.left;
            }
            node.left = null;
            node.right = null;
        }

        public void CopyTo(Array array, int index)
        {
            Node currentNode = first;
            for (int i = 0; i < array.Length && i < Count; i++) {
                array.SetValue(currentNode.value, i + index);
                currentNode = currentNode.right;
            }
        }

        public IEnumerator GetEnumerator()
        {
            return new LinkedHashmapEnumerable(first);
        }

        private class LinkedHashmapEnumerable : IEnumerator<V>
        {
            Node start, current;
            public LinkedHashmapEnumerable(Node start) {
                this.start = start;
                this.current = start;
            }
            public V Current => current.value;

            object IEnumerator.Current => current.value;

            public void Dispose()
            {
            }

            public bool MoveNext()
            {
                current = current.right;
                return current != null;
            }

            public void Reset()
            {
                current = start;
            }
        }

        private class Node {
            public Node left, right;
            public readonly K key;
            public V value;

            public Node(K key, V value) {
                this.key = key;
                this.value = value;
            }

            public override int GetHashCode() {
                return value.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                if ((obj == null && value == null) || GetType() != obj.GetType())
                {
                    return false;
                }
                
                return value.Equals(obj);
            }
            
        }        
    }

}