// <copyright file="MemoryCacheEntry.cs" company="Microsoft">
//   Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
// </copyright>
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;

namespace System.Runtime.Caching {
    internal class MemoryCacheEntry: MemoryCacheKey {
        const byte EntryStateMask = 0x1f;

        private Object _value;
        private DateTime _utcCreated;
        // expiration
        private DateTime _utcAbsExp;
        private TimeSpan _slidingExp;
        private ExpiresEntryRef _expiresEntryRef;
        private byte _expiresBucket; // index of the expiration list (bucket)
        // usage
        private byte _usageBucket;  // index of the usage list (== priority-1)
        private UsageEntryRef _usageEntryRef;   // ref into the usage list
        private DateTime _utcLastUpdateUsage;   // time we last updated usage

        private CacheEntryRemovedCallback _callback;
        private SeldomUsedFields _fields; // optimization to reduce workingset when the entry hasn't any dependencies

        class SeldomUsedFields {
            internal Collection<ChangeMonitor> _dependencies; // the entry's dependency needs to be disposed when the entry is released
            internal Dictionary<MemoryCacheEntryChangeMonitor, MemoryCacheEntryChangeMonitor> _dependents;  // dependents must be notified when this entry is removed
            internal MemoryCache _cache;
            internal Tuple<MemoryCacheStore, MemoryCacheEntry> _updateSentinel; // the MemoryCacheEntry (and its associated store) of the OnUpdateSentinel for this entry, if there is one
        }

        internal Object Value { 
            get { return _value; }
        }

        internal bool HasExpiration() {
            return _utcAbsExp < DateTime.MaxValue;
        }

        internal DateTime UtcAbsExp {
            get { return _utcAbsExp; }
            set { _utcAbsExp = value; }
        }

        internal DateTime UtcCreated {
            get { return _utcCreated; }
        }

        internal ExpiresEntryRef ExpiresEntryRef {
            get { return _expiresEntryRef; }
            set { _expiresEntryRef = value; }
        }

        internal byte ExpiresBucket {
            get { return _expiresBucket; }
            set { _expiresBucket = value; }
        }

        internal bool InExpires() {
            return !_expiresEntryRef.IsInvalid;
        }

        internal TimeSpan SlidingExp {
            get { return _slidingExp; }
        }

        internal EntryState State {
            get { return (EntryState)(_bits & EntryStateMask); }
            set { _bits = (byte)(((uint)_bits & ~(uint)EntryStateMask) | (uint)value); }
        }

        internal byte UsageBucket {
            get { return _usageBucket; }
        }

        internal UsageEntryRef UsageEntryRef {
            get { return _usageEntryRef; }
            set { _usageEntryRef = value; }
        }

        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Grandfathered suppression from original caching code checkin")]
        internal DateTime UtcLastUpdateUsage {
            get { return _utcLastUpdateUsage; }
            set { _utcLastUpdateUsage = value; }
        }

        internal MemoryCacheEntry(String key,
                                  Object value,
                                  DateTimeOffset absExp, 
                                  TimeSpan slidingExp, 
                                  CacheItemPriority priority, 
                                  Collection<ChangeMonitor> dependencies,
                                  CacheEntryRemovedCallback removedCallback,
                                  MemoryCache cache) : base(key) {
            if (value == null) {
                throw new ArgumentNullException("value");
            }
            _utcCreated = DateTime.UtcNow;
            _value = value;

            _slidingExp = slidingExp;
            if (_slidingExp > TimeSpan.Zero) {
                _utcAbsExp = _utcCreated + _slidingExp;
            }
            else {
                _utcAbsExp = absExp.UtcDateTime;
            }

            _expiresEntryRef = ExpiresEntryRef.INVALID;
            _expiresBucket = 0xff;

            _usageEntryRef = UsageEntryRef.INVALID;
            if (priority == CacheItemPriority.NotRemovable) {
                _usageBucket = 0xff;
            }
            else {
                _usageBucket = 0;
            }

            _callback = removedCallback;

            if (dependencies != null) {
                _fields = new SeldomUsedFields();
                _fields._dependencies = dependencies;
                _fields._cache = cache;
            }
        }

        internal void AddDependent(MemoryCache cache, MemoryCacheEntryChangeMonitor dependent) {
            lock (this) {
                if (State > EntryState.AddedToCache) {
                    return;
                }
                if (_fields == null) {
                    _fields = new SeldomUsedFields();
                }
                if (_fields._cache == null) {
                    _fields._cache = cache;
                }
                if (_fields._dependents == null) {
                    _fields._dependents = new Dictionary<MemoryCacheEntryChangeMonitor, MemoryCacheEntryChangeMonitor>();
                }
                _fields._dependents[dependent] = dependent;
            }
        }

