/*
* Copyright 2005,2006 WSO2, Inc. http://wso2.com
*
* 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.
*
*
*/

package org.apache.synapse.commons.throttle.core;

import org.apache.axis2.context.ConfigurationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.commons.throttle.core.factory.ThrottleContextFactory;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * Holds the all runtime data corresponding to call remote callers.
 * In addition to that this hold clean list for callers.
 */

public abstract class ThrottleContext {

    private static Log log = LogFactory.getLog(ThrottleContext.class.getName());

    /* The callersMap that contains all registered callers for a particular throttle */
    private Map callersMap;
    /* For mapping id (ip | domainame) to TimeStamp */
    private Map keyToTimeStampMap;
    /* The Time which next cleaning for this throttle will have to take place */
    private long nextCleanTime;
    /* The configuration of a throttle */
    private ThrottleConfiguration throttleConfiguration;
    /* The configuration that corresponding to this context – this holds all
     static (configuration) data */
    private String throttleId;
    /* Configuration Context for the current environment */
    private ConfigurationContext configctx;
    /* ThrottleDataHolder is used to keep the CallerContexts pertaining to a Throttle Context.*/
    private volatile ThrottleDataHolder dataHolder;
    /* The pre-fix of key for any caller */
    private String keyPrefix;
    /*is log level has set to debug */
    private boolean debugOn;

    /* Throttle replicating to replicate Throttle data */
    private ThrottleReplicator throttleReplicator;

    private ThrottleWindowReplicator throttleWindowReplicator;

    /**
     * default constructor – expects a throttle configuration.
     *
     * @param throttleConfiguration - configuration data according to the policy
     */
    public ThrottleContext(ThrottleConfiguration throttleConfiguration,
                           ThrottleReplicator throttleReplicator) {
        if (throttleConfiguration == null) {
            throw new InstantiationError("Couldn't create the throttle context " +
                    "from null a throttle configuration");
        }
        this.throttleReplicator = throttleReplicator;
        this.keyToTimeStampMap = new ConcurrentHashMap();
        this.callersMap = new ConcurrentSkipListMap();
        this.nextCleanTime = 0;
        this.throttleConfiguration = throttleConfiguration;
        this.debugOn = log.isDebugEnabled();
        this.throttleWindowReplicator = ThrottleContextFactory.getThrottleWindowReplicatorInstance();
        ThrottleContextFactory.getThrottleContextCleanupTaskInstance().addThrottleContext(this);
    }

    /**
     * To get the ThrottleConfiguration
     *
     * @return ThrottleConfiguration returns the ThrottleConfiguration of this context
     */
    public ThrottleConfiguration getThrottleConfiguration() {
        return throttleConfiguration;
    }

    /**
     * To get the runtime states of a remote caller
     *
     * @param id the remote caller id ex: domain , ip
     * @return Returns the CallerContext which holds runtime state of a remote caller
     */
    public CallerContext getCallerContext(String id) {

        if (id != null) {

            if (debugOn) {
                log.debug("Found a configuration with id :" + id);
            }
            // for cluster env , caller state is contained in the axis configuration context
            if (dataHolder != null && keyPrefix != null) {
                return dataHolder.getCallerContext(id);
            }
            // for non - clustered  env
            Long timeKey = (Long) keyToTimeStampMap.get(id);
            if (timeKey != null) {
                Object co = callersMap.get(timeKey);
                if (co != null) {
                    if (co instanceof CallerContext) {
                        return (CallerContext) co;
                    } else if (co instanceof LinkedList) {    // callers with same time window
                        LinkedList callers = (LinkedList) co;
                        synchronized (callers) {
                            for (Iterator it = callers.iterator(); it.hasNext(); ) {
                                CallerContext cc = (CallerContext) it.next();
                                if (cc != null && id.equals(cc.getId())) {
                                    return cc;
                                }
                            }
                        }
                    }
                }
            }
        } else {
            if (debugOn) {
                log.debug("Couldn't find a configuration for the remote caller : " + id);
            }
        }
        return null;
    }

    /**
     * setting callerContext - put callersMap against  time and
     * put time against remote caller id (ip/domain)
     *
     * @param callerContext - The remote caller's runtime data.
     * @param id            - The id of the remote caller
     */
    public void addCallerContext(CallerContext callerContext, String id) {
        if (callerContext != null && id != null) {
            addCaller(callerContext, id);
        }
    }

