/*
 * 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.deltaspike.core.util;

import jakarta.enterprise.inject.Vetoed;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;
import org.apache.deltaspike.core.spi.activation.ClassDeactivator;
import org.apache.deltaspike.core.spi.activation.Deactivatable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Helper methods for {@link ClassDeactivator}
 */
@Vetoed
public abstract class ClassDeactivationUtils
{
    private static final Logger LOG = Logger.getLogger(ClassDeactivationUtils.class.getName());

    /**
     * This Map holds the ClassLoader as first level to make it possible to have different configurations per 
     * WebApplication in an EAR or other Multi-ClassLoader scenario.
     *
     * The Map then contains a List of {@link ClassDeactivator}s in order of their configured ordinal.
     */
    private static Map<ClassLoader, List<ClassDeactivator>> classDeactivatorMap
        = new ConcurrentHashMap<>();

    /**
     * Cache for the result. It won't contain many classes but it might be accessed frequently.
     * Valid entries are only true or false. If an entry isn't available or null, it gets calculated.
     */
    private static Map<Class<? extends Deactivatable>, Boolean> activationStatusCache
        = new ConcurrentHashMap<Class<? extends Deactivatable>, Boolean>();

    private static ProjectStage previouslyDetectedProjectStage;

    private ClassDeactivationUtils()
    {
        // prevent instantiation
    }

    /**
     * Flush the caches to prevent ClassLoader leaks.
     * This is called internally by DeltaSpike.
     * Users do not have to explicitly call this method.
     * Does not have side effects as the cache is idempotent anyway.
     */
    public static void clearCache()
    {
        classDeactivatorMap.clear();
    }

    /**
     * Evaluates if the given {@link Deactivatable} is active.
     *
     * @param targetClass {@link Deactivatable} under test.
     * @return <code>true</code> if it is active, <code>false</code> otherwise
     */
    public static boolean isActivated(Class<? extends Deactivatable> targetClass)
    {
        performProjectStageDependentCleanup();

        //just to support parallel access to #isActivated (+ reset) in project-stage unit-test and development
        Map<Class<? extends Deactivatable>, Boolean> activeCache = activationStatusCache;
        Boolean activatedClassCacheEntry = activeCache.get(targetClass);

        if (activatedClassCacheEntry == null)
        {
            initDeactivatableCacheFor(targetClass, activeCache);
            activatedClassCacheEntry = activeCache.get(targetClass);
        }
        return activatedClassCacheEntry;
    }

    private static void performProjectStageDependentCleanup()
    {
        ProjectStage currentProjectStage = ProjectStageProducer.getInstance().getProjectStage();

        if (previouslyDetectedProjectStage != currentProjectStage)
        {
            previouslyDetectedProjectStage = currentProjectStage;
            //don't use #clear to support parallel access to #isActivated (+ reset) without synchronization
            activationStatusCache = new ConcurrentHashMap<Class<? extends Deactivatable>, Boolean>();

            //#clear is ok here due to the handling in the synchronized method #initDeactivatableCacheFor
            classDeactivatorMap.clear();
        }
        else if (currentProjectStage == ProjectStage.UnitTest || currentProjectStage == ProjectStage.Development)
        {
            activationStatusCache = new ConcurrentHashMap<Class<? extends Deactivatable>, Boolean>();
        }
    }

    private static synchronized void initDeactivatableCacheFor(Class<? extends Deactivatable> targetClass,
                                                               Map<Class<? extends Deactivatable>, Boolean> activeCache)
    {
        Boolean activatedClassCacheEntry = activeCache.get(targetClass);

        if (activatedClassCacheEntry != null) //double-check
        {
            return;
        }

        List<ClassDeactivator> classDeactivators = getClassDeactivators();

        Boolean isActivated = Boolean.TRUE;
        Class<? extends ClassDeactivator> deactivatedBy = null;

        LOG.fine("start evaluation if " + targetClass.getName() + " is de-/activated");

        // we get the classActivators ordered by it's ordinal
        // thus the last one which returns != null 'wins' ;)
        for (ClassDeactivator classDeactivator : classDeactivators)
        {
            Boolean isLocallyActivated = classDeactivator.isActivated(targetClass);

            if (isLocallyActivated != null)
            {
                isActivated = isLocallyActivated;

                /*
                * Check and log the details across class-deactivators
                */
                if (!isActivated)
                {
                    deactivatedBy = classDeactivator.getClass();
                    LOG.fine("Deactivating class " + targetClass);
                }
                else if (deactivatedBy != null)
                {
                    LOG.fine("Reactivation of: " + targetClass.getName() + " by " +
                            classDeactivator.getClass().getName() +
                            " - original deactivated by: " + deactivatedBy.getName() + ".\n" +
                            "If that isn't the intended behaviour, you have to use a higher ordinal for " +
                            deactivatedBy.getName());
                }
            }
        }

        cacheResult(targetClass, isActivated, activeCache);
    }

    private static void cacheResult(Class<? extends Deactivatable> targetClass, Boolean activated,
                                    Map<Class<? extends Deactivatable>, Boolean> activeCache)
    {
        activeCache.put(targetClass, activated);
        if (LOG.isLoggable(Level.FINE))
        {
            LOG.fine("class: " + targetClass.getName() + " activated=" + activated);
        }
    }

    /**
     * @return the List of configured @{link ClassDeactivator}s for the current context ClassLoader.
     */
    private static List<ClassDeactivator> getClassDeactivators()
    {
        ClassLoader classLoader = ClassUtils.getClassLoader(null);
        List<ClassDeactivator> classDeactivators = classDeactivatorMap.get(classLoader);

        if (classDeactivators == null)
        {
            return initConfiguredClassDeactivators(classLoader);
        }

        return classDeactivators;
    }

    //synchronized isn't needed - #initDeactivatableCacheFor is already synchronized
    private static List<ClassDeactivator> initConfiguredClassDeactivators(ClassLoader classLoader)
    {
        List<String> classDeactivatorClassNames = ConfigResolver.getAllPropertyValues(ClassDeactivator.class.getName());

        List<ClassDeactivator> classDeactivators = new ArrayList<ClassDeactivator>();

        for (String classDeactivatorClassName : classDeactivatorClassNames)
        {
            LOG.fine("processing ClassDeactivator: " + classDeactivatorClassName);

            try
            {
                ClassDeactivator currentClassDeactivator =
                        (ClassDeactivator) ClassUtils.instantiateClassForName(classDeactivatorClassName);
                classDeactivators.add(currentClassDeactivator);
            }
            catch (Exception e)
            {
                LOG.warning(classDeactivatorClassName + " can't be instantiated");
                throw new IllegalStateException(e);
            }
        }

        classDeactivatorMap.put(classLoader, classDeactivators);
        return classDeactivators;
    }
}
