/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * Application Versioning System
 * Copyright (C) 2008 France Telecom R&D
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.tomcat6.versioning;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.Cookie;

import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.versioning.VersioningServiceBase;

/**
 * Virtual Context implementation that handles operations with the additional
 * contexts and defines a new context called "private".
 * @author Frederic Germaneau
 * @author S. Ali Tokmen
 */
public class VirtualContext extends AbsVirtualContext {
    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(VirtualContext.class);

    /**
     * Reserved contexts.
     */
    private List<Context> reservedContexts = new ArrayList<Context>();

    /**
     * Disabled contexts.
     */
    private List<Context> disabledContexts = new ArrayList<Context>();

    /**
     * Private contexts.
     */
    private List<Context> privateContexts = new ArrayList<Context>();

    /**
     * Creates and registers a virtual context.
     * @param jmxService JMX server.
     * @param userURI User (virtual) URI of this context.
     * @param contextInstance The first context instance, will be mapped as
     *        default context.
     */
    public VirtualContext(final JmxService jmxService, final String userURI, final Context contextInstance) {
        super(jmxService, userURI, contextInstance);
    }

    /**
     * Adds a new context. Called by the parent if the policy is not default.
     * @param contextInstance Context instance (versioned path).
     * @param policy Policy (extensible).
     * @throws IllegalArgumentException If policy not recognized.
     */
    @Override
    protected void addContextInternal(final Context contextInstance, final String policy) throws IllegalArgumentException {
        if (VersioningServiceBase.DISABLED.equals(policy)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Addind disabled context " + contextInstance.getPath() + " for userURI " + userURI);
            }
            this.disabledContexts.add(contextInstance);
        } else if (VersioningServiceBase.PRIVATE.equals(policy)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Addind private context " + contextInstance.getPath() + " for userURI " + userURI);
            }
            this.privateContexts.add(contextInstance);
        } else if (VersioningServiceBase.RESERVED.equals(policy)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Addind reserved context " + contextInstance.getPath() + " for userURI " + userURI);
            }
            this.reservedContexts.add(contextInstance);
        } else {
            throw new IllegalArgumentException("Invalid policy: " + policy);
        }
    }

    /**
     * @return All versioned contexts with their POLICIES for this virtual
     *         context.
     */
    public Map<String, String> getContexts() {
        Map<String, String> result;
        int mapSize = disabledContexts.size() + reservedContexts.size() + privateContexts.size();
        if (defaultContext != null) {
            result = new HashMap<String, String>(1 + mapSize);
            result.put(defaultContext.getPath(), VersioningServiceBase.DEFAULT);
        } else {
            result = new HashMap<String, String>(mapSize);
        }
        for (Context context : disabledContexts) {
            result.put(context.getPath(), VersioningServiceBase.DISABLED);
        }
        for (Context context : reservedContexts) {
            result.put(context.getPath(), VersioningServiceBase.RESERVED);
        }
        for (Context context : privateContexts) {
            result.put(context.getPath(), VersioningServiceBase.PRIVATE);
        }
        return result;
    }

    /**
     * Checks if a given request has a private context.
     * @param req Request to check.
     * @return The private context for this request, null if none found.
     */
    protected String findPrivateContext(final Request req) {
        // TODO: this should return some non-null values someday
        return null;
    }

    /**
     * Checks if a given session has a disabled context.
     * @param req Request to check.
     * @return The disabled context for this request, null if none found.
     */
    protected String findDisabledContext(final Request req) {
        List<String> sessions = new ArrayList<String>();

        // Build list of sessions
        String requestSessionid = req.getRequestedSessionId();
        if (requestSessionid != null) {
            sessions.add(requestSessionid);
        }
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (Globals.SESSION_COOKIE_NAME.equals(cookie.getName())) {
                    sessions.add(cookie.getValue().toString());
                }
            }
        }
        if (sessions.size() == 0) {
            logger.debug("No session found");
            return null;
        }

        // Check for each session in each disabled context
        for (Context context : disabledContexts) {
            for (String sessionid : sessions) {
                try {
                    Session session = context.getManager().findSession(sessionid);
                    if (session != null && session.isValid()) {
                        logger.debug("Found session " + session + " for context " + context.getPath());
                        return context.getPath();
                    }
                } catch (IOException e) {
                    logger.error("Cannot read session", e);
                }
            }
        }

        logger.debug("None of the user's sessions belong to the disabled contexts");
        return null;
    }

    /**
     * Rebinds a context.
     * @param versionedPath Versioned path of the context.
     * @param policy New policy.
     * @return true if succeeded, false otherwise.
     */
    public boolean rebindContext(final String versionedPath, final String policy) {
        Context contextInstance;
        if (defaultContext != null && versionedPath.equals(defaultContext.getPath())) {
            contextInstance = defaultContext;
            defaultContext = null;
        } else {
            contextInstance = findDeleteContextObject(versionedPath, true);
        }

        if (contextInstance == null) {
            return false;
        } else {
            addContext(contextInstance, policy);
            return true;
        }
    }

    /**
     * Finds the context object corresponding to a path. This implementation
     * calls parent and check additional contexts.
     * @param versionedPath Path to check against.
     * @return Context object corresponding to versionedPath, null if none.
     */
    @Override
    protected Context findContextObject(final String versionedPath) {
        Context result = super.findContextObject(versionedPath);
        if (result == null) {
            result = findDeleteContextObject(versionedPath, false);
        }
        return result;
    }

    /**
     * Finds and optionally deletes a context object.
     * @param versionedPath Path to check against.
     * @param delete Whether to delete when found.
     * @return Context corresponding to versionedPath, null if none found.
     */
    protected Context findDeleteContextObject(final String versionedPath, final boolean delete) {
        // disabledContexts is null when parent
        // calls findContextObject at construction
        if (disabledContexts != null) {
            for (Context context : disabledContexts) {
                if (versionedPath.equals(context.getPath())) {
                    if (delete) {
                        disabledContexts.remove(context);
                    }
                    return context;
                }
            }
        }
        // reservedContexts is null when parent
        // calls findContextObject at construction
        if (reservedContexts != null) {
            for (Context context : reservedContexts) {
                if (versionedPath.equals(context.getPath())) {
                    if (delete) {
                        reservedContexts.remove(context);
                    }
                    return context;
                }
            }
        }
        // privateContexts is null when parent
        // calls findContextObject at construction
        if (privateContexts != null) {
            for (Context context : privateContexts) {
                if (versionedPath.equals(context.getPath())) {
                    if (delete) {
                        privateContexts.remove(context);
                    }
                    return context;
                }
            }
        }
        return null;
    }

    /**
     * Removes a context. If no more contexts left, will also remove this
     * virtual context.
     * @param versionedPath Versioned path of the context.
     * @return true if succeeded, false otherwise.
     */
    @Override
    public boolean removeContext(final String versionedPath) {
        boolean result = super.removeContext(versionedPath);
        if (!result) {
            result = (null != findDeleteContextObject(versionedPath, true));
        }
        if (defaultContext == null && privateContexts.isEmpty() && reservedContexts.isEmpty() && disabledContexts.isEmpty()) {
            removeVirtualContext();
        }
        return result;
    }

    /**
     * Checks whether a given context has been registered.
     * @param versionedPath Versioned path of the context.
     * @return true if found, false otherwise.
     */
    @Override
    public boolean hasContext(final String versionedPath) {
        boolean result = super.hasContext(versionedPath);
        if (!result) {
            result = (null != findDeleteContextObject(versionedPath, false));
        }
        return result;
    }

    /**
     * Finds the context URI string for a request.
     * @param req Request.
     * @return Context URI string for that request, null if none found.
     */
    public String findContext(final Request req) {
        String contextURI = findPrivateContext(req);
        logger.debug("findPrivateContext returned : " + contextURI);
        if (contextURI == null) {
            contextURI = findDisabledContext(req);
            logger.debug("findDisabledContext returned : " + contextURI);
        }
        if (contextURI == null) {
            if (defaultContext == null) {
                logger.debug("No context found, default also null. Returning null.");
            } else {
                // TODO: force session creation ?
                contextURI = defaultContext.getPath();
                logger.debug("No context found, returning default : " + contextURI);
            }
        }
        return contextURI;
    }
}
