/*
 * CacheDictionary.cs --
 *
 * Copyright (c) 2007-2012 by Joe Mistachkin.  All rights reserved.
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: $
 */

using System;
using System.Collections.Generic;

#if SERIALIZATION
using System.Runtime.Serialization;
using System.Security.Permissions;
#endif

using Eagle._Attributes;

namespace Eagle._Containers.Public
{
#if SERIALIZATION
    [Serializable()]
#endif
    [ObjectId("8ac7601d-e0f4-406c-9812-f3af76831d89")]
    public class CacheDictionary<TKey, TValue> : Dictionary<TKey, TValue>
    {
        #region Private Constants
        private const double NeverTrimMilliseconds = -1.0; /* never */
        private const double DefaultTrimMilliseconds = 60000.0; /* 1 minute */
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Private Data
        private DateTime? lastTrim;
        private double trimMilliseconds;

        ///////////////////////////////////////////////////////////////////////

        private Dictionary<TKey, DateTime?> accessed;
        private SortedDictionary<DateTime?, List<TKey>> keysAccessed;
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Public Constructors
        public CacheDictionary()
            : base()
        {
            Initialize();
        }

        ///////////////////////////////////////////////////////////////////////

        public CacheDictionary(
            IEqualityComparer<TKey> comparer
            )
            : base(comparer)
        {
            Initialize();
        }

        ///////////////////////////////////////////////////////////////////////

        public CacheDictionary(
            int capacity
            )
            : base(capacity)
        {
            Initialize();
        }

        ///////////////////////////////////////////////////////////////////////

        public CacheDictionary(
            int capacity,
            IEqualityComparer<TKey> comparer
            )
            : base(capacity, comparer)
        {
            Initialize();
        }

        ///////////////////////////////////////////////////////////////////////

        public CacheDictionary(
            IDictionary<TKey, TValue> dictionary
            )
            : base(dictionary)
        {
            Initialize();
        }

        ///////////////////////////////////////////////////////////////////////

