/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 * 
 *  http://aws.amazon.com/apache2.0
 * 
 * or in the "license" file accompanying this file. This file 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.
 */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Amazon.Util;
using Amazon.Runtime.Internal.Util;
using ThirdParty.Json.LitJson;
using System.Threading;

namespace Amazon.Runtime.Internal.Util
{
    /// <summary>
    /// Interface for a non-generic cache.
    /// </summary>
    public interface ICache
    {
        /// <summary>
        /// Clears the entire cache.
        /// </summary>
        void Clear();

        /// <summary>
        /// Maximum time to keep an item around after its last use.
        /// </summary>
        TimeSpan MaximumItemLifespan { get; set; }

        /// <summary>
        /// How often should the cache be cleared of old items.
        /// </summary>
        TimeSpan CacheClearPeriod { get; set; }

        /// <summary>
        /// The number of items in the cache.
        /// </summary>
        int ItemCount { get; }
    }

    /// <summary>
    /// Interface for a generic cache.
    /// </summary>
    /// <typeparam name="TKey"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    public interface ICache<TKey, TValue> : ICache
    {
        /// <summary>
        /// Retrieves a value out of the cache or from the source.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="creator"></param>
        /// <returns></returns>
        TValue GetValue(TKey key, Func<TKey, TValue> creator);

        /// <summary>
        /// Retrieves a value out of the cache or from the source.
        /// If the item was in the cache, isStaleItem is set to true;
        /// otherwise, if the item comes from the source, isStaleItem is false.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="creator"></param>
        /// <param name="isStaleItem"></param>
        /// <returns></returns>
        TValue GetValue(TKey key, Func<TKey, TValue> creator, out bool isStaleItem);

        /// <summary>
        /// Clears a specific value from the cache if it's there.
        /// </summary>
        /// <param name="key"></param>
        void Clear(TKey key);

        /// <summary>
        /// Returns the keys for all items in the cache.
        /// </summary>
        /// <returns></returns>
        List<TKey> Keys { get; }

        /// <summary>
        /// Executes specified operation, catches exception, clears the cache for
        /// the given key, retries the operation.
        /// </summary>
        /// <typeparam name="TOut"></typeparam>
        /// <param name="key"></param>
        /// <param name="operation"></param>
        /// <param name="onError"></param>
        /// <param name="shouldRetryForException"></param>
        /// <returns></returns>
        TOut UseCache<TOut>(TKey key, Func<TOut> operation, Action onError, Predicate<Exception> shouldRetryForException);
    }
    
    /// <summary>
    /// SDK-wide cache.
    /// Provides access to caches specific to a particular set of credentials
    /// and target region.
    /// </summary>
    public static class SdkCache
    {
        private static object cacheLock = new object();
        private static Cache<CacheKey, ICache> cache = new Cache<CacheKey, ICache>();

        /// <summary>
        /// Clear all caches
        /// </summary>
        public static void Clear()
        {
            cache.Clear();
        }

        /// <summary>
        /// Clear all caches of a particular type
        /// </summary>
        /// <param name="cacheType"></param>
        public static void Clear(object cacheType)
        {
            lock (cacheLock)
            {
                var keys = cache.Keys;
                foreach (CacheKey key in keys)
                {
                    if (AWSSDKUtils.AreEqual(key.CacheType, cacheType))
                    {
                        var value = cache.GetValue(key, null);
                        value.Clear();
                    }
                }
            }
        }

        /// <summary>
        /// Retrieve a cache of a specific type for a client object.
        /// The client object can be null in cases where a cache does
        /// not correspond to a specific AWS account or target region.
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="client"></param>
        /// <param name="cacheIdentifier"></param>
        /// <param name="keyComparer"></param>
        /// <returns></returns>
        public static ICache<TKey, TValue> GetCache<TKey, TValue>(
            object client,
            object cacheIdentifier,
            IEqualityComparer<TKey> keyComparer)
        {
            return GetCache<TKey, TValue>(client as AmazonServiceClient, cacheIdentifier, keyComparer);
        }

