/*
 * Copyright 2017 - 2020 Acosix GmbH
 *
 * 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 de.acosix.alfresco.simplecontentstores.repo.store.context;

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

import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.NodeContentContext;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class for thread-local data for an ongoing call / use of content store related components. This utility is necessary since not
 * all operations in the ContentStore API have access to the {@link ContentContext} passed in {@link ContentStore#getWriter(ContentContext)
 * getWriter} and {@link ContentStore#getReader(String) getReader} completely lacks that parameter. Additionally, some operations may
 * complete asynchronously when the original context is not even held anywhere in the call stack anymore. This utility class allows a
 * component that may asynchronously need the restore a specific context to obtain a handle on it.
 *
 * @author Axel Faust
 */
public final class ContentStoreContext
{

    /**
     * Key for accessing the {@link NodeRef node reference} of the node used as the context in the active
     * {@code getWriter}/{@code getReader} call
     */
    public static final String DEFAULT_ATTRIBUTE_NODE = "node";

    /**
     * Key for accessing the {@link QName qualified property name} of the content property used as the context in the active
     * {@code getWriter}/{@code getReader} call
     */
    public static final String DEFAULT_ATTRIBUTE_PROPERTY = "property";

    /**
     * Key for accessing the {@link ContentModel#PROP_NAME short name} of the site the node used as the context in the active
     * {@code getWriter}/{@code getReader} call, if the node is located in a site at all
     */
    public static final String DEFAULT_ATTRIBUTE_SITE = "site";

    /**
     * Key for accessing the {@link SiteModel#PROP_SITE_PRESET preset} of the site the node used as the context in the active
     * {@code getWriter}/{@code getReader} call, if the node is located in a site at all
     */
    public static final String DEFAULT_ATTRIBUTE_SITE_PRESET = "sitePreset";

    /**
     * Key for accessing the current content data of the node for which a call to
     * {@link ContentService#getReader(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) getReader}
     * is processed via the public {@link ContentService} bean, or a call to {@link ContentStore#getWriter(ContentContext) getWriter} is
     * provided with a {@link NodeContentContext}
     */
    public static final String DEFAULT_ATTRIBUTE_CONTENT_DATA = "contentData";

    private static final Logger LOGGER = LoggerFactory.getLogger(ContentStoreContext.class);

    private static final ThreadLocal<Map<String, Object>> CONTEXT_ATTRIBUTES = new ThreadLocal<>();

    /**
     * This custom functional interface should be used for encapsulating operations that need to be run in an active content store context.
     *
     * This interface is essentially a {@code FunctionalInterface} but does not use the annotation to not force Java 7 on users of this
     * addon (just yet).
     *
     * @author Axel Faust
     */
    public static interface ContentStoreOperation<R>
    {

        /**
         * Executes the operation.
         *
         * @return the result of the operation
         */
        public R execute();

    }

    /**
     * This custom functional interface encapsulates restoration handlers for a previously active content store context.
     *
     * This interface is essentially a {@code FunctionalInterface} but does not use the annotation to not force Java 7 on users of this
     * addon (just yet).
     *
     * @author Axel Faust
     */
    public static interface ContentStoreContextRestorator<R>
    {

        /**
         * Restores the previous content store context and executes an operation within that context.
         *
         * @param operation
         *            the operation to execute inside the restored context
         * @return the result of the operation
         */
        public R withRestoredContext(ContentStoreOperation<R> operation);

    }

    /**
     * Retrieves the value of a context attribute from the currently active content store context.
     *
     * @param key
     *            the key to the attribute value
     * @return the value of the attribute or {@code null} if it has not been set
     */
    public static Object getContextAttribute(final String key)
    {
        final Map<String, Object> currentMap = CONTEXT_ATTRIBUTES.get();
        final Object result = currentMap != null ? currentMap.get(key) : null;
        return result;
    }

    /**
     * Retrieves the value of a context attribute from the currently active content store context.
     *
     * @param key
     *            the key to the attribute value
     * @param value
     *            the value to set
     *
     * @throws IllegalStateException
     *             if there is no currently active content store context in the current thread context
     */
    public static void setContextAttribute(final String key, final Object value)
    {
        final Map<String, Object> currentMap = CONTEXT_ATTRIBUTES.get();
        if (currentMap == null)
        {
            throw new IllegalStateException("No content store context is currently active");
        }
        LOGGER.debug("Setting context attribute {} to {}", key, value);
        currentMap.put(key, value);
    }

    /**
     * Executes an operation within a new content store context. Code in the provided operation can call
     * {@link #setContextAttribute(String, Object) setContextAttribute} and be sure no {@link IllegalStateException} will be thrown.
     *
     * @param <R>
     *            the return type of the operation to execute
     * @param operation
     *            the operation to execute
     * @return the result of the operation
     */
    public static <R> R executeInNewContext(final ContentStoreOperation<R> operation)
    {
        final Map<String, Object> oldMap = CONTEXT_ATTRIBUTES.get();
        final Map<String, Object> newMap = new HashMap<>();
        CONTEXT_ATTRIBUTES.set(newMap);
        try
        {
            LOGGER.trace("Running operation {} in new context", operation);
            final R result = operation.execute();
            return result;
        }
        catch (final Exception ex)
        {
            if (ex instanceof RuntimeException)
            {
                throw (RuntimeException) ex;
            }
            LOGGER.debug("Unhandled exception in content store context operation", ex);
            throw new ContentIOException("Unhandled error in content store operation", ex);
        }
        finally
        {
            LOGGER.trace("Leaving context");
            CONTEXT_ATTRIBUTES.set(oldMap);
        }
    }

    /**
     * Obtains a handle to restore the current content store context state at a later point in time, e.g. to complete some asynchronous work
     * outside of the original content store context. Any attributes {@link #setContextAttribute(String, Object) set / modified} after this
     * operation is called will not be reflected in the restored content store context state.
     *
     * @param <R>
     *            the return type of any operation passed to the restoration handle
     * @return the restoration handle
     *
     * @throws IllegalStateException
     *             if there is no currently active content store context in the current thread context
     */
    public static <R> ContentStoreContextRestorator<R> getContextRestorationHandle()
    {
        final Map<String, Object> savedContextAttributes;
        {
            final Map<String, Object> currentMap = CONTEXT_ATTRIBUTES.get();
            if (currentMap == null)
            {
                throw new IllegalStateException("No content store context is currently active");
            }
            savedContextAttributes = new HashMap<>(currentMap);
        }

        // could have used Lambda in Java 8 but don't want to force it on addon users
        final ContentStoreContextRestorator<R> restorationHandle = operation -> {
            final Map<String, Object> oldMap = CONTEXT_ATTRIBUTES.get();
            final Map<String, Object> newMap = new HashMap<>(savedContextAttributes);
            CONTEXT_ATTRIBUTES.set(newMap);
            try
            {
                LOGGER.trace("Running operation {} in restored context {}", operation, savedContextAttributes);
                final R result = operation.execute();
                return result;
            }
            catch (final Exception ex)
            {
                if (ex instanceof RuntimeException)
                {
                    throw (RuntimeException) ex;
                }
                LOGGER.debug("Unhandled exception in content store context operation", ex);
                throw new ContentIOException("Unhandled error in content store operation", ex);
            }
            finally
            {
                LOGGER.trace("Leaving restored context");
                CONTEXT_ATTRIBUTES.set(oldMap);
            }
        };

        return restorationHandle;
    }
}