        public CacheDictionary(
            IDictionary<TKey, TValue> dictionary,
            IEqualityComparer<TKey> comparer
            )
            : base(dictionary, comparer)
        {
            Initialize();
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Protected Constructors
#if SERIALIZATION
        protected CacheDictionary(
            SerializationInfo info,
            StreamingContext context
            )
            : base(info, context)
        {
            Initialize();
        }
#endif
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Private Helper Methods
        #region Constructor Helper Methods
        private void Initialize()
        {
            lastTrim = null;
            trimMilliseconds = DefaultTrimMilliseconds;

            accessed = new Dictionary<TKey, DateTime?>(this.Comparer);
            keysAccessed = new SortedDictionary<DateTime?, List<TKey>>();
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Remove Helper Methods
        private void Remove( /* O(N) */
            int count
            )
        {
            IEnumerable<TKey> keys = GetOldKeys(count); /* O(N) */

            if (keys != null)
                foreach (TKey key in keys) /* O(M) */
                    if (key != null)
                        Remove(key);
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Iteration Helper Methods
        private IEnumerable<TKey> GetOldKeys( /* O(N) */
            int limit
            )
        {
            List<TKey> keys = new List<TKey>();

            if (keysAccessed != null)
            {
                //
                // NOTE: Gather X of the oldest keys.
                //
                foreach (KeyValuePair<DateTime?, List<TKey>> pair
                        in keysAccessed) /* O(N) */
                {
                    //
                    // NOTE: This may push us over the limit; however, we
                    //       should not really care about this.
                    //
                    if (pair.Value != null)
                        keys.AddRange(pair.Value);

                    //
                    // NOTE: Are we now over the limit?  If so, stop now.
                    //
                    if ((limit != 0) && (keys.Count >= limit))
                        break;
                }
            }

            return keys;
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Last Accessed Helper Methods
        private DateTime Now
        {
            get { return DateTime.UtcNow; }
        }

        ///////////////////////////////////////////////////////////////////////

        private void ClearAccessed()
        {
            if (accessed != null)
                accessed.Clear();

            if (keysAccessed != null)
                keysAccessed.Clear();
        }

        ///////////////////////////////////////////////////////////////////////

        private DateTime? GetAccessed(
            TKey key
            )
        {
            DateTime? dateTime;

            if ((key != null) && (accessed != null) &&
                accessed.TryGetValue(key, out dateTime))
            {
                return dateTime;
            }

            return null;
        }

        ///////////////////////////////////////////////////////////////////////

        private void UpdateAccessed(
            TKey key,
            DateTime? dateTime,
            bool add
            )
        {
            if (key == null)
                return;

            if (accessed != null)
            {
                //
                // NOTE: The date may be null here (i.e. it never expires).
                //
                if (accessed.ContainsKey(key))
                    accessed[key] = dateTime;
                else if (add)
                    accessed.Add(key, dateTime);
            }

            if (add && (dateTime != null) && (keysAccessed != null))
            {
                List<TKey> keys;

                if (keysAccessed.TryGetValue(dateTime, out keys))
                {
                    if (keys != null)
                    {
                        keys.Add(key);
                    }
                    else
                    {
                        keysAccessed[dateTime] = new List<TKey>(
                            new TKey[] { key });
                    }
                }
                else
                {
                    keysAccessed.Add(dateTime, new List<TKey>(
                        new TKey[] { key }));
                }
            }
        }

        ///////////////////////////////////////////////////////////////////////

        private void RemoveAccessed(
            TKey key,
            DateTime? dateTime
            )
        {
            if ((key != null) && (accessed != null))
                accessed.Remove(key);

            if ((dateTime != null) && (keysAccessed != null))
                keysAccessed.Remove(dateTime);
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Last Trim Helper Methods
        private double GetLastTrimMilliseconds()
        {
            if ((trimMilliseconds < 0.0) || (lastTrim == null))
                return NeverTrimMilliseconds;

            return DateTime.UtcNow.Subtract(
                (DateTime)lastTrim).TotalMilliseconds;
        }
        #endregion
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region System.Collections.Generic.Dictionary<TKey, TValue> Overrides
        public new TValue this[TKey key]
        {
            get
            {
                TValue value = base[key]; /* throw */

                UpdateAccessed(key, this.Now, false);

                return value;
            }
            set
            {
                base[key] = value; /* throw */

                UpdateAccessed(key, this.Now, true);
            }
        }

        ///////////////////////////////////////////////////////////////////////

        public new void Add(
            TKey key,
            TValue value
            )
        {
            base.Add(key, value); /* throw */

            UpdateAccessed(key, this.Now, true);
        }

        ///////////////////////////////////////////////////////////////////////

        public new void Clear()
        {
            ClearAccessed();

            base.Clear();
        }

        ///////////////////////////////////////////////////////////////////////

        public new bool ContainsKey(
            TKey key
            )
        {
            UpdateAccessed(key, this.Now, false);

            return base.ContainsKey(key);
        }

        ///////////////////////////////////////////////////////////////////////

        public new bool Remove(
            TKey key
            )
        {
            RemoveAccessed(key, GetAccessed(key));

            return base.Remove(key);
        }

        ///////////////////////////////////////////////////////////////////////

        public new bool TryGetValue(
            TKey key,
            out TValue value
            )
        {
            UpdateAccessed(key, this.Now, false);

            return base.TryGetValue(key, out value);
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region System.Runtime.Serialization.ISerializable Members
#if SERIALIZATION
        [SecurityPermission(
            SecurityAction.LinkDemand,
            Flags = SecurityPermissionFlag.SerializationFormatter)]
        public override void GetObjectData(
            SerializationInfo info,
            StreamingContext context
            )
        {
            if (info != null)
            {
                info.AddValue("lastTrim", lastTrim);
                info.AddValue("trimMilliseconds", trimMilliseconds);
                info.AddValue("accessed", accessed);
                info.AddValue("keysAccessed", keysAccessed);
            }

            base.GetObjectData(info, context);
        }
#endif
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Public Properties
        public virtual DateTime? LastTrim
        {
            get { return lastTrim; }
        }

        ///////////////////////////////////////////////////////////////////////

        public virtual double TrimMilliseconds
        {
            get { return trimMilliseconds; }
            set { trimMilliseconds = value; }
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Public Methods
        public virtual void TrimExcess( /* O(N) */
            int minimum,
            int maximum
            )
        {
            //
            // NOTE: Figure out how many milliseconds it has been since we last
            //       trimmed excess elements, if ever.
            //
            double milliseconds = GetLastTrimMilliseconds();

            //
            // NOTE: If we have never trimmed excess elements or it has been
            //       longer than the configured time span, do it again now.
            //
            if ((milliseconds == NeverTrimMilliseconds) ||
                (milliseconds > trimMilliseconds))
            {
                int count = this.Count;

                if (count > maximum)
                {
                    int removeCount = count - maximum;

                    if ((removeCount >= minimum) &&
                        ((count - removeCount) >= minimum))
                    {
                        Remove(removeCount);

                        //
                        // NOTE: Ok, we have successfully trimmed the excess
                        //       elements, record the current time so that we
                        //       will not do it again too soon.
                        //
                        lastTrim = DateTime.UtcNow;
                    }
                }
            }
        }
        #endregion
    }
}