    /**
     * Helper method to add a caller context
     *
     * @param callerContext The CallerContext
     * @param id            The id of the remote caller
     */
    private void addCaller(CallerContext callerContext, String id) {

        if (debugOn) {
            log.debug("Setting the caller with an id " + id);
        }
        //if this is a cluster env.,put the context into axis configuration context
        if (dataHolder != null && keyPrefix != null) {
            dataHolder.addCallerContext(id, callerContext);
        }
        // for clean up list
        Long time = new Long(callerContext.getNextTimeWindow());
        if (!callersMap.containsKey(time)) {
            callersMap.put(time, callerContext);
        } else {
            //if there are callersMap with same timewindow ,then use linkedList to hold those
            Object callerObject = callersMap.get(time);
            if (callerObject != null) {
                if (callerObject instanceof CallerContext) {
                    LinkedList callersWithSameTimeStamp = new LinkedList();
                    callersWithSameTimeStamp.add(callerObject);
                    callersWithSameTimeStamp.add(callerContext);
                    callersMap.remove(time);
                    callersMap.put(time, callersWithSameTimeStamp);
                } else if (callerObject instanceof LinkedList) {
                    LinkedList callersWithSameTimeStamp = (LinkedList) callerObject;
                    synchronized (callersWithSameTimeStamp) {
                        callersWithSameTimeStamp.add(callerContext);
                    }
                }
            }
        }
        //set Time Vs key
        keyToTimeStampMap.put(id, time);
    }

    /**
     * removing a caller with a given id - caller will remove from clean list
     *
     * @param id Caller ID
     */
    public void removeCallerContext(String id) {
        if (id != null) {
            if(log.isDebugEnabled()) {
                log.debug("REMOVING CALLER CONTEXT WITH ID" + id);
            }
            removeCaller(id);
        }
    }

    /**
     * Helper method to remove a caller
     *
     * @param id The id of the caller
     */
    private void removeCaller(String id) {
        Long time = (Long) keyToTimeStampMap.get(id);
        // if (time != null) {
        if (dataHolder != null && keyPrefix != null) {
            log.debug("Removing the caller with the configuration id " + id);
            dataHolder.removeCaller(id);
        }
        if (time != null) {
            callersMap.remove(time);
            keyToTimeStampMap.remove(id);
        }
    }

    /**
     * /**
     * processing cleaning list- only process callerContexts which unit time already had over
     *
     * @param time - the current System Time
     * @throws ThrottleException
     */

