/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.commons.chromattic;

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

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.chromattic.api.Chromattic;
import org.chromattic.api.ChromatticBuilder;
import org.chromattic.api.UndeclaredRepositoryException;
import org.exoplatform.container.component.BaseComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.PropertiesParam;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;

/**
 * <p>
 * The chromattic life cycle objets is a plugin that allow to bootstrap a chromattic builder and make it managed either locally
 * or globally.
 * </p>
 *
 * <p>
 * It is allowed to create subclasses of this class to override the methods {@link #onOpenSession(SessionContext)} or
 * {@link #onCloseSession(SessionContext)} to perform additional treatment on the session context at a precise phase of its life
 * cycle.
 * </p>
 *
 * <p>
 * The life cycle domain uniquely identifies the chromattic domain among all domain registered against the
 * {@link org.exoplatform.commons.chromattic.ChromatticManager} manager.
 * </p>
 *
 * The plugin takes an instance of {@link org.exoplatform.container.xml.InitParams} as parameter that contains the following
 * entries:
 *
 * <ul>
 * <li>The <code>domain-name</code> string that is the life cycle domain name</li>
 * <li>The <code>workspace-name</code> string that is the repository workspace name associated with this life cycle</li>
 * <li>The <code>entities</code> list value that contains the list of chromattic entities that will be registered against the
 * builder chromattic builder</li>
 * </ul>
 *
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public class ChromatticLifeCycle extends BaseComponentPlugin {

    /** . */
    private static final Map<String, ChromatticBuilder.Option<?>> RECOGNIZED_OPTIONS;

    static {
        Map<String, ChromatticBuilder.Option<?>> options = new HashMap<String, ChromatticBuilder.Option<?>>();
        for (ChromatticBuilder.Option<?> option : new ChromatticBuilder.Option<?>[] { ChromatticBuilder.JCR_OPTIMIZE_ENABLED,
                ChromatticBuilder.JCR_OPTIMIZE_HAS_NODE_ENABLED, ChromatticBuilder.JCR_OPTIMIZE_HAS_PROPERTY_ENABLED,
                ChromatticBuilder.OBJECT_FORMATTER_CLASSNAME, ChromatticBuilder.ROOT_NODE_PATH,
                ChromatticBuilder.CREATE_ROOT_NODE }) {
            options.put(option.getName(), option);
        }
        RECOGNIZED_OPTIONS = options;
    }

    /** . */
    private final String domainName;

    /** . */
    private final String workspaceName;

    /** . */
    Chromattic realChromattic;

    /** . */
    private ChromatticImpl chromattic;

    /** . */
    ChromatticManager manager;

    /** . */
    private final List<String> entityClassNames;

    /** . */
    final ThreadLocal<LocalContext> currentContext = new ThreadLocal<LocalContext>();

    /** . */
    final Logger log = LoggerFactory.getLogger(ChromatticLifeCycle.class);

    /** . */
    private final Map<String, String> optionMap;

    public ChromatticLifeCycle(InitParams params) {
        Map<String, String> options = new HashMap<String, String>();
        PropertiesParam pp = params.getPropertiesParam("options");
        if (pp != null) {
            options.putAll(pp.getProperties());
        }

        this.domainName = params.getValueParam("domain-name").getValue();
        this.workspaceName = params.getValueParam("workspace-name").getValue();
        this.entityClassNames = params.getValuesParam("entities").getValues();
        this.optionMap = options;
    }

    public String getDomainName() {
        return domainName;
    }

    public final String getRepositoryName() {
        try {
            return manager.repositoryService.getCurrentRepository().getConfiguration().getName();
        } catch (RepositoryException e) {
            throw new UndeclaredRepositoryException("JCR exceptions are really bad", e);
        }
    }

    public final String getWorkspaceName() {
        return workspaceName;
    }

    public final Chromattic getChromattic() {
        return chromattic;
    }

    public final ChromatticManager getManager() {
        return manager;
    }

    AbstractContext getLoginContext() {
        Synchronization sync = manager.getSynchronization();

        //
        if (sync != null) {
            return sync.getContext(domainName);
        }

        //
        return currentContext.get();
    }

    /**
     * Returns <code>#getContext(false)</code>.
     *
     * @see #getContext(boolean)
     * @return a session context
     */
    public final SessionContext getContext() {
        return getContext(false);
    }

    /**
     * A best effort to return a session context whether it's local or global.
     *
     * @param peek true if no context should be automatically created
     * @return a session context
     */
    public final SessionContext getContext(boolean peek) {
        log.trace("Requesting context");
        Synchronization sync = manager.getSynchronization();

        //
        if (sync != null) {
            log.trace("Found synchronization about to get the current context for chromattic " + domainName);
            SynchronizedContext context = sync.getContext(domainName);

            //
            if (context == null && !peek) {
                log.trace("No current context found, about to open one");
                context = sync.openContext(this);
            } else {
                log.trace("Found a context and will return it");
            }

            //
            return context;
        }

        //
        log.trace("No active synchronization about to try the current local context");
        LocalContext localContext = currentContext.get();
        log.trace("Found local context " + localContext);

        //
        return localContext;
    }

    final SessionContext openSynchronizedContext() {
        log.trace("Opening a global context");
        AbstractContext context = (AbstractContext) getContext(true);

        //
        if (context != null) {
            String msg = "A global context is already opened";
            log.trace(msg);
            throw new IllegalStateException(msg);
        }

        // Attempt to get the synchronization
        log.trace("Ok, no global context found, asking current synchronization");
        Synchronization sync = manager.getSynchronization();

        //
        if (sync == null) {
            String msg = "Need global synchronization for opening a global context";
            log.trace(msg);
            throw new IllegalStateException(msg);
        }

        //
        log.trace("Opening a global context for the related sync");
        return sync.openContext(this);
    }

    /**
     * Opens a context and returns it. If there is a global ongoing synchronization then the context will be scoped to that
     * synchronization, otherwise it will be a local context.
     *
     * @return the session context
     */
    public final SessionContext openContext() {
        log.trace("Opening a context");
        AbstractContext context = (AbstractContext) getContext(true);

        //
        if (context != null) {
            String msg = "A context is already opened";
            log.trace(msg);
            throw new IllegalStateException(msg);
        }

        //
        log.trace("Ok, no context found, asking current synchronization");
        Synchronization sync = manager.getSynchronization();

        //
        if (sync != null) {
            log.trace("Found a synchronization, about to open a global context");
            context = sync.openContext(this);
        } else {
            log.trace("Not synchronization found, about to a local context");
            LocalContext localContext = new LocalContext(this);
            currentContext.set(localContext);
            onOpenSession(localContext);
            context = localContext;
        }

        //
        return context;
    }

    public final void closeContext(boolean save) {
        log.trace("Requesting for context close with save=" + save + " going to look for any context");
        AbstractContext context = (AbstractContext) getContext(true);

        //
        if (context == null) {
            String msg = "Cannot close non existing context";
            log.trace(msg);
            throw new IllegalStateException(msg);
        }

        //
        Synchronization sync = manager.getSynchronization();

        if (sync != null) {
            sync.close(this, save);
        } else {
            try {
                context.close(save);
            } finally {
                currentContext.set(null);
            }
            onCloseSession(context);
        }
    }

    protected void onOpenSession(SessionContext context) {
    }

    protected void onCloseSession(SessionContext context) {
    }

    Session doLogin() throws RepositoryException {
        AbstractContext loginContext = getLoginContext();

        //
        if (loginContext == null) {
            throw new IllegalStateException("Could not obtain a login context");
        }

        //
        return loginContext.doLogin();
    }

    public final void start() throws Exception {
        log.debug("About to setup Chromattic life cycle " + domainName);
        ChromatticBuilder builder = ChromatticBuilder.create();

        //
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        for (String className : entityClassNames) {
            String fqn = className.trim();
            log.debug("Adding class " + fqn + " to life cycle " + domainName);
            Class<?> entityClass = cl.loadClass(fqn);
            builder.add(entityClass);
        }

        // Set up boot context
        PortalSessionLifeCycle.bootContext.set(this);

        //
        try {
            for (Map.Entry<String, String> optionEntry : optionMap.entrySet()) {
                ChromatticBuilder.Option<?> option = RECOGNIZED_OPTIONS.get(optionEntry.getKey());
                if (option != null) {
                    log.debug("Setting Chromattic option " + optionEntry);
                    setOption(builder, option, optionEntry.getValue());
                }
            }

            // Set it now, so we are sure that it will be the correct life cycle
            builder.setOptionValue(ChromatticBuilder.SESSION_LIFECYCLE_CLASSNAME, PortalSessionLifeCycle.class.getName());

            //
            log.debug("Building Chromattic " + domainName);
            realChromattic = builder.build();
            chromattic = new ChromatticImpl(this);
        } catch (Exception e) {
            log.error("Could not start Chromattic " + domainName, e);
        } finally {
            PortalSessionLifeCycle.bootContext.set(null);
        }
    }

    private <D> void setOption(ChromatticBuilder builder, ChromatticBuilder.Option<D> option, String value) {
        log.debug("Setting Chromattic option " + option.getDisplayName());
        builder.setOptionValue(option, option.getInstance(value).getValue());
    }

    public final void stop() {
        // Nothing to do for now
    }
}
