package com.eactive.eai.adapter.websocket;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.mina.util.ExpirationListener;
import org.java_websocket.WebSocket;

import com.eactive.eai.common.util.DatetimeUtil;
import com.eactive.eai.common.util.Logger;

public class WebSocketExpiringMap<K, V> implements Map<K, V> {

	private static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

	/** The Constant DEFAULT_TIME_TO_LIVE. */
	public static final int DEFAULT_TIME_TO_LIVE = 60;

	/** The Constant DEFAULT_EXPIRATION_INTERVAL. */
	public static final int DEFAULT_EXPIRATION_INTERVAL = 1;

	/** The expirer count. */
	private static volatile int expirerCount = 1;

	/** The delegate. */
	private final ConcurrentHashMap<K, ExpiringObject> delegate;

	/** The expiration listeners. */
	private final CopyOnWriteArrayList<ExpirationListener> expirationListeners;

	/** The expirer. */
	private final Expirer expirer;

	/**
	 * Instantiates a new expiring map.
	 */
	public WebSocketExpiringMap() {
		this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
	}

	/**
	 * Instantiates a new expiring map.
	 * 
	 * @param timeToLive the time to live
	 */
	public WebSocketExpiringMap(int timeToLive) {
		this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
	}

	/**
	 * Instantiates a new expiring map.
	 * 
	 * @param timeToLive         the time to live
	 * @param expirationInterval the expiration interval
	 */
	public WebSocketExpiringMap(int timeToLive, int expirationInterval) {
		this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener>(), timeToLive,
				expirationInterval);
	}

	/**
	 * Instantiates a new expiring map.
	 * 
	 * @param delegate            the delegate
	 * @param expirationListeners the expiration listeners
	 * @param timeToLive          the time to live
	 * @param expirationInterval  the expiration interval
	 */
	private WebSocketExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
			CopyOnWriteArrayList<ExpirationListener> expirationListeners, int timeToLive, int expirationInterval) {
		this.delegate = delegate;
		this.expirationListeners = expirationListeners;

		this.expirer = new Expirer();
		expirer.setExpirationInterval(expirationInterval);
		expirer.setTimeToLive(timeToLive);
		expirer.startExpiring();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
	 */
	public V put(K key, V value) {
		ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
		if (answer == null) {
			return null;
		}

		return answer.getValue();
	}

	/**
	 * Put.
	 * 
	 * @param key        the key
	 * @param value      the value
	 * @param timeToLive the time to live
	 * 
	 * @return the v
	 */
	public V put(K key, V value, int timeToLive) {
		ExpiringObject answer = delegate.put(key,
				new ExpiringObject(key, value, System.currentTimeMillis(), timeToLive));
		if (answer == null) {
			return null;
		}

		return answer.getValue();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#get(java.lang.Object)
	 */
	public V get(Object key) {
		ExpiringObject object = delegate.get(key);
		if (object != null) {
			return object.getValue();
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#remove(java.lang.Object)
	 */
	public V remove(Object key) {
		ExpiringObject answer = delegate.remove(key);
		if (answer == null) {
			return null;
		}

		return answer.getValue();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#containsKey(java.lang.Object)
	 */
	public boolean containsKey(Object key) {
		return delegate.containsKey(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#containsValue(java.lang.Object)
	 */
	public boolean containsValue(Object value) {
		return delegate.containsValue(value);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#size()
	 */
	public int size() {
		return delegate.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#isEmpty()
	 */
	public boolean isEmpty() {
		return delegate.isEmpty();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#clear()
	 */
	public void clear() {
		delegate.clear();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return delegate.hashCode();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#keySet()
	 */
	public Set<K> keySet() {
		return delegate.keySet();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		return delegate.equals(obj);
	}

    /* (non-Javadoc)
     * @see java.util.Map#putAll(java.util.Map)
     */
    public void putAll(Map<? extends K, ? extends V> inMap) {
        for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
        	if(e != null){ //취약점 소스 수정
        		this.put(e.getKey(), e.getValue());
        	}
        }
    }

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.Map#values()
	 */
	public Collection<V> values() {
		throw new UnsupportedOperationException();
	}

    /* (non-Javadoc)
     * @see java.util.Map#entrySet()
     */
    public Set<Map.Entry<K, V>> entrySet() {
        throw new UnsupportedOperationException();
    }

	/**
	 * Adds the expiration listener.
	 * 
	 * @param listener the listener
	 */
//    public void addExpirationListener(ExpirationListener<? extends V> listener) {
//        expirationListeners.add(listener);
//    }
//
//    /**
//     * Removes the expiration listener.
//     * 
//     * @param listener the listener
//     */
//    public void removeExpirationListener(
//            ExpirationListener<? extends V> listener) {
//        expirationListeners.remove(listener);
//    }

	/**
	 * Gets the expirer.
	 * 
	 * @return the expirer
	 */
	public Expirer getExpirer() {
		return expirer;
	}

	/**
	 * Gets the expiration interval.
	 * 
	 * @return the expiration interval
	 */
	public int getExpirationInterval() {
		return expirer.getExpirationInterval();
	}

	/**
	 * Gets the time to live.
	 * 
	 * @return the time to live
	 */
	public int getTimeToLive() {
		return expirer.getTimeToLive();
	}

	/**
	 * Sets the expiration interval.
	 * 
	 * @param expirationInterval the new expiration interval
	 */
	public void setExpirationInterval(int expirationInterval) {
		expirer.setExpirationInterval(expirationInterval);
	}

	/**
	 * Sets the time to live.
	 * 
	 * @param timeToLive the new time to live
	 */
	public void setTimeToLive(int timeToLive) {
		expirer.setTimeToLive(timeToLive);
	}

	/**
	 * The Class ExpiringObject.
	 * <ul>
	 * <li>1. Function :</li>
	 * <li>2. Summary :</li>
	 * <li>3. Notification :</li>
	 * </ul>
	 * 
	 * @author : $Author:
	 * @version : $Revision: 1.4 $
	 */
	private class ExpiringObject {

		/** The key. */
		private K key;

		/** The value. */
		private V value;

		/** The access time. */
		private long accessTime;

		/** The time to live millis. */
		private long timeToLiveMillis;

		/**
		 * Instantiates a new expiring object.
		 * 
		 * @param key        the key
		 * @param value      the value
		 * @param accessTime the access time
		 */
		ExpiringObject(K key, V value, long accessTime) {
			this(key, value, accessTime, 0);
		}

		/**
		 * Instantiates a new expiring object.
		 * 
		 * @param key              the key
		 * @param value            the value
		 * @param accessTime       the access time
		 * @param timeToLiveMillis the time to live millis
		 */
		ExpiringObject(K key, V value, long accessTime, int timeToLiveMillis) {

			if (value == null) {
				throw new IllegalArgumentException("An expiring object cannot be null.");
			}

			this.key = key;
			this.value = value;
			this.accessTime = accessTime;
			this.timeToLiveMillis = timeToLiveMillis * 1000;
		}

		/**
		 * Gets the time to live millis.
		 * 
		 * @return the time to live millis
		 */
		public long getTimeToLiveMillis() {
			return timeToLiveMillis;
		}

		/**
		 * Sets the time to live millis.
		 * 
		 * @param timeToLiveMillis the new time to live millis
		 */
		public void setTimeToLiveMillis(long timeToLiveMillis) {
			this.timeToLiveMillis = timeToLiveMillis;
		}

		/**
		 * Gets the access time.
		 * 
		 * @return the access time
		 */
		public long getAccessTime() {
			return accessTime;
		}

		/**
		 * Sets the access time.
		 * 
		 * @param lastAccessTime the new access time
		 */
		public void setAccessTime(long lastAccessTime) {
			this.accessTime = lastAccessTime;
		}

		/**
		 * Gets the key.
		 * 
		 * @return the key
		 */
		public K getKey() {
			return key;
		}

		/**
		 * Gets the value.
		 * 
		 * @return the value
		 */
		public V getValue() {
			return value;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			return value.equals(obj);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return value.hashCode();
		}
	}

	/**
	 * The Class Expirer.
	 * <ul>
	 * <li>1. Function :</li>
	 * <li>2. Summary :</li>
	 * <li>3. Notification :</li>
	 * </ul>
	 * 
	 * @author : $Author:
	 * @version : $Revision: 1.4 $
	 */
	public class Expirer implements Runnable {

		/** The state lock. */
		private ReadWriteLock stateLock = new ReentrantReadWriteLock();

		/** The time to live millis. */
		private long timeToLiveMillis;

		/** The expiration interval millis. */
		private long expirationIntervalMillis;

		/** The running. */
		private boolean running = false;

		/** The expirer thread. */
		private final Thread expirerThread;

		/**
		 * Instantiates a new expirer.
		 */
		public Expirer() {
			expirerThread = new Thread(this, "ExpiringMapExpirer-" + (expirerCount++));
			expirerThread.setDaemon(true);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		public void run() {
			while (running) {
				processExpires();

				try {
					Thread.sleep(expirationIntervalMillis);
				} catch (InterruptedException e) {
					logger.warn("sleep error", e);
				}
			}
		}

        /**
         * Process expires.
         * expire 조건 : timeNow >= expireTime
         * expireTime = accessTime + timeToLive
         */
        private void processExpires() {
            long timeNow = System.currentTimeMillis();
//            logger.debug("WebSocketExpiringMap] processExpires START");

			long timeToLiveMillis = 0;

			for (ExpiringObject o : delegate.values()) {

				if (o.getTimeToLiveMillis() > 0) {
					timeToLiveMillis = o.getTimeToLiveMillis();
				} else {
					timeToLiveMillis = this.timeToLiveMillis;
				}

				if (timeToLiveMillis <= 0)
					continue;

				long expireTime = o.getAccessTime() + timeToLiveMillis;

				if (timeNow >= expireTime) {
					ExpiringObject object = delegate.remove(o.getKey());
					if (object != null) {
						Object co = object.getValue();
						if (co instanceof WebSocket) {
							logger.warn("WebSocketExpiringMap] processExpires Close WebSocket - "
									+ ((WebSocket) co).getRemoteSocketAddress());
							((WebSocket) co).close();
						}
					}
				}
			}
		}

		/**
		 * Start expiring.
		 */
		public void startExpiring() {
			stateLock.writeLock().lock();

			try {
				if (!running) {
					running = true;
					expirerThread.start();
				}
			} finally {
				stateLock.writeLock().unlock();
			}
		}

		/**
		 * Start expiring if not started.
		 */
		public void startExpiringIfNotStarted() {
			stateLock.readLock().lock();
			try {
				if (running) {
					return;
				}
			} finally {
				stateLock.readLock().unlock();
			}

			stateLock.writeLock().lock();
			try {
				if (!running) {
					running = true;
					expirerThread.start();
				}
			} finally {
				stateLock.writeLock().unlock();
			}
		}

		/**
		 * Stop expiring.
		 */
		public void stopExpiring() {
			stateLock.writeLock().lock();

			try {
				if (running) {
					running = false;
					expirerThread.interrupt();
				}
			} finally {
				stateLock.writeLock().unlock();
			}
		}

		/**
		 * Checks if is running.
		 * 
		 * @return true, if is running
		 */
		public boolean isRunning() {
			stateLock.readLock().lock();

			try {
				return running;
			} finally {
				stateLock.readLock().unlock();
			}
		}

		/**
		 * Gets the time to live.
		 * 
		 * @return the time to live
		 */
		public int getTimeToLive() {
			stateLock.readLock().lock();

			try {
				return (int) timeToLiveMillis / 1000;
			} finally {
				stateLock.readLock().unlock();
			}
		}

		/**
		 * Sets the time to live.
		 * 
		 * @param timeToLive the new time to live
		 */
		public void setTimeToLive(long timeToLive) {
			stateLock.writeLock().lock();

			try {
				this.timeToLiveMillis = timeToLive * 1000;
			} finally {
				stateLock.writeLock().unlock();
			}
		}

		/**
		 * Gets the expiration interval.
		 * 
		 * @return the expiration interval
		 */
		public int getExpirationInterval() {
			stateLock.readLock().lock();

			try {
				return (int) expirationIntervalMillis / 1000;
			} finally {
				stateLock.readLock().unlock();
			}
		}

		/**
		 * Sets the expiration interval.
		 * 
		 * @param expirationInterval the new expiration interval
		 */
		public void setExpirationInterval(long expirationInterval) {
			stateLock.writeLock().lock();

			try {
				this.expirationIntervalMillis = expirationInterval * 1000;
			} finally {
				stateLock.writeLock().unlock();
			}
		}
	}
}
