#region License

/*
 * Copyright 2002-2004 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#endregion

using System;
using System.Collections;
using System.Web;
using System.Web.Caching;
using Common.Logging;
using Spring.Util;

namespace Spring.Caching
{
    /// <summary>
    /// An <see cref="ICache"/> implementation backed by ASP.NET Cache (see <see cref="HttpRuntime.Cache"/>).
    /// </summary>
    /// <remarks>
    /// <para>
    /// Because ASP.NET Cache uses strings as cache keys, you need to ensure
    /// that the key object type has properly implemented <b>ToString</b> method.
    /// </para>
    /// <para>
    /// Despite the shared underlying <see cref="HttpRuntime.Cache"/>, it is possible to use more than
    /// one instance of <see cref="AspNetCache"/> without interfering each other.
    /// </para>
    /// </remarks>
    /// <author>Aleksandar Seovic</author>
    /// <author>Erich Eichinger</author>
    public class AspNetCache : AbstractCache
    {
        #region Internal Abstractions

        /// <summary>
        /// Abstracts the underlying runtime cache
        /// </summary>
        public interface IRuntimeCache : IEnumerable
        {
            /// <summary>
            /// Insert an item into the cache.
            /// </summary>
            void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration,
                   TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback);

            /// <summary>
            /// Removes an item from the cache.
            /// </summary>
            /// <param name="key">The key of the item to remove</param>
            /// <returns>The object that has been removed from the cache</returns>
            object Remove(string key);

            /// <summary>
            /// Retrieve an item with the specified key from the cache.
            /// </summary>
            /// <param name="key">The key of the item to be retrieved</param>
            /// <returns>The item, if found. <value>null</value> otherwise</returns>
            object Get(string key);
        }

        /// <summary>
        /// Actually delegates all calls to the underlying <see cref="HttpRuntime.Cache"/>
        /// </summary>
        private class RuntimeCache : IRuntimeCache
        {
            private readonly Cache _runtimeCache = HttpRuntime.Cache;

            public object Remove(string key)
            {
                return _runtimeCache.Remove(key);
            }

            public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback )
            {
                _runtimeCache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback);
            }

            public object Get(string key)
            {
                return _runtimeCache.Get(key);
            }

            public IEnumerator GetEnumerator()
            {
                return _runtimeCache.GetEnumerator();
            }
        }

        #endregion

        #region Fields

        // logger instance for this class
        private static readonly ILog Log = LogManager.GetLogger(typeof(AspNetCache));
        // the concrete cache implementation
        private readonly IRuntimeCache _cache;
        // the (unique!) name of this particular cache instance.
        private readonly string _cacheName;

        // hold default values for this cache instance.
        private CacheItemPriority _priority = CacheItemPriority.Default;
        private bool _slidingExpiration = false;

        #endregion

        /// <summary>
        /// Initializes a new instance of <see cref="AspNetCache"/> 
        /// </summary>
        public AspNetCache() 
            :this(new RuntimeCache())
        {
        }

        /// <summary>
        /// Initializes a new instance of <see cref="AspNetCache"/>
        /// with the specified <see cref="IRuntimeCache"/> implementation.
        /// </summary>
        /// <param name="runtimeCache"></param>
        public AspNetCache(IRuntimeCache runtimeCache)
        {
            _cache = runtimeCache;
            // noop
            _cacheName = typeof(AspNetCache).FullName + "[" + this.GetHashCode() + "].";            
        }

        #region Configurable Properties

        /// <summary>
        /// Gets/Sets a flag, whether to use sliding expiration policy.
        /// </summary>
        public bool SlidingExpiration
        {
            get { return _slidingExpiration; }
            set { _slidingExpiration = value; }
        }

        /// <summary>
        /// Gets/Sets a default priority to be applied to all items inserted into this cache.
        /// </summary>
        public CacheItemPriority Priority
        {
            get { return _priority; }
            set { _priority = value; }
        }

        #endregion

        /// <summary>
        /// Gets a collection of all cache item keys.
        /// </summary>
        public override ICollection Keys
        {
            get
            {
                ArrayList keys = new ArrayList();
                
                foreach (DictionaryEntry entry in _cache)
                {
                    string key = (string) entry.Key;
                    if (key.StartsWith(_cacheName))
                    {
                        keys.Add(key.Substring(_cacheName.Length));
                    }
                }

                return keys;
            }
        }

        /// <summary>
        /// Retrieves an item from the cache.
        /// </summary>
        /// <param name="key">
        /// Item key.
        /// </param>
        /// <returns>
        /// Item for the specified <paramref name="key"/>, or <c>null</c>.
        /// </returns>
        public override object Get(object key)
        {
            return key == null ? null : _cache.Get(GenerateKey(key));
        }

        /// <summary>
        /// Removes an item from the cache.
        /// </summary>
        /// <param name="key">
        /// Item key.
        /// </param>
        public override void Remove(object key)
        {
            if (key != null)
            {
                if (Log.IsDebugEnabled) Log.Debug(string.Format("removing item '{0}' from cache '{1}'", key, this._cacheName));
                _cache.Remove(GenerateKey(key));
            }
        }

        /// <summary>
        /// Inserts an item into the cache.
        /// </summary>
        /// <remarks>
        /// Items inserted using this method have default <see cref="Priority"/> and default <see cref="SlidingExpiration"/>
        /// </remarks>
        /// <param name="key">
        /// Item key.
        /// </param>
        /// <param name="value">
        /// Item value.
        /// </param>
        /// <param name="timeToLive">
        /// Item's time-to-live (TTL).
        /// </param>
        protected override void DoInsert(object key, object value, TimeSpan timeToLive)
        {
            this.Insert( key, value, timeToLive, _slidingExpiration, _priority );
        }

        /// <summary>
        /// Inserts an item into the cache.
        /// </summary>
        /// <param name="key">
        /// Item key.
        /// </param>
        /// <param name="value">
        /// Item value.
        /// </param>
        /// <param name="timeToLive">
        /// Item's time-to-live.
        /// </param>
        /// <param name="slidingExpiration">
        /// Flag specifying whether the item's time-to-live should be reset
        /// when the item is accessed.
        /// </param>
        public void Insert(object key, object value, TimeSpan timeToLive, bool slidingExpiration)
        {
            this.Insert(key, value, timeToLive, slidingExpiration, _priority);            
        }

        /// <summary>
        /// Inserts an item into the cache.
        /// </summary>
        /// <param name="key">
        /// Item key.
        /// </param>
        /// <param name="value">
        /// Item value.
        /// </param>
        /// <param name="timeToLive">
        /// Item's time-to-live.
        /// </param>
        /// <param name="slidingExpiration">
        /// Flag specifying whether the item's time-to-live should be reset
        /// when the item is accessed.
        /// </param>
        /// <param name="itemPriority">
        /// Item priority.
        /// </param>
        public void Insert(object key, object value, TimeSpan timeToLive, bool slidingExpiration, CacheItemPriority itemPriority)
        {
            AssertUtils.ArgumentNotNull(key, "key");
            AssertUtils.State( TimeSpan.Zero <= timeToLive, "timeToLive" );

            if (Log.IsDebugEnabled) Log.Debug(string.Format("adding item '{0}' to cache '{1}'", key, this._cacheName));

            if (TimeSpan.Zero < timeToLive)
            {
                if (slidingExpiration)
                {
                    TimeSpan slidingExpirationSpan = timeToLive;
                    _cache.Insert(GenerateKey(key), value, null, Cache.NoAbsoluteExpiration, slidingExpirationSpan, itemPriority, null);
                }
                else
                {
                    DateTime absoluteExpiration = DateTime.Now.Add(timeToLive);
                    _cache.Insert(GenerateKey(key), value, null, absoluteExpiration, Cache.NoSlidingExpiration, itemPriority, null);
                }
            }
            else
            {
                _cache.Insert(GenerateKey(key), value, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, itemPriority, null);
            }
        }

        /// <summary>
        /// Generate a key to be used for the underlying <see cref="Cache"/> implementation.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string GenerateKey( object key )
        {
            return _cacheName + key;
        }
    }
}