/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.causeway.commons.internal.context;

import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.collections._Lists;

import org.jspecify.annotations.NonNull;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * Provides a context for storing and retrieving singletons (usually application scoped).
 * Writes to the store are implemented thread-safe.
 * </p>
 * <p>
 * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
 * These may be changed or removed without notice!
 * </p>
 * @since 2.0
 */
public final class _Context {

    private _Context(){}

    /**
     * Thread-safety note: We let threads synchronize on writes to the singletonMap,
     * NOT on reads.<br/>
     * If there is a race-condition between a writing and a reading thread, by design
     * the first one wins.<br/>
     * If synchronization is required it should happen elsewhere, not here!<br/>
     */
    private static final Map<Class<?>, Object> singletonMap = new ConcurrentHashMap<>();

    private static final Object $LOCK = new Object[0];

    /**
     * Puts a singleton instance onto the current context.
     * @param type non-null
     * @param singleton non-null
     * @throws IllegalStateException if there is already an instance of same {@code type}
     *  on the current context.
     */
    public static <T> void putSingleton(final @NonNull Class<? super T> type, final @NonNull T singleton) {

        // let writes to the map be atomic
        synchronized ($LOCK) {
            if(singletonMap.containsKey(type))
                throw new IllegalStateException(
                        "there is already a singleton of type '"+type+"' on this context.");
            singletonMap.put(type, singleton);
        }
    }

    /**
     * Puts a singleton instance onto the current context, that
     * either overrides any already present or ignores the call depending on {@code override}.
     * @param type non-null
     * @param singleton non-null
     * @param override whether to overrides any already present singleton or not
     * @return whether the {@code singleton} was put on the context or ignored because there is already one present
     */
    public static <T> boolean put(final @NonNull Class<? super T> type, final @NonNull T singleton, final boolean override) {
        // let writes to the map be atomic
        synchronized ($LOCK) {
            if(singletonMap.containsKey(type)) {
                if(!override)
                    return false;
            }
            singletonMap.put(type, singleton);
            return true;
        }
    }

    /**
     * Optionally the singleton instance of {@code type}, based on whether there is any registered.
     * @param type non-null
     */
    public static <T> Optional<T> lookup(final Class<? super T> type) {
        return Optional.ofNullable(getIfAny(type));
    }

    /**
     * Gets a singleton instance of {@code type} if there is any, null otherwise.
     * @param type non-null
     * @return null, if there is no such instance
     */
    public static <T> T getIfAny(final Class<? super T> type) {
        return _Casts.uncheckedCast(singletonMap.get(type));
    }

    /**
     * If the specified key is not already associated with a value (or is mapped to null),
     * attempts to compute its value using the given factory function and enters it into this map unless null.
     * @param type
     * @param factory
     * @return null, if there is no such instance
     */
    public static <T> T computeIfAbsent(final @NonNull Class<? super T> type, final @NonNull Function<Class<? super T>, T> factory) {

        final T existingIfAny = _Casts.uncheckedCast(singletonMap.get(type));
        if(existingIfAny!=null) {
            return existingIfAny;
        }

        // Note: we don't want to do this inside the synchronized block
        final T t = factory.apply(type);

        // we don't store null to the map
        if(t==null) {
            return null;
            //        	throw _Exceptions.unrecoverable(String.format("factory to compute new value for type '%s' "
            //        			+ "returned 'null', which is not allowed", type));
        }

        // let writes to the map be atomic
        synchronized ($LOCK) {

            // Note: cannot just use 'singletonMap.computeIfAbsent(toKey(type), __->factory.apply(type));'
            // here because it does not allow for modification of singletonMap inside the factory call
            // Also we do need a second check for existing key, since it might have been changed by another
            // thread since.
            final T existingIfAny2 = _Casts.uncheckedCast(singletonMap.get(type));
            if(existingIfAny2!=null) {
                return existingIfAny2;
            }

            singletonMap.put(type, t);
            return t;
        }
    }

    /**
     * If the specified key is not already associated with a value (or is mapped to null),
     * attempts to compute its value using the given factory supplier and enters it into this map unless null.
     * @param type
     * @param factory
     * @return null, if there is no such instance
     */
    public static <T> T computeIfAbsent(final @NonNull Class<? super T> type, final @NonNull Supplier<T> factory) {
        return computeIfAbsent(type, __->factory.get());
    }

    /**
     * Gets a singleton instance of {@code type} if there is any,
     * otherwise returns the {@code fallback}'s result,
     * which could be null.
     * @param type non-null
     * @param fallback non-null
     */
    public static <T> T getOrElse(final Class<? super T> type, final @NonNull Supplier<T> fallback) {
        return Optional
                .ofNullable(_Casts.<T>uncheckedCast(getIfAny(type)))
                .orElseGet(fallback);
    }

    /**
     * Gets a singleton instance of {@code type} if there is any,
     * otherwise throws the {@code onNotFound}'s result.
     * @param type non-null
     * @param onNotFound non-null
     * @throws Exception
     */
    public static <T, E extends Exception> T getElseThrow (
            final @NonNull Class<? super T> type,
            final @NonNull Supplier<E> onNotFound) throws E {
        return Optional
                .ofNullable(_Casts.<T>uncheckedCast(getIfAny(type)))
                .orElseThrow(onNotFound);
    }

