/*
 * Copyright (C) 2014 Indeed Inc.
 *
 * 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 com.indeed.imhotep.service;

import com.google.common.cache.CacheBuilder;
import com.indeed.util.core.io.Closeables2;
import com.indeed.util.core.reference.SharedReference;
import com.indeed.util.varexport.Export;
import com.indeed.imhotep.api.ImhotepSession;

import org.apache.log4j.Logger;

import java.util.HashMap;
import java.util.Map;

/**
 * @author jplaisance
 */
public abstract class AbstractSessionManager<E> implements SessionManager<E> {

    private static final Logger log = Logger.getLogger(AbstractSessionManager.class);

    private final Map<String, Session<E>> sessionMap = new HashMap<String, Session<E>>();
    private final Map<String, Exception> failureCauseMap = CacheBuilder.newBuilder().maximumSize(200).<String, Exception>build().asMap();

    protected void addSession(String sessionId, Session<E> session) {
        synchronized (sessionMap) {
            if (sessionMap.containsKey(sessionId)) {
                throw new IllegalArgumentException("there already exists a session with id "+sessionId);
            }
            sessionMap.put(sessionId, session);
        }
    }

    @Override
    public SharedReference<ImhotepSession> getSession(final String sessionId) {
        final Session<E> session = internalGetSession(sessionId);
        return session.imhotepSession.copy();
    }

    @Override
    public boolean sessionIsValid(final String sessionId) {
        synchronized (sessionMap) {
            return sessionMap.containsKey(sessionId);
        }
    }

    @Override
    public void removeAndCloseIfExists(final String sessionId) {
        final SharedReference<ImhotepSession> imhotepSession;
        synchronized (sessionMap) {
            final Session<E> session = sessionMap.remove(sessionId);
            if (session == null) {
                return;
            }
            imhotepSession = session.imhotepSession;
        }
        Closeables2.closeQuietly(imhotepSession, log);
    }

    @Override
    public void removeAndCloseIfExists(final String sessionId, Exception e) {
        final SharedReference<ImhotepSession> imhotepSession;
        synchronized (sessionMap) {
            final Session<E> session = sessionMap.remove(sessionId);
            if (session == null) {
                return;
            }
            imhotepSession = session.imhotepSession;
        }
        failureCauseMap.put(sessionId, e);
        Closeables2.closeQuietly(imhotepSession, log);
    }

    @Override
    public void setNumStats(final String sessionId, final int newNumStats) {
        final Session<E> session = internalGetSession(sessionId);
        session.numStats = newNumStats;
    }

    @Override
    public int getNumStats(final String sessionId) {
        final Session<E> session = internalGetSession(sessionId);
        return session.numStats;
    }

    @Override
    public Map<String, Long> getLastActionTimes() {
        final Map<String, Session<E>> sessionMap = cloneSessionMap();

        final Map<String, Long> ret = new HashMap<String, Long>();
        for (final Map.Entry<String, Session<E>> e : sessionMap.entrySet()) {
            ret.put(e.getKey(), e.getValue().lastActionTime);
        }
        return ret;
    }

    protected Map<String, Session<E>> cloneSessionMap() {
        final Map<String, Session<E>> clone;
        synchronized (sessionMap) {
            clone = new HashMap<String, Session<E>>(sessionMap);
        }
        return clone;
    }

    protected Session<E> internalGetSession(final String sessionId) {
        final Session<E> session;
        synchronized (sessionMap) {
            checkSession(sessionId);
            session = sessionMap.get(sessionId);
        }
        session.lastActionTime = System.currentTimeMillis();
        return session;
    }

    // do not call this method if you do not hold sessionMap's monitor
    private void checkSession(final String sessionId) {
        if (!sessionMap.containsKey(sessionId)) {
            final Exception e = failureCauseMap.get(sessionId);
            throw new IllegalArgumentException("there does not exist a session with id " + sessionId, e);
        }
    }

    protected static final class Session<E> {
        protected final SharedReference<ImhotepSession> imhotepSession;
        protected final E sessionState;
        protected final String username;
        protected final String ipAddress;
        protected final int clientVersion;
        protected final String dataset;

        private volatile int numStats;
        private volatile long lastActionTime;

        protected Session(
                ImhotepSession imhotepSession,
                E sessionState,
                String username,
                String ipAddress,
                int clientVersion,
                String dataset
        ) {
            this.imhotepSession = SharedReference.create(imhotepSession);
            this.sessionState = sessionState;
            this.username = username;
            this.ipAddress = ipAddress;
            this.clientVersion = clientVersion;
            this.dataset = dataset;

            lastActionTime = System.currentTimeMillis();
        }
    }

    @Export(name = "session-count", doc = "# of open sessions")
    public int getSessionCount() {
        synchronized (sessionMap) {
            return sessionMap.size();
        }
    }
}
