/**
 * 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.cxf;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.SystemPropertyAction;

/**
 * Factory to create CXF Bus objects.
 * <p>CXF includes a large number of components that provide services, such
 * as WSDL parsing, and message processing. To avoid creating these objects over and over, and to
 * allow them to be shared easily, they are associated with a data structure called a bus.
 * </p>
 * <p>
 * You don't ever have to explicitly create or manipulate bus objects. If you simply use the CXF
 * or JAX-WS APIs to create clients or servers, CXF will create a default bus for you. You can create a bus
 * explicitly if you need to customize components on the bus or maintain several independent buses
 * with independent configurations.
 * </p>
 * <p>
 * This class maintains the default bus for the entire process and a set of thread-default buses. All CXF
 * components that reference the bus, which is to say all CXF components, will obtain a default bus from this
 * class if you do not set a specific bus.
 * </p>
 * <p>
 * If you create a bus when there is no default bus in effect, that bus will become the default bus.
 * </p>
 * <p>
 * This class holds a reference to the global default bus and a reference to each thread default
 * bus. The thread references are weak with respect to the threads, but otherwise ordinary.
 * Thus, so long as the thread remains alive
 * there will be a strong reference to the bus, and it will not get garbage-collected.
 * If you want to recover memory used CXF, you can set
 * the default and per-thread default bus to null, explicitly.
 * </p>
 */
public abstract class BusFactory {

    public static final String BUS_FACTORY_PROPERTY_NAME = "org.apache.cxf.bus.factory";
    public static final String DEFAULT_BUS_FACTORY = "org.apache.cxf.bus.CXFBusFactory";

    protected static Bus defaultBus;
    
    static class BusHolder {
        Bus bus;
        volatile boolean stale;
    }
    
    protected static final Map<Thread, BusHolder> THREAD_BUSSES = new WeakHashMap<Thread, BusHolder>();
    protected static final ThreadLocal<BusHolder> THREAD_BUS = new ThreadLocal<BusHolder>();

    private static final Logger LOG = LogUtils.getL7dLogger(BusFactory.class);

    /**
     * Creates a new bus. While concrete <code>BusFactory</code> may offer differently parameterized methods
     * for creating a bus, all factories support this no-arg factory method.
     *
     * @return the newly created bus.
     */
    public abstract Bus createBus();

    /**
     * Returns the default bus, creating it if necessary.
     *
     * @return the default bus.
     */
    public static synchronized Bus getDefaultBus() {
        return getDefaultBus(true);
    }

    /**
     * Returns the default bus
     *
     * @param createIfNeeded Set to true to create a default bus if one doesn't exist
     * @return the default bus.
     */
    public static synchronized Bus getDefaultBus(boolean createIfNeeded) {
        if (defaultBus == null && createIfNeeded) {
            defaultBus = newInstance().createBus();
        }
        if (defaultBus == null) {
            // never set up.
            return null;
        } else {
            return defaultBus;
        }
    }
    
    private static BusHolder getThreadBusHolder(boolean set) {
        BusHolder h = THREAD_BUS.get();
        if (h == null || h.stale) {
            Thread cur = Thread.currentThread();
            synchronized (THREAD_BUSSES) {
                h = THREAD_BUSSES.get(cur);
            }
            if (h == null || h.stale) {
                h = new BusHolder();
            
                synchronized (THREAD_BUSSES) {
                    THREAD_BUSSES.put(cur, h);
                }
            }
            if (set) {
                THREAD_BUS.set(h);
            }
        }
        return h;
    }
    

    /**
     * Sets the default bus.
     *
     * @param bus the default bus.
     */
    public static synchronized void setDefaultBus(Bus bus) {
        defaultBus = bus;
        BusHolder b = getThreadBusHolder(false);
        b.bus = bus;
        if (bus == null) {
            b.stale = true;
            THREAD_BUS.remove();
        }
    }