        private void CallCacheEntryRemovedCallback(MemoryCache cache, CacheEntryRemovedReason reason) {
            if (_callback == null) {
                return;
            }
            CacheEntryRemovedArguments args = new CacheEntryRemovedArguments(cache, reason, new CacheItem(Key, _value));
            try {
                _callback(args);
            }
            catch {
                // 
            }
        }

        internal void CallNotifyOnChanged() {
            if (_fields != null && _fields._dependencies != null) {
                foreach (ChangeMonitor monitor in _fields._dependencies) {
                    monitor.NotifyOnChanged(new OnChangedCallback(this.OnDependencyChanged));
                }
            }
        }

        // Associates this entry with an update sentinel. If this entry has a sliding expiration, we need to
        // touch the sentinel so that it doesn't expire.
        internal void ConfigureUpdateSentinel(MemoryCacheStore sentinelStore, MemoryCacheEntry sentinelEntry) {
            lock (this) {
                if (_fields == null) {
                    _fields = new SeldomUsedFields();
                }
                _fields._updateSentinel = Tuple.Create(sentinelStore, sentinelEntry);
            }
        }

        internal bool HasUsage() {
            return _usageBucket != 0xff;
        }

        internal bool InUsage() {
            return !_usageEntryRef.IsInvalid;
        }

        private void OnDependencyChanged(Object state) {
            if (State == EntryState.AddedToCache) {
                _fields._cache.RemoveEntry(this.Key, this, CacheEntryRemovedReason.ChangeMonitorChanged);
            }
        }

        internal void Release(MemoryCache cache, CacheEntryRemovedReason reason) {
            State = EntryState.Closed;

            // Are there any cache entries that depend on this entry?
            // If so, we need to fire their dependencies.
            Dictionary<MemoryCacheEntryChangeMonitor, MemoryCacheEntryChangeMonitor>.KeyCollection deps = null;
            // clone the dependents
            lock (this) {
                if (_fields != null && _fields._dependents != null && _fields._dependents.Count > 0) {
                    deps = _fields._dependents.Keys;
                    // set to null so RemoveDependent does not attempt to access it, since we're not
                    // using a copy of the KeyCollection.
                    _fields._dependents = null;
                    Dbg.Assert(_fields._dependents == null, "_fields._dependents == null");
                }
            }
            if (deps != null) {
                foreach (MemoryCacheEntryChangeMonitor dependent in deps) {
                    if (dependent != null) {
                        dependent.OnCacheEntryReleased();
                    }
                }
            }

            CallCacheEntryRemovedCallback(cache, reason);

            // Dispose any dependencies
            if (_fields != null && _fields._dependencies != null) {
                foreach (ChangeMonitor monitor in _fields._dependencies) {
                    monitor.Dispose();
                }
            }
        }

        internal void RemoveDependent(MemoryCacheEntryChangeMonitor dependent) {
            lock (this) {
                if (_fields._dependents != null) {
                    _fields._dependents.Remove(dependent);
                }
            }
        }

        internal void UpdateSlidingExp(DateTime utcNow, CacheExpires expires) {
            if (_slidingExp > TimeSpan.Zero) {
                DateTime utcNewExpires = utcNow + _slidingExp;
                if (utcNewExpires - _utcAbsExp >= CacheExpires.MIN_UPDATE_DELTA || utcNewExpires < _utcAbsExp) {
                    expires.UtcUpdate(this, utcNewExpires);
                }
            }
        }

        internal void UpdateSlidingExpForUpdateSentinel() {
            // We don't need a lock to get information about the update sentinel
            SeldomUsedFields fields = _fields;
            if (fields != null) {
                Tuple<MemoryCacheStore, MemoryCacheEntry> sentinelInfo = fields._updateSentinel;

                // touch the update sentinel to keep it from expiring
                if (sentinelInfo != null) {
                    MemoryCacheStore sentinelStore = sentinelInfo.Item1;
                    MemoryCacheEntry sentinelEntry = sentinelInfo.Item2;
                    sentinelStore.UpdateExpAndUsage(sentinelEntry, updatePerfCounters: false); // perf counters shouldn't be polluted by touching update sentinel entry
                }
            }
        }

        internal void UpdateUsage(DateTime utcNow, CacheUsage usage) {
            // update, but not more frequently than once per second.
            if (InUsage() && _utcLastUpdateUsage < utcNow - CacheUsage.CORRELATED_REQUEST_TIMEOUT) {
                _utcLastUpdateUsage = utcNow;
                usage.Update(this);
                if (_fields != null && _fields._dependencies != null) {
                    foreach (ChangeMonitor monitor in _fields._dependencies) {
                        MemoryCacheEntryChangeMonitor m = monitor as MemoryCacheEntryChangeMonitor;
                        if (m == null) {
                            continue;
                        }
                        foreach (MemoryCacheEntry e in m.Dependencies) {
                            MemoryCacheStore store = e._fields._cache.GetStore(e);
                            e.UpdateUsage(utcNow, store.Usage);
                        }
                    }
                }
            }
        }
    }
}
