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

import org.osgi.framework.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;

/**
 * This class listens to the global Apache Unomi bundle lifecycle, to provide statistics and state of the overall
 * system. It notably displays messages for successfull or unsuccessfull startups as well as startup times.
 */
public class BundleWatcher implements SynchronousBundleListener, ServiceListener {

    private static final Logger logger = LoggerFactory.getLogger(BundleWatcher.class.getName());

    private long startupTime;
    private Map<String,Long> bundleStartupTimes = new LinkedHashMap<>();
    private long unomiStartedBundleCount = 0;
    private long requiredStartedBundleCount;

    private String requiredServices;
    private Set<Filter> requiredServicesFilters = new LinkedHashSet<>();
    private long matchedRequiredServicesCount = 0;

    private BundleContext bundleContext;
    private boolean startupMessageAlreadyDisplayed = false;
    private boolean shutdownMessageAlreadyDisplayed = false;
    private List<String> logoLines = new ArrayList<>();

    public void setRequiredStartedBundleCount(long requiredStartedBundleCount) {
        this.requiredStartedBundleCount = requiredStartedBundleCount;
    }

    public void setRequiredServices(String requiredServices) {
        this.requiredServices = requiredServices;
        String[] requiredServiceArray = requiredServices.split(",");
        requiredServicesFilters.clear();
        for (String requiredService : requiredServiceArray) {
            try {
                requiredServicesFilters.add(bundleContext.createFilter(requiredService.trim()));
            } catch (InvalidSyntaxException e) {
                logger.error("Error creating require service filter {}", requiredService.trim(), e);
            }
        }
    }

    public void setBundleContext(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
    }

    public void init() {
        bundleContext.addBundleListener(this);
        bundleContext.addServiceListener(this);
        loadLogo();
        startupTime = System.currentTimeMillis();
        System.out.println("Initializing Unomi...");
        logger.info("Bundle watcher initialized.");
    }

    public void destroy() {
        bundleContext.removeServiceListener(this);
        bundleContext.removeBundleListener(this);
        logger.info("Bundle watcher shutdown.");
    }

    @Override
    public void bundleChanged(BundleEvent event) {
        switch (event.getType()) {
            case BundleEvent.STARTING:
                break;
            case BundleEvent.STARTED:
                if (event.getBundle().getSymbolicName().startsWith("org.apache.unomi")) {
                    unomiStartedBundleCount++;
                    checkStartupComplete();
                }
                break;
            case BundleEvent.STOPPING:
                break;
            case BundleEvent.STOPPED:
                if (event.getBundle().getSymbolicName().startsWith("org.apache.unomi")) {
                    unomiStartedBundleCount--;
                }
                break;
        }
    }

    @Override
    public void serviceChanged(ServiceEvent event) {
        ServiceReference serviceReference = event.getServiceReference();
        if (serviceReference == null) {
            return;
        }
        switch (event.getType()) {
            case ServiceEvent.REGISTERED:
                addStartedService(serviceReference);
                checkStartupComplete();
                break;
            case ServiceEvent.MODIFIED:
                break;
            case ServiceEvent.UNREGISTERING:
                removeStartedService(serviceReference);
                break;
        }
    }

    private void addStartedService(ServiceReference serviceReference) {
        for (Filter requiredService : requiredServicesFilters) {
            if (requiredService.match(serviceReference)) {
                matchedRequiredServicesCount++;
            }
        }
    }

    private void removeStartedService(ServiceReference serviceReference) {
        for (Filter requiredService : requiredServicesFilters) {
            if (requiredService.match(serviceReference)) {
                matchedRequiredServicesCount--;
                if (!shutdownMessageAlreadyDisplayed) {
                    System.out.println("Apache Unomi shutting down...");
                    logger.info("Apache Unomi no longer available, as required service {} is shutdown !", serviceReference);
                    shutdownMessageAlreadyDisplayed = true;
                }
                startupMessageAlreadyDisplayed = false;
            }
        }
    }

    private void checkStartupComplete() {
        if (!isStartupComplete()) {
            return;
        }
        if (!startupMessageAlreadyDisplayed) {
            long totalStartupTime = System.currentTimeMillis() - startupTime;
            if (logoLines.size() > 0) {
                for (String logoLine : logoLines) {
                    System.out.println(logoLine);
                }
            }
            String buildNumber = "n/a";
            if (bundleContext.getBundle().getHeaders().get("Implementation-Build") != null) {
                buildNumber = bundleContext.getBundle().getHeaders().get("Implementation-Build");
            }
            String timestamp = "n/a";
            if (bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp") != null) {
                timestamp = bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp");
            }
            String versionMessage = "  " + bundleContext.getBundle().getVersion().toString() + "  Build:" + buildNumber + "  Timestamp:" + timestamp;
            System.out.println(versionMessage);
            System.out.println("--------------------------------------------------------------------------");
            System.out.println("Successfully started " + unomiStartedBundleCount + " bundles and " + matchedRequiredServicesCount + " required services in " + totalStartupTime + " ms");
            logger.info("Apache Unomi version: " + versionMessage);
            logger.info("Apache Unomi successfully started {} bundles and {} required services in {} ms", unomiStartedBundleCount, matchedRequiredServicesCount, totalStartupTime);
            startupMessageAlreadyDisplayed = true;
            shutdownMessageAlreadyDisplayed = false;
        }
    }

    public boolean isStartupComplete() {
        if (unomiStartedBundleCount < requiredStartedBundleCount) {
            return false;
        }
        if (matchedRequiredServicesCount < requiredServicesFilters.size()) {
            return false;
        }
        return true;
    }

    private void loadLogo() {
        URL logoURL = bundleContext.getBundle().getResource("logo.txt");
        if (logoURL != null) {
            BufferedReader bufferedReader = null;
            try {
                bufferedReader = new BufferedReader(new InputStreamReader(logoURL.openStream()));
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    if (!line.trim().startsWith("#")) {
                        logoLines.add(line);
                    }
                }
            } catch (IOException e) {
                logger.error("Error loading logo lines", e);
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException e) {
                    }
                }
            }
        }
    }

}