    /**
     * Gets a singleton instance of {@code type} if there is any,
     * otherwise throws a NoSuchElementException.
     * @param type non-null
     */
    public static <T> T getElseFail(final Class<? super T> type) {
        return Optional
                .ofNullable(_Casts.<T>uncheckedCast(getIfAny(type)))
                .orElseThrow(()->new NoSuchElementException(
                        String.format("Could not resolve an instance of type '%s'", type.getName())));
    }

    // -- REMOVAL

    public static void remove(final Class<?> type) {
        // let writes to the map be atomic
        synchronized ($LOCK) {
            singletonMap.remove(type);
        }
        tryClose(type);
    }

    // -- CLEARING

    /**
     * Removes any singleton references from the current context. <br/>
     * Any singletons that implement the AutoClosable interface are being closed.
     */
    public static void clear() {

        // let writes to the map be atomic
        synchronized ($LOCK) {

            closeAnyClosables(_Lists.newArrayList(singletonMap.values()));

            singletonMap.clear();
            //_Context_ThreadLocal.clear();
        }
    }

    private static void closeAnyClosables(final List<Object> objects) {
        _NullSafe.stream(objects)
        .forEach(_Context::tryClose);
    }

    // -- THREAD LOCAL SUPPORT

//    /**
//     * Clear key {@code type} from current thread's map.
//     * @param type - the key into the thread-local store
//     */
//    public static void threadLocalClear(final Class<?> type) {
//        _Context_ThreadLocal.clear(type);
//    }
//
//    /**
//     * Puts {@code payload} onto the current thread's map.
//     * @param type - the key into the thread-local store
//     * @param payload
//     * @return a Runnable which, when run, removes any references to payload
//     */
//    public static <T> Runnable threadLocalPut(final Class<? super T> type, final T payload) {
//        return _Context_ThreadLocal.put(type, payload);
//    }
//
//
//    /**
//     * Looks up current thread's values for any instances that match the given type, as previously stored
//     * with {@link _Context#threadLocalPut(Class, Object)}.
//     * @param type - the key into the thread-local store
//     */
//    public static <T> Can<T> threadLocalGet(final Class<? super T> type) {
//        return _Context_ThreadLocal.get(type);
//    }
//
//    /**
//     * Looks up current thread's values for any instances that match the given type, as previously stored
//     * with {@link _Context#threadLocalPut(Class, Object)}.
//     * @param type - the key into the thread-local store
//     * @param requiredType - the required type of the elements in the returned bin
//     */
//    public static <T> Can<T> threadLocalSelect(final Class<? super T> type, final Class<? super T> requiredType) {
//        return _Context_ThreadLocal.select(type, requiredType);
//    }
//
//    /**
//     * Removes any of current thread's values as stored with {@link _Context#threadLocalPut(Class, Object)}.
//     */
//    public static <T> void threadLocalCleanup() {
//        _Context_ThreadLocal.cleanupThread();
//    }

    // -- DEFAULT CLASSLOADER

    private static final Supplier<ClassLoader> FALLBACK_CLASSLOADER =
            Thread.currentThread()::getContextClassLoader;

    /**
     * As set by the framework's bootstrapping mechanism.
     * @return the default class loader (non-null)
     */
    public static ClassLoader getDefaultClassLoader() {
        return getOrElse(ClassLoader.class, FALLBACK_CLASSLOADER);
    }

    /**
     * Set by the framework's bootstrapping mechanism.
     * @param classLoader the framework's default class loader
     * @param override whether to override if already registered
     */
    public static void setDefaultClassLoader(final @NonNull ClassLoader classLoader, final boolean override) {
        final boolean alreadyRegistered = _Context.getIfAny(ClassLoader.class)!=null;
        if(!alreadyRegistered || override) {
            // let writes to the map be atomic
            synchronized ($LOCK) {
                singletonMap.put(ClassLoader.class, classLoader);
            }
        }
    }

    // -- CLASS LOADING SHORTCUTS

    /**
     * Uses the frameworks default-ClassLoader to load a class by name.
     * @param className
     * @return class by name
     * @throws ClassNotFoundException
     */
    public static Class<?> loadClass(final String className) throws ClassNotFoundException{
        return getDefaultClassLoader().loadClass(className);
    }

    /**
     * Uses the frameworks default-ClassLoader to load and initialize a class by name.<br/>
     * <b>Initialize</b> the class, that is, all static initializers will be run. <br/>
     * (For details on initialize see Section 12.4 of The Java Language Specification)
     * @param className
     * @throws ClassNotFoundException
     */
    public static Class<?> loadClassAndInitialize(final String className) throws ClassNotFoundException{
        return Class.forName(className, true, getDefaultClassLoader());
    }

    public static boolean isJUnitTest() {
        for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
            if (element.getClassName().startsWith("org.junit.")) {
                return true;
            }
        }
        return false;
    }

    // -- HELPER

    private static void tryClose(final Object singleton) {
        if(singleton==null) {
            return;
        }
        if(singleton instanceof AutoCloseable) {
            try {
                ((AutoCloseable)singleton).close();
            } catch (Exception e) {
                // [ahuber] nothing we can do here, so ignore
            }
        }
    }

}
