/*
 * 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.felix.scr.impl.logger;

import java.io.Closeable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.Logger;
import org.osgi.service.log.LoggerFactory;
import org.osgi.util.tracker.ServiceTracker;

/*
 * A class that tracks the LoggerFactory of a bundle and its associated Logger objects. 
 * When a bundle is stopped and/or the service is unregistered, the logger objects are cleaned.
 * 
 * The basic technique is to use a facade. Instead of returning a log object, we return a facade. The 
 * methods delegate to the actual logger object. If there is no logger object, we create one.
 * 
 * The LogDomain represents every bundle. Per LogDomain, we keep the facades. If the factory goes,
 * we reset the facades.
 */
class LogManager extends ServiceTracker<Object, Object> implements BundleListener
{

    private static final String LOGGER_FACTORY_CLASS_NAME = "org.osgi.service.log.LoggerFactory";

    final BundleContext scrContext;
    final BundleContext systemContext;
    final AtomicBoolean closed = new AtomicBoolean(false);

    /*
     * Locks access to guarded fields
     */
    class Lock
    {
        final Map<Bundle, LogDomain> domains = new HashMap<>();
        int trackingCount;
        Object factory;
        int ranking = 0;

        synchronized LogDomain getLogDomain(Bundle bundle)
        {
            LogDomain domain = domains.get(bundle);
            if (domain == null)
            {
                domain = new LogDomain(bundle);
                domains.put(bundle, domain);
            }
            return domain;
        }

        synchronized void removedFactory(Object service)
        {
            if (this.factory == service)
            {
                this.factory = null;
                reset();
            }
        }

        synchronized void setFactory(int ranking, Object service)
        {
            if (this.factory == null)
            {
                this.factory = service;
                this.ranking = ranking;
            }
            else if (this.ranking < ranking)
            {
                this.factory = service;
                this.ranking = ranking;
                reset();
            }
        }

        synchronized void reset()
        {
            for (LogDomain domain : domains.values())
            {
                domain.reset();
            }
        }

        synchronized Object getLogger(LoggerFacade facade, Bundle bundle, String name)
        {
            if (factory == null)
            {
                return facade.logger = null;
            }
            else
            {
                try
                {
                    return facade.logger = ((LoggerFactory) factory).getLogger(bundle,
                        name, Logger.class);
                }
                catch (IllegalArgumentException e)
                {
                    // The bundle probably got uninstalled or somehow unresolved,
                    // fallback to the SCR bundle's logger
                    return ((LoggerFactory) factory).getLogger(context.getBundle(), name,
                        Logger.class);
                }
            }
        }

        synchronized LogDomain remove(Bundle bundle)
        {
            return domains.remove(bundle);
        }

        synchronized void close()
        {
            reset();
            domains.clear();
        }

    }

    final Lock lock = new Lock();
    
    private final LogConfiguration config;

    LogManager(BundleContext context, LogConfiguration config)
    {
        super(context, LOGGER_FACTORY_CLASS_NAME, null);
        this.scrContext = context;
        this.systemContext = context.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext();
        this.config = config;
    }
    
    /**
     * Initializes the log manager. This internally executes the following:
     * 
     * <ul>
     * <li>
     * Track all bundles for retrieving the log levels of each bundles if the log extension 
     * is ({@code ds.log.extension}) not set or set to {@code false} (log extension disabled)
     * </li>
     * <li>
     * Don't track any bundles if log extension is enabled since we don't need the log levels 
     * of the respective bundles. For log extension, we use the log level of the SCR bundle itself.
     * <li>
     * <li>
     * Start the service tracker to track the OSGi LoggerFactory service
     * </li>
     * </ul>
     */
    public void init() 
    {
        if (!config.isLogExtensionEnabled()) 
        {
            // Use system context to do cleanup of domains.
            // This ensures we always can cleanup even when the SCR context
            // cannot see the bundle.
            systemContext.addBundleListener(this);
        }
        this.open();
    }

    @Override
    public Object addingService(ServiceReference<Object> reference)
    {
        Object service = super.addingService(reference);
        Integer ranking = (Integer) reference.getProperty(Constants.SERVICE_RANKING);
        if (ranking == null)
            ranking = 0;
        lock.setFactory(ranking, service);
        return service;
    }

    @Override
    public void removedService(ServiceReference<Object> reference, Object service)
    {
        super.removedService(reference, service);
        lock.removedFactory(service);
    }

    <T> T getLogger(Bundle bundle, String name, Class<T> type)
    {
        return type.cast(lock.getLogDomain(bundle).getLogger(name));
    }

    @SuppressWarnings("resource")
    @Override
    public void bundleChanged(BundleEvent event)
    {
        if (event.getType() == BundleEvent.STOPPED && !closed.get())
        {
            LogDomain domain = lock.remove(event.getBundle());
            if (domain != null)
            {
                domain.close();
            }
        }
    }

    /*
     * Tracks a bundle's LoggerFactory service
     */
    class LogDomain implements Closeable
    {

        private final Bundle bundle;
        private final Set<LoggerFacade> facades = new HashSet<>();

        LogDomain(Bundle bundle)
        {
            this.bundle = bundle;
        }

        private void reset()
        {
            synchronized (facades)
            {
                for (LoggerFacade facade : facades)
                {
                    facade.reset();
                }
            }
        }

        LoggerFacade getLogger(String name)
        {
            LoggerFacade facade = createLoggerFacade(this, name);
            synchronized (facades)
            {
                facades.add(facade);
            }
            return facade;
        }

        @Override
        public void close()
        {
            reset();
        }

    }

    class LoggerFacade
    {
        private final String name;
        private final LogDomain domain;
        volatile Object logger;
        volatile String prefix;

        LoggerFacade(LogDomain logDomain, String name)
        {
            this.domain = logDomain;
            this.name = name;
        }

        void reset()
        {
            logger = null;
        }

        Object getLogger()
        {
            Object l = this.logger;
            if (l == null)
            {
                l = lock.getLogger(this, domain.bundle, name);
            }
            return l;
        }

        Bundle getBundle()
        {
            return domain.bundle;
        }

        String getName()
        {
            return name;
        }

    }

    public void close()
    {
        if (closed.compareAndSet(false, true))
        {
            lock.close();
            super.close();
            this.systemContext.removeBundleListener(this);
        }
    }

    LoggerFacade createLoggerFacade(LogDomain logDomain, String name)
    {
        assert !closed.get();
        return new LoggerFacade(logDomain, name);
    }

}