    /**
     * Sets the default bus for the thread.
     *
     * @param bus the default bus.
     */
    public static void setThreadDefaultBus(Bus bus) {
        if (bus == null) {
            BusHolder h = THREAD_BUS.get();
            if (h == null) {
                Thread cur = Thread.currentThread();
                synchronized (THREAD_BUSSES) {
                    h = THREAD_BUSSES.get(cur);
                }
            }
            if (h != null) {
                h.bus = null;
                h.stale = true;
                THREAD_BUS.remove();
            }
        } else {
            BusHolder b = getThreadBusHolder(true);
            b.bus = bus;
        }
    }
    
    /**
     * Sets the default bus for the thread.
     *
     * @param bus the new thread default bus.
     * @return the old thread default bus or null
     */
    public static Bus getAndSetThreadDefaultBus(Bus bus) {
        if (bus == null) {
            BusHolder b = THREAD_BUS.get();
            if (b == null) {
                Thread cur = Thread.currentThread();
                synchronized (THREAD_BUSSES) {
                    b = THREAD_BUSSES.get(cur);
                }
            }
            if (b != null) {
                Bus orig = b.bus;
                b.bus = null;
                b.stale = true;
                THREAD_BUS.remove();
                return orig;
            }
            return null;
        }
        BusHolder b = getThreadBusHolder(true);
        Bus old = b.bus;
        b.bus = bus;
        return old;
    }

    /**
     * Gets the default bus for the thread.
     *
     * @return the default bus.
     */
    public static Bus getThreadDefaultBus() {
        return getThreadDefaultBus(true);
    }

    /**
     * Gets the default bus for the thread, creating if needed
     *
     * @param createIfNeeded Set to true to create a default bus if one doesn't exist
     * @return the default bus.
     */
    public static Bus getThreadDefaultBus(boolean createIfNeeded) {
        if (createIfNeeded) {
            BusHolder b = getThreadBusHolder(false);
            if (b.bus == null) {
                b.bus = createThreadBus();
            }
            return b.bus;
        }
        BusHolder h = THREAD_BUS.get();
        if (h == null || h.stale) {
            Thread cur = Thread.currentThread();
            synchronized (THREAD_BUSSES) {
                h = THREAD_BUSSES.get(cur);
            }
        }
        return h == null || h.stale ? null : h.bus;
    }
    private static synchronized Bus createThreadBus() {
        BusHolder b = getThreadBusHolder(false);
        if (b.bus == null) {
            b.bus = getDefaultBus(true);
        }
        return b.bus;
    }
    /**
     * Removes a bus from being a thread default bus for any thread.
     * <p>
     * This is typically done when a bus has ended its lifecycle (i.e.: a call to
     * {@link Bus#shutdown(boolean)} was invoked) and it wants to remove any reference to itself for any
     * thread.
     *
     * @param bus the bus to remove
     */
    public static void clearDefaultBusForAnyThread(final Bus bus) {
        synchronized (THREAD_BUSSES) {
            for (final Iterator<BusHolder> iterator = THREAD_BUSSES.values().iterator();
                iterator.hasNext();) {
                BusHolder itBus = iterator.next();
                if (bus == null || itBus == null || itBus.bus == null
                    || itBus.stale
                    || bus.equals(itBus.bus)) {
                    if (itBus != null) {
                        itBus.bus = null;
                        //mark as stale so if a thread asks again, it will create a new one
                        itBus.stale = true;  
                    }
                    //This will remove the BusHolder from the only place that should
                    //strongly reference it
                    iterator.remove();
                }
            }
        }
    }

    /**
     * Sets the default bus if a default bus is not already set.
     *
     * @param bus the default bus.
     * @return true if the bus was not set and is now set
     */
    public static synchronized boolean possiblySetDefaultBus(Bus bus) {
        BusHolder b = getThreadBusHolder(false);
        if (b.bus == null) {
            b.bus = bus;
        }
        if (defaultBus == null) {
            defaultBus = bus;
            return true;
        }
        return false;
    }

    /**
     * Create a new BusFactory The class of the BusFactory is determined by looking for the system propery:
     * org.apache.cxf.bus.factory or by searching the classpath for:
     * META-INF/services/org.apache.cxf.bus.factory
     *
     * @return a new BusFactory to be used to create Bus objects
     */
    public static BusFactory newInstance() {
        return newInstance(null);
    }