    public void processCleanList(long time) {
        if (debugOn) {
            log.debug("Cleaning up process is executing");
        }
        if (time > nextCleanTime) {
            SortedMap map = ((ConcurrentNavigableMap) callersMap).headMap(new Long(time));
            if (map != null && map.size() > 0) {
                for (Iterator it = map.values().iterator(); it.hasNext(); ) {
                    Object o = it.next();
                    if (o != null) {
                        if (o instanceof CallerContext) { // In the case nextAccessTime is unique
                            CallerContext c = ((CallerContext) o);
                            String key = c.getId();
                            String role = c.getRoleId();
                            if (key != null) {
                                if (dataHolder != null && keyPrefix != null) {
                                    c = dataHolder.getCallerContext(key);
                                }
                                if (c != null) {
                                    c.cleanUpCallers(
                                            this.throttleConfiguration.getCallerConfiguration(role)
                                            , this
                                            , time);
                                }
                            }
                        }
                        if (o instanceof LinkedList) { //In the case nextAccessTime of callers are same
                            LinkedList callers = (LinkedList) o;
                            synchronized (callers) {
                                for (Iterator ite = callers.iterator(); ite.hasNext(); ) {
                                    CallerContext c = (CallerContext) ite.next();
                                    String key = c.getId();
                                    String role = c.getRoleId();
                                    if (key != null) {
                                        if (dataHolder != null && keyPrefix != null) {
                                            c = (CallerContext) dataHolder.getCallerContext(key);
                                        }
                                        if (c != null) {
                                            c.cleanUpCallers(
                                                    this.throttleConfiguration.getCallerConfiguration(role)
                                                    , this
                                                    , time);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            nextCleanTime = time + ThrottleConstants.DEFAULT_THROTTLE_CLEAN_PERIOD;
        }
    }

    public void setThrottleId(String throttleId) {
        if (throttleId == null) {
            throw new IllegalArgumentException("The throttle id cannot be null");
        }
        this.throttleId = throttleId;
        this.keyPrefix = ThrottleConstants.THROTTLE_PROPERTY_PREFIX + throttleId;
    }

    public String getThrottleId() {
        return this.throttleId;
    }

    public ConfigurationContext getConfigurationContext() {
        return this.configctx;
    }

    public void setConfigurationContext(ConfigurationContext configurationContext) {
        this.configctx = configurationContext;
        if (dataHolder == null) {
            initDataHolder();
        }
    }

    private void initDataHolder() {
        if (configctx != null) {
            if (dataHolder == null) {
                dataHolder = (ThrottleDataHolder) configctx.getPropertyNonReplicable(ThrottleConstants.THROTTLE_INFO_KEY);
                synchronized (configctx) {
                    if (dataHolder == null) {
                        dataHolder = new ThrottleDataHolder();
                        configctx.setNonReplicableProperty(ThrottleConstants.THROTTLE_INFO_KEY, dataHolder);
                    }
                }
            }
        }
    }

    /**
     * @return Returns the type of throttle ex : ip /domain
     */
    public abstract int getType();

    /**
     * To add the caller and replicates the states of the given caller
     *
     * @param callerContext The states of the caller
     * @param id            The id of the caller
     */
    public void addAndFlushCallerContext(CallerContext callerContext, String id) {
        if (callerContext != null && id != null) {
            addCaller(callerContext, id);
            replicateCaller(id);
        }
    }

    /**
     * To replicates the states of the already exist caller
     *
     * @param callerContext The states of the caller
     * @param id            The id of the remote caller
     */
    public void flushCallerContext(CallerContext callerContext, String id) {
        if (dataHolder != null && callerContext != null && id != null) {
            dataHolder.addCallerContext(id, callerContext); // have to do, because we always get
            //  any property as non-replicable
            replicateCaller(id);
        }
    }

    /**
     * Removes the caller and replicate the states
     *
     * @param id The Id of the caller
     */
    public void removeAndFlushCaller(String id) {
        if (id != null) {
            if(log.isDebugEnabled()) {
                log.debug("REMOVING AND FLUSHING CALLER CONTEXT WITH ID " + id);
            }
            removeCaller(id);
            replicateCaller(id);
        }
    }

    /**
     * Removes the caller and destroy shared params of caller
     *
     * @param id The Id of the caller
     */
    public void removeAndDestroyShareParamsOfCaller(String id) {
        if (id != null) {
            if(log.isDebugEnabled()) {
                log.info("REMOVE AND DESTROY OF SHARED PARAM OF CALLER WITH ID " + id);
            }
            removeCaller(id);
            SharedParamManager.removeTimestamp(id);
            SharedParamManager.removeCounter(id);
        }
    }

    /**
     * Helper method to replicates states of the caller with given key
     *
     * @param id The id of the caller
     */
    private void replicateCaller(String id) {

        if (configctx != null && keyPrefix != null) {
            try {
                if (debugOn) {
                    log.debug("Going to replicate the states of the caller : " + id);
                }

                throttleReplicator.setConfigContext(configctx);
                throttleReplicator.add(id);

            } catch (Exception clusteringFault) {
                log.error("Error during the replicating states ", clusteringFault);
            }
        }
    }

    /**
     * Replicate the time window of this caller
     * @param id
     */
    public void replicateTimeWindow(String id) {
        if (configctx != null && keyPrefix != null) {
            try {
                if (debugOn) {
                    log.debug("Going to replicate the time window states of the caller : " + id);
                }

                throttleWindowReplicator.setConfigContext(configctx);
                throttleWindowReplicator.add(id);

            } catch (Exception e) {
                log.error("Error during the replicating window change ", e);
            }
        }
    }

    /**
     * This method will clean up callers which has next access time below from provided time
     * This will first check the prohibited period and then it will check next access time lesser than unit time before a
     * cleanup  a caller
     *
     * @param time to clean up the caller contexts
     */
    public void cleanupCallers(long time) {

        SortedMap map = ((ConcurrentNavigableMap) callersMap).headMap(new Long(time));
        if (log.isDebugEnabled()) {
            log.debug("CallerMap Size before cleanup process : " + map.size());
        }
        if (map != null && map.size() > 0) {
            for (Iterator it = map.values().iterator(); it.hasNext(); ) {
                Object o = it.next();
                if (o != null) {
                    if (o instanceof CallerContext) { // In the case nextAccessTime is unique
                        CallerContext c = ((CallerContext) o);
                        String key = c.getId();
                        String role = c.getRoleId();
                        if (key != null) {
                            if (dataHolder != null && keyPrefix != null) {
                                c = dataHolder.getCallerContext(key);
                            }
                            if (c != null) {
                                c.cleanUpCallers(
                                        this.throttleConfiguration.getCallerConfiguration(role), this, time);
                            }
                        }
                    }
                    if (o instanceof LinkedList) { //In the case nextAccessTime of callers are same
                        LinkedList callers = (LinkedList) o;
                        Iterator ite = callers.iterator();
                        while (ite.hasNext()) {
                            CallerContext c = (CallerContext) ite.next();
                            String key = c.getId();
                            String role = c.getRoleId();
                            if (key != null) {
                                if (dataHolder != null && keyPrefix != null) {
                                    c = (CallerContext) dataHolder.getCallerContext(key);
                                }
                                if (c != null) {
                                    c.cleanUpCallers(
                                            this.throttleConfiguration.getCallerConfiguration(role), this, time);
                                }
                            }
                        }
                    }
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("CallerMap Size after cleanup process : " + map.size());
        }
    }
}
