/*
 * Copyright 2013 the original author or authors.
 *
 * 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 org.powermock.core.testlisteners;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Core static utility to help modules, such as PowerMockRunner, that needs to
 * communicate with some 3rd-party framework in order to properly fire
 * events to PowerMockTestListener instances.
 */
public class GlobalNotificationBuildSupport {

    public interface Callback {
        void suiteClassInitiated(Class<?> testClass);

        void testInstanceCreated(Object testInstance);
    }

    private static final Map<String, Callback> testSuiteCallbacks =
            new ConcurrentHashMap<String, Callback>();

    private static final Map<Class<?>, Callback> initiatedTestSuiteClasses =
            new ConcurrentHashMap<Class<?>, Callback>();

    private static final ThreadLocal<Class<?>> pendingInitiatedTestClass =
            new ThreadLocal<Class<?>>();

    public static void prepareTestSuite(
            String testClassName, Callback callback) {
        if (testSuiteCallbacks.containsKey(testClassName)) {
            throw new IllegalStateException(
                    "Needs to wait for concurrent test-suite execution to start!");
        } else {
            testSuiteCallbacks.put(testClassName, callback);
            Class<?> initiatedTestClass = pendingInitiatedTestClass.get();
            if (null != initiatedTestClass
                    && initiatedTestClass.getName().equals(testClassName)) {
                System.err.println("Detected late test-suite preparation of "
                        + "already initiated test-" + initiatedTestClass);
                testClassInitiated(initiatedTestClass);
            }
        }
    }

    public static void testClassInitiated(Class<?> testClass) {
        if (!initiatedTestSuiteClasses.containsKey(testClass)) {
            Callback callback = testSuiteCallbacks.get(testClass.getName());
            if (null == callback) {
                pendingInitiatedTestClass.set(testClass);
            } else {
                initiatedTestSuiteClasses.put(testClass, callback);
                callback.suiteClassInitiated(testClass);
                pendingInitiatedTestClass.set(null);
            }
        }
    }

    private static int countInitializersInTrace(final String className) {
        int initializerCount = 0;
        for (StackTraceElement ste : new Throwable().getStackTrace()) {
            if ("<init>".equals(ste.getMethodName())
                    && className.equals(ste.getClassName())
                    && 2 <= ++initializerCount) {
                return 2;
            }
        }
        return initializerCount;
    }

    public static void testInstanceCreated(Object testInstance) {
        for (Class<?> c = testInstance.getClass(); Object.class != c; c = c.getSuperclass()) {
            Callback callback = initiatedTestSuiteClasses.get(c);
            if (null != callback) {
                if (1 == countInitializersInTrace(c.getName())) {
                    callback.testInstanceCreated(testInstance);
                }
                return;
            }
        }
    }

    public static void closeTestSuite(Class<?> testClass) {
        Callback callback = initiatedTestSuiteClasses.remove(testClass);
        if (null != callback
                && !initiatedTestSuiteClasses.values().contains(callback)) {
            testSuiteCallbacks.values().remove(callback);
        }
    }

    public static void closePendingTestSuites(Callback callback) {
        testSuiteCallbacks.values().remove(callback);
        initiatedTestSuiteClasses.values()
                .removeAll(java.util.Collections.singleton(callback));
    }
}
