/*
 * Copyright 2009-2017 Aconex
 *
 * 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 io.pcp.parfait.timing;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import net.jcip.annotations.ThreadSafe;

/**
 * <p>
 * Map-like functions to keep track of key/value pairs for application threads.
 * Keys are Strings, with values of any arbitrary object. Optionally keeps a log
 * framework's MDC weakly in sync with changes -- that is, there is no atomicity
 * guarantee so it's plausible that this class' context information and the
 * logger's will not be in a consistent state; however, log4j and logback's MDC
 * expose data <em>only</em> to the current thread via a thread-local so this is
 * unlikely to be a problem in practice.
 * </p>
 * <p>
 * Most methods operate on the context of the calling thread; only
 * {@link #forThread(Thread)} allows cross-thread information retrieval.
 * </p>
 * 
 * @author Cowan
 */
@ThreadSafe
public class ThreadContext {
    private static final CacheLoader<Thread, Map<String, Object>> NEW_CONTEXT_CREATOR = new CacheLoader<Thread, Map<String, Object>>() {
        @Override
        public Map<String, Object> load(Thread thread) throws Exception {
            return new ConcurrentHashMap<>();
        }
    };
    private final LoadingCache<Thread, Map<String, Object>> PER_THREAD_CONTEXTS = CacheBuilder.newBuilder().weakKeys().build(NEW_CONTEXT_CREATOR);

    private volatile MdcBridge mdcBridge = new NullMdcBridge();

    public ThreadContext() {
        this(new NullMdcBridge());
    }

    public ThreadContext(MdcBridge mdcBridge) {
        // TODO should that be a static variable..?
        this.mdcBridge=mdcBridge;
    }

    /**
     * Adds the given key/value pair to the current thread's context, and updates {@link MdcBridge} with
     * same.
     */
    public void put(String key, Object value) {
        PER_THREAD_CONTEXTS.getUnchecked(Thread.currentThread()).put(key, value);
        mdcBridge.put(key, value);
    }

    /**
     * Removes the given key from the current thread's context and {@link MdcBridge}.
     */
    public void remove(String key) {
        PER_THREAD_CONTEXTS.getUnchecked(Thread.currentThread()).remove(key);
        mdcBridge.remove(key);
    }

    /**
     * Retrieves the value corresponding to the supplied key for the current thread (null if no such
     * value exists)
     */
    public Object get(String key) {
        return PER_THREAD_CONTEXTS.getUnchecked(Thread.currentThread()).get(key);
    }

    /**
     * Clears all values for the current thread.
     */
    public void clear() {

        /**
         * Unfortunately log4j's MDC historically never had a mechanism to block remove keys,
         * so we're forced to do this one by one.
         */
        for (String key : allKeys()) {
           mdcBridge.remove(key);
        }

        PER_THREAD_CONTEXTS.getUnchecked(Thread.currentThread()).clear();
    }

    /**
     * Retrieves a copy of the thread context for the given thread
     */
    public Map<String, Object> forThread(Thread t) {
        return new HashMap<String, Object>(PER_THREAD_CONTEXTS.getUnchecked(t));
    }

    public Collection<String> allKeys() {
        Set<String> keys = new HashSet<String>();
        for (Map<String, Object> threadMdc : PER_THREAD_CONTEXTS.asMap().values()) {
            keys.addAll(threadMdc.keySet());
        }
        return keys;
    }

    public Object getForThread(Thread thread, String key) {
        return PER_THREAD_CONTEXTS.getUnchecked(thread).get(key);
    }

    /**
     * Factory method that creates a new ThreadContext initialized to also update Log4j's MDC.
     */
    public static ThreadContext newMDCEnabledContext() {
        return new ThreadContext(new Log4jMdcBridge());
    }

    /**
     * Factory method that creates a new ThreadContext initialised to also update SLF4J's MDC
     */
    public static ThreadContext newSLF4JEnabledContext() {
        return new ThreadContext(new Slf4jMDCBridge());
    }

    public interface MdcBridge {
    	void put(String key, Object object);

		void remove(String key);
    }
    
    public static class NullMdcBridge implements MdcBridge {
		@Override
		public void put(String key, Object object) {
			// no-op
		}

		@Override
		public void remove(String key) {
			// no-op
		}
    }
    
    public static class Log4jMdcBridge implements MdcBridge {
		@Override
		public void put(String key, Object object) {
			org.apache.log4j.MDC.put(key, object);
		}

		@Override
		public void remove(String key) {
			org.apache.log4j.MDC.remove(key);
		}
	}

    public static class Slf4jMDCBridge implements MdcBridge {
		@Override
		public void put(String key, Object object) {
			org.slf4j.MDC.put(key, String.valueOf(object));
		}

		@Override
		public void remove(String key) {
			org.slf4j.MDC.remove(key);
		}
	}

}
