/*
 * 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.karaf.bundle.command;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.Option;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.wiring.FrameworkWiring;

@Command(scope = "bundle", name = "load-test", description = "Load test bundle lifecycle")
@Service
public class LoadTest implements Action {

    @Option(name = "--threads", description = "number of concurrent threads")
    int threads = 2;

    @Option(name = "--delay", description = "maximum delay between actions")
    int delay = 1;

    @Option(name = "--iterations", description = "number of iterations per thread")
    int iterations = 100;

    @Option(name = "--refresh", description = "percentage of bundle refresh vs restart")
    int refresh = 20;

    @Option(name = "--excludes", description = "List of bundles (ids or symbolic names) to exclude")
    List<String> excludes = Arrays.asList("0", "org.ops4j.pax.url.mvn", "org.ops4j.pax.logging.pax-logging-api", "org.ops4j.pax.logging.pax-logging-service");

    @Reference
    Session session;

    @Reference
    BundleContext bundleContext;

    @Override
    public Object execute() throws Exception {
        if (!confirm(session)) {
            return null;
        }
        final BundleContext bundleContext = this.bundleContext.getBundle(0).getBundleContext();
        final FrameworkWiring wiring = bundleContext.getBundle().adapt(FrameworkWiring.class);
        final CountDownLatch latch = new CountDownLatch(threads);
        final Bundle[] bundles = bundleContext.getBundles();
        final AtomicBoolean[] locks = new AtomicBoolean[bundles.length];
        for (int b = 0; b < locks.length; b++) {
            locks[b] = new AtomicBoolean(true);
            // Avoid touching excluded bundles
            if (excludes.contains(Long.toString(bundles[b].getBundleId()))
                    || excludes.contains(bundles[b].getSymbolicName())) {
                continue;
            }
            // Only touch active bundles
            if (bundles[b].getState() != Bundle.ACTIVE) {
                continue;
            }
            // Now set the lock to available
            locks[b].set(false);
        }
        for (int i = 0; i < threads; i++) {
            new Thread() {
                public void run() {
                    try {
                        Random rand = new Random();
                        for (int j = 0; j < iterations; j++) {
                            for (;;) {
                                int b = rand.nextInt(bundles.length);
                                if (locks[b].compareAndSet(false, true)) {
                                    try {
                                        // Only touch active bundles
                                        if (bundles[b].getState() != Bundle.ACTIVE) {
                                            continue;
                                        }
                                        if (rand.nextInt(100) < refresh) {
                                            try {
                                                bundles[b].update();
                                                final CountDownLatch latch = new CountDownLatch(1);
                                                wiring.refreshBundles(Collections.singletonList(bundles[b]), new FrameworkListener() {
                                                    public void frameworkEvent(FrameworkEvent event) {
                                                        latch.countDown();
                                                    }
                                                });
                                                latch.await();
                                            } finally {
                                                while (true) {
                                                    try {
                                                        bundles[b].start(Bundle.START_TRANSIENT);
                                                        break;
                                                    } catch (Exception e) {
                                                        Thread.sleep(1);
                                                    }
                                                }
                                            }
                                        } else {
                                            try {
                                                bundles[b].stop(Bundle.STOP_TRANSIENT);
                                            } finally {
                                                while (true) {
                                                    try {
                                                        bundles[b].start(Bundle.START_TRANSIENT);
                                                        break;
                                                    } catch (Exception e) {
                                                        Thread.sleep(1);
                                                    }
                                                }
                                            }
                                        }
                                        Thread.sleep(rand.nextInt(delay));
                                    } catch (Exception e) {
                                        boolean ignore = false;
                                        if (e instanceof BundleException && e.getMessage() != null) {
                                            String msg = e.getMessage();
                                            if ("Cannot acquire global lock to update the bundle.".equals(msg) ||
                                                    "Unable to acquire global lock for resolve.".equals(msg) ||
                                                    msg.matches("Bundle .* cannot be update, since it is either starting or stopping.")) {
                                                ignore = true;
                                            }
                                        }
                                        if (!ignore) {
                                            e.printStackTrace();
                                        }
                                    } finally {
                                        locks[b].set(false);
                                    }
                                }
                                break;
                            }
                        }
                    } catch (Throwable t) {
                        t.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                }
            }.start();
        }
        new Thread() {
            @Override
            public void run() {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println("Load test finished");
            }
        }.start();
        return null;
    }

    private boolean confirm(Session session) throws IOException {
        for (;;) {
            String msg = "You are about to perform a start/stop/refresh load test on bundles.\nDo you wish to continue (yes/no): ";
            String str = session.readLine(msg, null);
            if ("yes".equalsIgnoreCase(str)) {
                return true;
            }
            if ("no".equalsIgnoreCase(str)) {
                return false;
            }
        }
    }
}
