/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 */
package org.apache.synapse.transport.certificatevalidation.cache;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.transport.certificatevalidation.Constants;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Cache Manager takes care and maintains an LRU cache which implements ManageableCache Interface.
 * CAUTION!! If CacheManager is too much involved with the cache, other threads will be affected.
 */

public class CacheManager {

    private final boolean DO_NOT_INTERRUPT_IF_RUNNING = false;
    private ScheduledExecutorService scheduler;
    private ScheduledFuture scheduledFuture = null;
    private ManageableCache cache;
    private int cacheMaxSize;
    private int delay;
    private CacheManagingTask cacheManagingTask;
    private static final Log log = LogFactory.getLog(CacheManager.class);

    /**
     * A new cacheManager will be started on the given ManageableCache object.
     *
     * @param cache        a Manageable Cache which could be managed by this cache manager.
     * @param cacheMaxSize Maximum size of the cache. If the cache exceeds this size, LRU values will be
     *                     removed
     */
    public CacheManager(ManageableCache cache, int cacheMaxSize, int delay) {
        int NUM_THREADS = 1;
        scheduler = Executors.newScheduledThreadPool(NUM_THREADS);
        this.cache = cache;
        this.cacheMaxSize = cacheMaxSize;
        this.cacheManagingTask = new CacheManagingTask();
        this.delay = delay;
        start();
    }

    /**
     * To Start the CacheManager. Should be called only once per CacheManager so called in constructor.
     * CacheManager will run its TimerTask every "delay" number of seconds.
     */
    private boolean start() {
        if(scheduledFuture == null || (scheduledFuture.isCancelled())) {
            scheduledFuture = scheduler.scheduleWithFixedDelay(cacheManagingTask,
                    delay, delay, TimeUnit.MINUTES);
            log.info(cache.getClass().getSimpleName()+" Cache Manager Started");
            return true;
        }
        return false;
    }

    /**
     * To wake cacheManager up at will. If this method is called while its task is running, it will run its task again
     * soon after its done. CacheManagerTask will be rescheduled as before.
     * @return true if successfully waken up. false otherwise.
     */
    public boolean wakeUpNow(){
        if(scheduledFuture !=null) {
            if(!scheduledFuture.isCancelled()) {
                scheduledFuture.cancel(DO_NOT_INTERRUPT_IF_RUNNING);
            }
            scheduledFuture = scheduler.scheduleWithFixedDelay(cacheManagingTask,
                    0, delay,TimeUnit.MINUTES);
            log.info(cache.getClass().getSimpleName()+" Cache Manager Wakened Up.....");
            return true;
        }
        return false;
    }

    public boolean changeDelay(int delay) throws IllegalArgumentException {
        int min = Constants.CACHE_MIN_DELAY_MINS;
        int max = Constants.CACHE_MAX_DELAY_MINS;
        if(delay < min || delay > max) {
            throw new IllegalArgumentException("Delay time should should be between " + min + " and " +
                    max + " minutes");
        }
        this.delay = delay;
        return wakeUpNow();
    }

    public int getDelay(){
        return delay;
    }

    /**
     * Gracefully stop cacheManager.
     */
    public boolean stop(){
        if(scheduledFuture !=null && !scheduledFuture.isCancelled()){
            scheduledFuture.cancel(DO_NOT_INTERRUPT_IF_RUNNING);
            log.info(cache.getClass().getSimpleName()+" Cache Manager Stopped.....");
            return true;
        }
        return false;
    }

    public boolean isRunning() {
        return !scheduledFuture.isCancelled();
    }

    /**
     * This is the Scheduled Task the CacheManager uses in order to remove invalid cache values and
     * to remove LRU values if the cache reaches cacheMaxSize.
     */
    private class CacheManagingTask implements Runnable {

        public void run() {

            long start = System.currentTimeMillis();
            log.info(cache.getClass().getSimpleName()+" Cache Manager Task Started.");

            ManageableCacheValue nextCacheValue;
            //cache.getCacheSize() can vary when new entries are added. So get cache size at this point
            int cacheSize = cache.getCacheSize();
            int numberToRemove = (cacheSize>cacheMaxSize)?  cacheSize - cacheMaxSize: 0;

            List<ManageableCacheValue> entriesToRemove = new ArrayList<ManageableCacheValue>();
            LRUEntryCollector lruEntryCollector = new LRUEntryCollector(entriesToRemove, numberToRemove);

            //Start looking at cache entries from the beginning.
            cache.resetIterator();
            //Iteration through the cache entries.
            while ((cacheSize--)>0) {

                nextCacheValue = cache.getNextCacheValue();
                if (nextCacheValue == null) {
                    log.info("Cache manager iteration through Cache values done");
                    break;
                }

                //Updating invalid cache values
                if (!nextCacheValue.isValid()) {
                    log.info("Updating Invalid Cache Value by Manager");
                    nextCacheValue.updateCacheWithNewValue();
                }

                //There are LRU entries tobe removed since cacheSize > maxCacheSize. So collect them.
                if(numberToRemove>0) {
                    lruEntryCollector.collectEntriesToRemove(nextCacheValue);
                }
            }

            //LRU entries removing
            for(ManageableCacheValue oldCacheValue: entriesToRemove) {
                log.info("Removing LRU value from cache");
                oldCacheValue.removeThisCacheValue();
            }

            log.info(cache.getClass().getSimpleName()+" Cache Manager Task Done. Took " +
                    (System.currentTimeMillis() - start) + " ms.");
        }

        private class LRUEntryCollector {

            private List<ManageableCacheValue> entriesToRemove;
            private int listMaxSize;

            LRUEntryCollector(List<ManageableCacheValue> entriesToRemove, int numberToRemove){
                this.entriesToRemove = entriesToRemove;
                this.listMaxSize = numberToRemove;
            }

            /**
             * This method collects the listMaxSize number of LRU values from the Cache. These values
             * will be removed from the cache. This uses a part of the Logic in Insertion Sort.
             * @param value to be collected.
             */
            private void collectEntriesToRemove(ManageableCacheValue value) {

                entriesToRemove.add(value);
                int i = entriesToRemove.size()-1;
                int j = i;
                for(; j>0 && (value.getTimeStamp() < entriesToRemove.get(j-1).getTimeStamp()); j--) {
                    entriesToRemove.remove(j);
                    entriesToRemove.add(j,(entriesToRemove.get(j-1)));
                }
                entriesToRemove.remove(j);
                entriesToRemove.add(j,value);
                /**
                 * First entry in the list will be the oldest. Last is the earliest in the list.
                 * So remove the earliest since we need to collect the old (LRU) values to remove
                 * from cache later
                 */
                if(entriesToRemove.size() > listMaxSize) {
                    entriesToRemove.remove(entriesToRemove.size() -1);
                }
            }

        }
    }
}