        /// <summary>
        /// Retrieve a cache of a specific type for a client object.
        /// The client object can be null in cases where a cache does
        /// not correspond to a specific AWS account or target region.
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="client"></param>
        /// <param name="cacheIdentifier"></param>
        /// <param name="keyComparer"></param>
        /// <returns></returns>
        public static ICache<TKey, TValue> GetCache<TKey, TValue>(
            AmazonServiceClient client,
            object cacheIdentifier,
            IEqualityComparer<TKey> keyComparer)
        {
            // If client is null, create an empty key to use.
            // This supports mock frameworks (where the service is mocked up,
            // but there is no client to get credentials/region from) and
            // caches that do not depend on a client (such as a cache
            // for client-side reflection data).
            CacheKey key;
            if (client == null)
                key = CacheKey.Create(cacheIdentifier);
            else
                key = CacheKey.Create(client, cacheIdentifier);

            ICache value = null;
            lock (cacheLock)
            {
                value = cache.GetValue(key, k => new Cache<TKey, TValue>(keyComparer));
            }

            var typedValue = value as Cache<TKey, TValue>;
            if (value != null && typedValue == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
                    "Unable to cast cache of type {0} as cache of type {1}",
                    value.GetType().FullName,
                    typeof(Cache<TKey, TValue>).FullName));
            }

