/*
 * 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.dm.itest.api;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.itest.util.Ensure;
import org.apache.felix.dm.itest.util.TestBase;
import org.junit.Assert;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;

/**
 * This class validates that some aspect aware services are correctly managed and ordered when components and aspects are 
 * registered concurrently.
 * 
 * By default, this class uses a custom threadpool, but a subclass may override this class and call "setParallel()" method, in 
 * this case we won't use any threadpool, since calling setParallel() method means we are using a parallel Dependency Manager.
 * 
 * @see AspectRaceParallelTest
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class AspectRaceTest extends TestBase {
    final static int SERVICES = 3;
    final static int ASPECTS_PER_SERVICE = 10;
    final static int ITERATIONS = 1000;
    final AtomicInteger m_IDGenerator = new AtomicInteger();

    public void testConcurrentAspects() {
        try {
            warn("starting aspect race test");
            initThreadPool(); // only if setParallel() has not been called (only if a parallel DM is not used).

            for (int loop = 1; loop <= ITERATIONS; loop++) {
                // Perform concurrent injections of "S" service and S aspects into the Controller component;
                debug("Iteration: " + loop);
                
                // Use a helper class to wait for components to be started/stopped.
                int count = 1 /* for controller */ + SERVICES + (SERVICES * ASPECTS_PER_SERVICE);
                ComponentTracker tracker = new ComponentTracker(count, count);
                
                // Create the components (controller / services / aspects)
                Controller controller = new Controller();
                Factory f = new Factory();
                f.createComponents(controller, tracker);
                
                // Activate the components asynchronously
                f.registerComponents();
                
                // Wait for the components to be started (using the tracker)
                if (!tracker.awaitStarted(5000)) {
                    throw new IllegalStateException("Could not start components timely.");
                }
                
                // Check aspect chains consistency.
                controller.checkConsistency();
                
                // unregister all services and aspects.
                f.unregisterComponents();
                
                // use component tracker to wait for all components to be stopped.
                if (!tracker.awaitStopped(5000)) {
                    throw new IllegalStateException("Could not stop components timely.");
                }
                
                m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS);

                if ((loop) % 50 == 0) {
                    warn("Performed " + loop + " tests.");
                }

                if (super.errorsLogged()) {
                    throw new IllegalStateException("Race test interrupted (some error occured, see previous logs)");
                }
            }
        }

        catch (Throwable t) {
            error("Test failed", t);
            Assert.fail("Test failed: " + t.getMessage());
        } finally {
            m_dm.clear();
            shutdownThreadPool();
        }
    }

    private void initThreadPool() {
        // Create a threadpool only if setParallel() method has not been called.
        if (! m_parallel) {
            int cores = Math.max(16, Runtime.getRuntime().availableProcessors());
            m_threadPool = new ForkJoinPool(cores);
        }
    }
    
    void shutdownThreadPool() {
        if (! m_parallel && m_threadPool != null) {
            m_threadPool.shutdown();
            try {
                m_threadPool.awaitTermination(60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
            }
        }
    }

    public interface S {
        void invoke(Ensure e);

        int getRank();
    }

    public static class SImpl implements S {

        SImpl() {
        }

        public void invoke(Ensure e) {
            e.step(1);
        }

        public String toString() {
            return "SImpl";
        }

        @Override
        public int getRank() {
            return Integer.MIN_VALUE;
        }
    }

    public class SAspect implements S {
        volatile S m_next;
        final int m_rank;
        volatile Component m_component;

        SAspect(int rank) {
            m_rank = rank;
        }

        public synchronized void added(S s) {
            debug("aspect.added: this rank=%d, next rank=%d", getRank(), s.getRank());
            m_next = s;
        }

        public synchronized void swap(S oldS, S newS) {
            debug("aspect.swap: this rank=%d, old rank=%d, next rank=%d", getRank(), oldS.getRank(), newS.getRank());
            m_next = newS;
        }

        public synchronized void removed(S s) {
            debug("aspect.remove: this rank=%d, removed rank=%d", getRank(), s.getRank());
            m_next = null;
        }

        public synchronized void invoke(Ensure e) {
            debug("aspect.invoke: this rank=%d, next rank=%d", this.getRank(), m_next.getRank());
            Assert.assertTrue(m_rank > m_next.getRank());
            m_next.invoke(e);
        }

        public String toString() {
            return "[Aspect/rank=" + m_rank + "], next="
                + ((m_next != null) ? m_next : "null");
        }

        @Override
        public int getRank() {
            return m_rank;
        }
    }

    class Factory {
        int m_serviceId;
        Component m_controller;
        final ConcurrentLinkedQueue<Component> m_services = new ConcurrentLinkedQueue<Component>();
        final ConcurrentLinkedQueue<Component> m_aspects = new ConcurrentLinkedQueue<Component>();   
        
        private void createComponents(Controller controller, ComponentTracker tracker) {
            // create the controller
            int controllerID = m_IDGenerator.incrementAndGet();
            m_controller = m_dm.createComponent()
                .setImplementation(controller)
                .setComposition("getComposition")
                .add(tracker);
            for (int i = 0; i < SERVICES; i ++) {
                m_controller.add(m_dm.createServiceDependency()
                    .setService(S.class, "(controller.id=" + controllerID + ")")
                    .setCallbacks("bind", null, "unbind", "swap")
                    .setRequired(true));
            }
            
            // create the services
            for (int i = 1; i <= SERVICES; i++) {
                int aspectId = m_IDGenerator.incrementAndGet();
                Component s = m_dm.createComponent();
                Hashtable<String, String> props = new Hashtable<String, String>();
                props.put("controller.id", String.valueOf(controllerID));
                props.put("aspect.id", String.valueOf(aspectId));
                s.setInterface(S.class.getName(), props)
                 .setImplementation(new SImpl());
                s.add(tracker);
                m_services.add(s);
                
                // create the aspects for that service
                for (int j = 1; j <= ASPECTS_PER_SERVICE; j++) {
                    final int rank = j;
                    SAspect sa = new SAspect(rank);
                    Component a = 
                        m_dm.createAspectService(S.class, "(aspect.id=" + aspectId + ")", rank, "added", null, "removed", "swap")
                            .setImplementation(sa);
                    a.add(tracker);
                    m_aspects.add(a);
                }
            }
        }

        public void registerComponents() {
            // If setParallel() has been called (we are using a parallel dependency manager), then no needs to use a custom thread pool.
            if (m_parallel) { // using a parallel DM.
                for (final Component s : m_services) {
                    m_dm.add(s);
                }
                m_dm.add(m_controller);
                for (final Component a : m_aspects) {
                    m_dm.add(a);
                }
            } else {
                for (final Component s : m_services) {
                    m_threadPool.execute(new Runnable() {
                        public void run() {
                            m_dm.add(s);
                        }
                    });
                }
                m_threadPool.execute(new Runnable() {
                    public void run() {
                        m_dm.add(m_controller);
                    }
                });
                for (final Component a : m_aspects) {
                    m_threadPool.execute(new Runnable() {
                        public void run() {
                            m_dm.add(a);
                        }
                    });
                }
            }
        }

        public void unregisterComponents() throws InterruptedException, InvalidSyntaxException {        
            m_dm.remove(m_controller);
            for (final Component s : m_services) {
                m_dm.remove(s);
            }
            for (final Component a : m_aspects) {
                m_dm.remove(a);
            }
        }
    }

    public class Controller {
        final Composition m_compo = new Composition();
        final HashSet<S> m_services = new HashSet<S>();

        Object[] getComposition() {
            return new Object[] { this, m_compo };
        }

        synchronized void bind(ServiceReference sr, Object service) {
            debug("controller.bind: %s", service);
            S s = (S) service;
            m_services.add(s);
            debug("bind: service count after bind: %d", m_services.size());
        }

        synchronized void swap(S previous, S current) {
            debug("controller.swap: previous=%s, current=%s", previous, current);
            if (!m_services.remove(previous)) {
                debug("swap: unknow previous service: " + previous);
            }
            m_services.add(current);
            debug("controller.swap: service count after swap: %d", m_services.size());
        }

        synchronized void unbind(S a) {
            debug("unbind " + a);
            m_services.remove(a);
        }
        
        synchronized void checkConsistency() {
            debug("service count: %d", m_services.size());
            for (S s : m_services) {
                info("checking service: %s", s);
                Ensure ensure = new Ensure(false);
                s.invoke(ensure);
            }
        }
    }

    public static class Composition {
    }
}