    /**
     * Create a new BusFactory
     *
     * @param className The class of the BusFactory to create. If null, uses the default search algorithm.
     * @return a new BusFactory to be used to create Bus objects
     */
    public static BusFactory newInstance(String className) {
        BusFactory instance = null;
        if (className == null) {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            className = getBusFactoryClass(loader);
            if (className == null && loader != BusFactory.class.getClassLoader()) {
                className = getBusFactoryClass(BusFactory.class.getClassLoader());
            }
        }
        if (className == null) {
            className = BusFactory.DEFAULT_BUS_FACTORY;
        }

        Class<? extends BusFactory> busFactoryClass;
        try {
            busFactoryClass = ClassLoaderUtils.loadClass(className, BusFactory.class)
                .asSubclass(BusFactory.class);
            
            instance = busFactoryClass.newInstance();
        } catch (Exception ex) {
            LogUtils.log(LOG, Level.SEVERE, "BUS_FACTORY_INSTANTIATION_EXC", ex);
            throw new RuntimeException(ex);
        }
        return instance;
    }

    protected void initializeBus(Bus bus) {
    }

    private static String getBusFactoryClass(ClassLoader classLoader) {

        String busFactoryClass = null;
        String busFactoryCondition = null;

        // next check system properties
        busFactoryClass = SystemPropertyAction.getPropertyOrNull(BusFactory.BUS_FACTORY_PROPERTY_NAME);
        if (isValidBusFactoryClass(busFactoryClass)) {
            return busFactoryClass;
        }

        try {
            // next, check for the services stuff in the jar file
            String serviceId = "META-INF/services/" + BusFactory.BUS_FACTORY_PROPERTY_NAME;
            InputStream is = null;

            if (classLoader == null) {
                classLoader = Thread.currentThread().getContextClassLoader();
            }

            if (classLoader == null) {
                is = ClassLoader.getSystemResourceAsStream(serviceId);
            } else {
                is = classLoader.getResourceAsStream(serviceId);
            }
            if (is == null) {
                serviceId = "META-INF/cxf/" + BusFactory.BUS_FACTORY_PROPERTY_NAME;

                if (classLoader == null) {
                    classLoader = Thread.currentThread().getContextClassLoader();
                }

                if (classLoader == null) {
                    is = ClassLoader.getSystemResourceAsStream(serviceId);
                } else {
                    is = classLoader.getResourceAsStream(serviceId);
                }
            }

            if (is != null) {
                try (BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
                    busFactoryClass = rd.readLine();
                    busFactoryCondition = rd.readLine();
                }
            }
            if (isValidBusFactoryClass(busFactoryClass) 
                && busFactoryCondition != null) {
                try {
                    Class<?> cls =  ClassLoaderUtils.loadClass(busFactoryClass, BusFactory.class)
                        .asSubclass(BusFactory.class);
                    if (busFactoryCondition.startsWith("#")) {
                        busFactoryCondition = busFactoryCondition.substring(1);
                    }
                    int idx = busFactoryCondition.indexOf(',');
                    while (idx != -1) {
                        cls.getClassLoader().loadClass(busFactoryCondition.substring(0, idx));
                        busFactoryCondition = busFactoryCondition.substring(idx + 1);
                        idx = busFactoryCondition.indexOf(',');
                    }
                    cls.getClassLoader().loadClass(busFactoryCondition);
                } catch (ClassNotFoundException e) {
                    busFactoryClass = DEFAULT_BUS_FACTORY;
                } catch (NoClassDefFoundError e) {
                    busFactoryClass = DEFAULT_BUS_FACTORY;
                }
                
            }
            return busFactoryClass;

        } catch (Exception ex) {
            LogUtils.log(LOG, Level.SEVERE, "FAILED_TO_DETERMINE_BUS_FACTORY_EXC", ex);
        }
        return busFactoryClass;
    }

    private static boolean isValidBusFactoryClass(String busFactoryClassName) {
        return busFactoryClassName != null && !"".equals(busFactoryClassName);
    }

}