            return typedValue;
        }

        // Composite cache key consisting of credentials, region, service url, cache type
        internal class CacheKey
        {
            public ImmutableCredentials ImmutableCredentials { get; private set; }
            public RegionEndpoint RegionEndpoint { get; private set; }
            public string ServiceUrl { get; private set; }
            public object CacheType { get; private set; }

            private CacheKey()
            {
                ImmutableCredentials = null;
                RegionEndpoint = null;
                ServiceUrl = null;
                CacheType = null;
            }

            public static CacheKey Create(AmazonServiceClient client, object cacheType)
            {
                if (client == null) throw new ArgumentNullException("client");

                var key = new CacheKey();

                var credentials = client.Credentials;
                key.ImmutableCredentials = credentials == null ?
                    null : credentials.GetCredentials();
                key.RegionEndpoint = client.Config.RegionEndpoint;
                key.ServiceUrl = client.Config.ServiceURL;
                key.CacheType = cacheType;

                return key;
            }
            public static CacheKey Create(object cacheType)
            {
                var key = new CacheKey();
                key.CacheType = cacheType;
                return key;
            }

            #region Public overrides

            public override int GetHashCode()
            {
                return Hashing.Hash(
                    ImmutableCredentials,
                    RegionEndpoint,
                    ServiceUrl,
                    CacheType);
            }
            public override bool Equals(object obj)
            {
                if (object.ReferenceEquals(this, obj))
                    return true;

                CacheKey ck = obj as CacheKey;
                if (ck == null)
                    return false;

                var allEqual = AWSSDKUtils.AreEqual(
                    new object[] { this.ImmutableCredentials, this.RegionEndpoint, this.ServiceUrl, this.CacheType },
                    new object[] { ck.ImmutableCredentials, ck.RegionEndpoint, ck.ServiceUrl, ck.CacheType });

                return allEqual;
            }

            #endregion
        }
    }

    // Implementation of generic ICache<TKey,TValue> interface
    internal class Cache<TKey, TValue> : ICache<TKey, TValue>
    {
        #region Private members

        private Dictionary<TKey, CacheItem<TValue>> Contents;
        private readonly object CacheLock = new object();

        #endregion

        #region Constructor

        public Cache(IEqualityComparer<TKey> keyComparer = null)
        {
            Contents = new Dictionary<TKey, CacheItem<TValue>>(keyComparer);

            MaximumItemLifespan = DefaultMaximumItemLifespan;
            CacheClearPeriod = DefaultCacheClearPeriod;
        }

        #endregion

        #region Public members

        public static TimeSpan DefaultMaximumItemLifespan = TimeSpan.FromHours(6);
        public static TimeSpan DefaultCacheClearPeriod = TimeSpan.FromHours(1);
        public DateTime LastCacheClean { get; private set; }

        #endregion

        #region ICache implementation

        public TValue GetValue(TKey key, Func<TKey, TValue> creator)
        {
            bool isStaleItem;
            return GetValueHelper(key, out isStaleItem, creator);
        }
        public TValue GetValue(TKey key, Func<TKey, TValue> creator, out bool isStaleItem)
        {
            return GetValueHelper(key, out isStaleItem, creator);
        }
        public void Clear(TKey key)
        {
            lock (CacheLock)
            {
                Contents.Remove(key);
            }
        }
        public void Clear()
        {
            lock (CacheLock)
            {
                Contents.Clear();
                LastCacheClean = GetCorrectedLocalTime();
            }
        }
        public List<TKey> Keys
        {
            get
            {
                lock (CacheLock)
                {
                    return Contents.Keys.ToList();
                }
            }
        }

        private TimeSpan maximumItemLifespan;
        public TimeSpan MaximumItemLifespan
        {
            get { return maximumItemLifespan; }
            set
            {
                if (value < TimeSpan.Zero)
                    throw new ArgumentOutOfRangeException("value");
                maximumItemLifespan = value;
            }
        }

        private TimeSpan cacheClearPeriod;
        public TimeSpan CacheClearPeriod
        {
            get { return cacheClearPeriod; }
            set
            {
                if (value < TimeSpan.Zero)
                    throw new ArgumentOutOfRangeException("value");
                cacheClearPeriod = value;
            }
        }

        public int ItemCount
        {
            get
            {
                lock (CacheLock)
                {
                    return Contents.Count;
                }
            }
        }

        public TOut UseCache<TOut>(TKey key, Func<TOut> operation,
            Action onError, Predicate<Exception> shouldRetryForException)
        {
            TOut output = default(TOut);

            try
            {
                output = operation();
            }
            catch(Exception e)
            {
                // if predicate is specified, check whether to retry on exception
                // otherwise, retry
                var shouldRetry = shouldRetryForException == null || shouldRetryForException(e);
                if (shouldRetry)
                {
                    // clear existing value
                    Clear(key);

                    // allow calling code to cleanup
                    if (onError != null)
                        onError();

                    // retry operation
                    output = operation();
                }
                else
                    throw;
            }

            return output;
        }

        #endregion

        #region Private methods and classes

        private TValue GetValueHelper(TKey key, out bool isStaleItem, Func<TKey, TValue> creator = null)
        {
            isStaleItem = true;
            CacheItem<TValue> item = null;
            if (AWSConfigs.UseSdkCache)
            {
                lock (CacheLock)
                {
                    if (!Contents.TryGetValue(key, out item) || !IsValidItem(item))
                    {
                        if (creator == null)
                            throw new InvalidOperationException("Unable to calculate value for key " + key);

                        var value = creator(key);
                        isStaleItem = false;
                        item = new CacheItem<TValue>(value);
                        Contents[key] = item;

                        RemoveOldItems_Locked();
                    }
                }
            }
            else
            {
                if (creator == null)
                    throw new InvalidOperationException("Unable to calculate value for key " + key);

                var value = creator(key);
                item = new CacheItem<TValue>(value);
                isStaleItem = false;
            }

            if (item == null)
                throw new InvalidOperationException("Unable to find value for key " + key);

            return item.Value;
        }

        private bool IsValidItem(CacheItem<TValue> item)
        {
            if (item == null)
                return false;
            var cutoff = GetCorrectedLocalTime() - this.MaximumItemLifespan;
            if (item.LastUseTime < cutoff)
                return false;

            return true;
        }
        private void RemoveOldItems_Locked()
        {
            if (LastCacheClean + CacheClearPeriod > AWSConfigs.utcNowSource().ToLocalTime())
                return;

            // Remove all items that were not accessed since the cutoff.
            // Using a cutoff is more optimal than item.Age, as we only need
            // to do DateTime calculation once, not for each item.
            var cutoff = GetCorrectedLocalTime() - MaximumItemLifespan;

            var keysToRemove = new List<TKey>();
            foreach (var kvp in Contents)
            {
                var key = kvp.Key;
                var item = kvp.Value;

                if (item == null || item.LastUseTime < cutoff)
                    keysToRemove.Add(key);
            }

            foreach (var key in keysToRemove)
                Contents.Remove(key);

            LastCacheClean = GetCorrectedLocalTime();
        }

        private class CacheItem<T>
        {
            private T _value;

            public T Value
            {
                get
                {
                    LastUseTime = GetCorrectedLocalTime();
                    return _value;
                }
                private set
                {
                    _value = value;
                }
            }
            public DateTime LastUseTime { get; private set; }

            public CacheItem(T value)
            {
                Value = value;
                LastUseTime = GetCorrectedLocalTime();
            }
        }

        private static DateTime GetCorrectedLocalTime()
        {
#pragma warning disable CS0612 // Type or member is obsolete
            return AWSSDKUtils.CorrectedUtcNow.ToLocalTime();
#pragma warning restore CS0612 // Type or member is obsolete
        }

        #endregion
    }
}