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

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.ComponentDeclaration;
import org.apache.felix.dm.ComponentDependencyDeclaration;
import org.apache.felix.dm.DependencyManager;
import org.apache.felix.dm.diagnostics.CircularDependency;
import org.apache.felix.dm.diagnostics.DependencyGraph;
import org.apache.felix.dm.diagnostics.DependencyGraph.ComponentState;
import org.apache.felix.dm.diagnostics.DependencyGraph.DependencyState;
import org.apache.felix.dm.diagnostics.MissingDependency;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Descriptor;
import org.apache.felix.service.command.Parameter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;

/**
 * Shell command for showing all services and dependencies that are managed
 * by the dependency manager.
 * 
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
@Descriptor("Commands used to dump all existing Dependency Manager components")
public class DMCommand {
    /**
     * Bundle context used to create OSGi filters.
     */
    private final BundleContext m_context;
        
    /**
     * Comparator used to compare component declarations based on their bundle ids
     */
    private static final ComponentDeclarationComparator COMPONENT_DECLARATION_COMPARATOR = new ComponentDeclarationComparator();

    /**
     * Constant used by the wtf command, when listing missing services.
     */
    private static final String SERVICE = "service";
    
    /**
     * Constant used by the wtf command, when listing missing configurations.
     */
    private static final String CONFIGURATION = "configuration";
    
    /**
     * Constant used by the wtf command, when listing missing resource dependencies
     */
    private static final String RESOURCE = "resource";
    
    /**
     * Constant used by the wtf command, when listing missing bundle dependencies
     */
    private static final String BUNDLE = "bundle";

    /**
     * Name of a specific gogo shell variable, which may be used to configure "compact" mode.
     * Example: g! dependencymanager.compact=true
     */
    private final static String ENV_COMPACT = "dependencymanager.compact";
    
    /**
     * Name of a specific gogo shell variable, which may be used to configure an OSGi filter, normally
     * passed to the "dm services" option. It is used to display only some service providing components
     * matching the given filter. The filter can contain an "objectClass" option.
     * Example: 
     *   g! dependencymanager.services="(protocol=http)"
     *   g! dependencymanager.services="(&(objectClass=foo.Bar)(protocol=http))"
     */
    private final static String ENV_SERVICES = "dependencymanager.services";

    /**
     * Name of a specific gogo shell variable, which may be used to configure a filter on the
     * component implementation class name.
     * The value of this shell variable may contain multiple regex (space separated), and each regex can
     * be negated using "!".
     * Example: g! dependencymanager.components="foo.bar.* ga.bu.zo.*"
     */
    private final static String ENV_COMPONENTS = "dependencymanager.components";
        
    /**
     * Constructor.
     */
    public DMCommand(BundleContext context) {
        m_context = context;
    }

    /**
     * Dependency Manager "dm" command. We use gogo annotations, in order to automate documentation,
     * and also to automatically manage optional flags/options and parameters ordering.
     * 
     * @param session the gogo command session, used to get some variables declared in the shell
     *        This parameter is automatically passed by the gogo runtime.
     * @param nodeps false means that dependencies are not displayed
     * @param compact true means informations are displayed in a compact format. This parameter can also be 
     *        set using the "dependencymanager.compact" gogo shell variable.
     * @param notavail only unregistered components / unavailable dependencies are displayed 
     * @param stats true means some statistics are displayed
     * @param services an osgi filter used to filter on some given osgi service properties.  This parameter can also be 
     *        set using the "dependencymanager.services" gogo shell variable.
     * @param components a regular expression to match either component implementation class names.  This parameter can also be 
     *        set using the "dependencymanager.components" gogo shell variable.
     * @param componentIds only components matching one of the specified components ids are displayed
     * @param bundleIds a list of bundle ids or symbolic names, used to filter on some given bundles
     */
    @Descriptor("List dependency manager components")
    public void dm(
            CommandSession session,

            @Descriptor("Hides component dependencies") 
            @Parameter(names = {"nodeps", "nd"}, presentValue = "true", absentValue = "false") 
            boolean nodeps,

            @Descriptor("Displays components using a compact form") 
            @Parameter(names = {"compact", "cp"}, presentValue = "true", absentValue = "") 
            String compact,
            
            @Descriptor("Only displays unavailable components") 
            @Parameter(names = {"notavail", "na"}, presentValue = "true", absentValue = "false") 
            boolean notavail,

            @Descriptor("Detects where are the root failures") 
            @Parameter(names = {"wtf"}, presentValue = "true", absentValue = "false") 
            boolean wtf,

            @Descriptor("Displays components statistics") 
            @Parameter(names = {"stats", "stat", "st"}, presentValue = "true", absentValue = "false") 
            boolean stats,

            @Descriptor("<OSGi filter used to filter some service properties>") 
            @Parameter(names = {"services", "s"}, absentValue = "") 
            String services,

            @Descriptor("<Regex(s) used to filter on component implementation class names (comma separated), can be negated using \"!\" prefix>") 
            @Parameter(names = {"components", "c"}, absentValue = "") 
            String components,
            
            @Descriptor("<List of component identifiers to display (comma separated)>") 
            @Parameter(names = {"componentIds", "cid", "ci"}, absentValue = "") 
            String componentIds,

            @Descriptor("<List of bundle ids or bundle symbolic names to display (comma separated)>") 
            @Parameter(names = {"bundleIds", "bid", "bi", "b"}, absentValue = "") 
            String bundleIds,
            
            @Descriptor("<Max number of top components to display (0=all)> This command displays components callbacks (init/start) times>") 
            @Parameter(names = {"top"}, absentValue = "-1") 
            int top) throws Throwable
        {
        
        boolean comp = Boolean.parseBoolean(getParam(session, ENV_COMPACT, compact));
        services = getParam(session, ENV_SERVICES, services);
        String[] componentsRegex = getParams(session, ENV_COMPONENTS, components);
        ArrayList<String> bids = new ArrayList<String>(); // list of bundle ids or bundle symbolic names
        ArrayList<Long> cids = new ArrayList<Long>(); // list of component ids
        
        // Parse and check componentIds option
        StringTokenizer tok = new StringTokenizer(componentIds, ", ");
        while (tok.hasMoreTokens()) {
            try {
                cids.add(Long.parseLong(tok.nextToken()));
            } catch (NumberFormatException e) {
                System.out.println("Invalid value for componentIds option");
                return;
            }
        }

        // Parse services filter
        Filter servicesFilter = null;
        try {
            if (services != null) {
                servicesFilter = m_context.createFilter(services);
            }
        } catch (InvalidSyntaxException e) {
            System.out.println("Invalid services OSGi filter: " + services);
            e.printStackTrace(System.err);
            return;
        }

        // Parse and check bundleIds option
        tok = new StringTokenizer(bundleIds, ", ");
        while (tok.hasMoreTokens()) {
            bids.add(tok.nextToken());
        }
        
        if (top != -1) {
            showTopComponents(top);
            return;
        }
        
        if (wtf) {
            wtf();
            return;
        }
        
        DependencyGraph graph = null;
        if(notavail) {
        	graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.ALL_UNAVAILABLE);
        } else {
        	graph = DependencyGraph.getGraph(ComponentState.ALL, DependencyState.ALL);
        }
        
        List<ComponentDeclaration> allComponents = graph.getAllComponents();
        Collections.sort(allComponents, COMPONENT_DECLARATION_COMPARATOR);
        long numberOfComponents = 0;
        long numberOfDependencies = 0;
        long lastBundleId = -1;
        
        for(ComponentDeclaration cd : allComponents) {
        	Bundle bundle = cd.getBundleContext().getBundle();
        	if(!matchBundle(bundle, bids)) {
        		continue;
        	}
        	
        	Component component = (Component)cd;
        	String name = cd.getName();
        	if (!mayDisplay(component, servicesFilter, componentsRegex, cids)) {
                continue;
            }
        	
        	numberOfComponents++;
    		long bundleId = bundle.getBundleId();
    		if(lastBundleId != bundleId) {
    			lastBundleId = bundleId;
    			if (comp) {
                    System.out.println("[" + bundleId + "] " + compactName(bundle.getSymbolicName()));
                } else {
                    System.out.println("[" + bundleId + "] " + bundle.getSymbolicName());
                }
    		}
    		if (comp) {
                System.out.print(" [" + cd.getId() + "] " + compactName(name) + " "
                        + compactState(ComponentDeclaration.STATE_NAMES[cd.getState()]));
            } else {
                System.out.println(" [" + cd.getId() + "] " + name + " "
                        + ComponentDeclaration.STATE_NAMES[cd.getState()]);
            }
    		
    		if(!nodeps) {
    			List<ComponentDependencyDeclaration> dependencies = graph.getDependecies(cd);
    			if(!dependencies.isEmpty()) {
    				numberOfDependencies += dependencies.size();
    				if (comp) {
                        System.out.print('(');
                    }
    				for(int j = 0; j < dependencies.size(); j ++) {
        				ComponentDependencyDeclaration dep = dependencies.get(j);
        				
        				String depName = dep.getName();
                        String depType = dep.getType();
                        int depState = dep.getState();

                        if (comp) {
                            if (j > 0) {
                                System.out.print(' ');
                            }
                            System.out.print(compactName(depName) + " " + compactState(depType) + " "
                                    + compactState(ComponentDependencyDeclaration.STATE_NAMES[depState]));
                        } else {
                            System.out.println("    " + depName + " " + depType + " "
                                    + ComponentDependencyDeclaration.STATE_NAMES[depState]);
                        }

        			}
                    if (comp) {
                        System.out.print(')');
                    }
    			}
    		}
    		if (comp) {
                System.out.println();
            }
        }
        
        if(stats) {
        	System.out.println("Statistics:");
        	System.out.println(" - Dependency managers: " + DependencyManager.getDependencyManagers().size());
        	System.out.println(" - Components: " + numberOfComponents);
            if (!nodeps) {
                System.out.println(" - Dependencies: " + numberOfDependencies);
            }
        }

        }

    /**
     * Displays components callbacks (init/start/stop/destroy) elapsed time.
     * The components are sorted (the most time consuming components are displayed first).
     * @param max the max number of components to display (0 means all components)
     */
    private void showTopComponents(int max) {
        List<Component> components = new ArrayList<>();
        for (DependencyManager manager : DependencyManager.getDependencyManagers()) {
           components.addAll(manager.getComponents()); 
        }
        Collections.sort(components, new Comparator<Component>() {
            @Override
            public int compare(Component c1, Component c2) {
                Map<String, Long> c1Times = c1.getComponentDeclaration().getCallbacksTime();
                Map<String, Long> c2Times = c2.getComponentDeclaration().getCallbacksTime();
                Long c1Start = c1Times.get("start");
                Long c2Start = c2Times.get("start");
                if (c1Start != null) {
                    if (c2Start != null) {
                        return c1Start > c2Start ? 1 : -1;
                    } else {
                        return 1;
                    }
                } else {
                    if (c2Start != null) {
                        return -1;
                    } else {
                        return 0;
                    }
                }
            }
        });
        
        Collections.reverse(components);

        System.out.printf("%-100s %10s %10s%n%n", "Top components (sorted by start duration time)", "[init time]", "[start time]");
        
        if (components.size() > 0) {
            System.out.println();
            
            max = max == 0 ? components.size() : Math.min(components.size(), max);
            for (int i = 0 ; i < components.size()  && i < max; i++) {
                ComponentDeclaration decl = components.get(i).getComponentDeclaration();
                System.out.printf("%-100s %10d %10d%n", decl.getClassName(),
                    decl.getCallbacksTime().get("init"), decl.getCallbacksTime().get("start"));
            }
        }
    }

    private boolean matchBundle(Bundle bundle, List<String> ids) {
        if (ids.size() == 0) {
            return true;
        }
        
        for (int i = 0; i < ids.size(); i ++) {
            String id = ids.get(i);
            try {
                Long longId = Long.valueOf(id);
                if (longId == bundle.getBundleId()) {
                    return true;
                }                
            } catch (NumberFormatException e) {
                // must match symbolic name
                if (id.equals(bundle.getSymbolicName())) {
                    return true;
                }
            }
        }
        
        return false;
    }

    /**
     * Returns the value of a command arg parameter, or from the gogo shell if the parameter is not passed to
     * the command.
     */
    private String getParam(CommandSession session, String param, String value) {
        if (value != null && value.length() > 0) {
            return value;
        }
        Object shellParamValue = session.get(param);
        return shellParamValue != null ? shellParamValue.toString() : null;
    }

    /**
     * Returns the value of a command arg parameter, or from the gogo shell if the parameter is not passed to
     * the command. The parameter value is meant to be a list of values separated by a blank or a comma. 
     * The values are split and returned as an array.
     */
    private String[] getParams(CommandSession session, String name, String value) {
        String values = null;
        if (value == null || value.length() == 0) {
            value = (String) session.get(name);
            if (value != null) {
                values = value;
            }
        } else {
            values = value;
        }
        if (values == null) {
            return new String[0];
        }      
        return values.trim().split(", ");
    }

    /**
     * Checks if a component can be displayed. We make a logical OR between the three following conditions:
     * 
     *  - the component service properties are matching a given service filter ("services" option)
     *  - the component implementation class name is matching some regex ("components" option)
     *  - the component declaration name is matching some regex ("names" option)
     *  
     *  If some component ids are provided, then the component must also match one of them.
     */
    private boolean mayDisplay(Component component, Filter servicesFilter, String[] components, List<Long> componentIds) {   
        // Check component id
        if (componentIds.size() > 0) {
            long componentId = ((ComponentDeclaration) component).getId();
            if (componentIds.indexOf(componentId) == -1) {
                return false;
            }
        }
        
        if (servicesFilter == null && components.length == 0) {
            return true;
        }     
        
        // Check component service properties
        boolean servicesMatches = servicesMatches(component, servicesFilter);        

        // Check components regexs, which may match component implementation class name
        boolean componentsMatches = componentMatches(((ComponentDeclaration) component).getClassName(), components);

        // Logical OR between service properties match and component service/impl match.
        return servicesMatches || componentsMatches;   
    }

    /**
     * Checks if a given filter is matching some service properties possibly provided by a component
     */
    private boolean servicesMatches(Component component, Filter servicesFilter) {
        boolean match = false;
        if (servicesFilter != null) {
            String[] services = ((ComponentDeclaration) component).getServices();
            if (services != null) {
                Dictionary<String, Object> properties = component.getServiceProperties();
                if (properties == null) {
                    properties = new Hashtable<String, Object>();
                }
                if (properties.get(Constants.OBJECTCLASS) == null) {
                    properties.put(Constants.OBJECTCLASS, services);
                }
                match = servicesFilter.match(properties);
            }
        }
        return match;
    }

    /**
     * Checks if the component implementation class name (or some possible provided services) are matching
     * some regular expressions.
     */
    private boolean componentMatches(String description, String[] names) {
        for (int i = 0; i < names.length; i ++) {
            String name = names[i];
            boolean not = false;
            if (name.startsWith("!")) {
                name = name.substring(1);
                not = true;
            }
            boolean match = false;

            if (description.matches(name)) {
                match = true;
            }
                       
            if (not) {
                match = !match;
            }
            
            if (match) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Compact names that look like state strings. State strings consist of
     * one or more words. Each word will be shortened to the first letter,
     * all letters concatenated and uppercased.
     */
    private String compactState(String input) {
        StringBuffer output = new StringBuffer();
        StringTokenizer st = new StringTokenizer(input);
        while (st.hasMoreTokens()) {
            output.append(st.nextToken().toUpperCase().charAt(0));
        }
        return output.toString();
    }

    /**
     * Compacts names that look like fully qualified class names. All packages
     * will be shortened to the first letter, except for the last one. So
     * something like "org.apache.felix.MyClass" will become "o.a.f.MyClass".
     */
    private String compactName(String input) {
        StringBuffer output = new StringBuffer();
        int lastIndex = 0;
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            switch (c) {
                case '.' :
                    output.append(input.charAt(lastIndex));
                    output.append('.');
                    lastIndex = i + 1;
                    break;
                case ' ' :
                case ',' :
                    if (lastIndex < i) {
                        output.append(input.substring(lastIndex, i));
                    }
                    output.append(c);
                    lastIndex = i + 1;
                    break;
                default:
            }
        }
        if (lastIndex < input.length()) {
            output.append(input.substring(lastIndex));
        }
        return output.toString();
    }

    public void wtf() {
    	
    	DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE);
    	List<ComponentDeclaration> unregisteredComponents = graph.getAllComponents();
    	
    	if(unregisteredComponents.isEmpty()) {
    		System.out.println("No unregistered components found");
    	} else {
    		String message = unregisteredComponents.size() + " unregistered components found";
			System.out.println(message);
            System.out.println("----------------------------------------------------".substring(0, message.length()));
    	}
    	
    	listResolvedBundles();
    	listInstalledBundles();
    	
    	List<CircularDependency> circularDependencies = graph.getCircularDependencies();
    	if(!circularDependencies.isEmpty()) {
    		System.out.println("Circular dependencies:");
    		printCircularDependencies(circularDependencies);
    	}
    	
    	List<MissingDependency> missingConfigDependencies = graph.getMissingDependencies(CONFIGURATION);
    	if(!missingConfigDependencies.isEmpty()) {
    		System.out.println("The following configuration(s) are missing: ");
    		printMissingDependencies(missingConfigDependencies);
    	}
    	
    	List<MissingDependency> missingServiceDependencies = graph.getMissingDependencies(SERVICE);
    	if(!missingServiceDependencies.isEmpty()) {
    		System.out.println("The following service(s) are missing: ");
    		printMissingDependencies(missingServiceDependencies);
    	}

    	
    	List<MissingDependency> missingResourceDependencies = graph.getMissingDependencies(RESOURCE);
    	if(!missingResourceDependencies.isEmpty()) {
    		System.out.println("The following resource(s) are missing: ");
    		printMissingDependencies(missingResourceDependencies);
    	}
    	
    	List<MissingDependency> missingBundleDependencies = graph.getMissingDependencies(BUNDLE);
    	if(!missingBundleDependencies.isEmpty()) {
    		System.out.println("The following bundle(s) are missing: ");
    		printMissingDependencies(missingBundleDependencies);
    	}
    	
    	List<MissingDependency> missingCustomDependencies = graph.getMissingCustomDependencies();
    	if(!missingCustomDependencies.isEmpty()) {
    		System.out.println("The following custom dependency(ies) are missing: ");
    		printMissingCustomDependencies(missingCustomDependencies);
    	}
    }

	private void printCircularDependencies(List<CircularDependency> circularDependencies) {
		for(CircularDependency c : circularDependencies) {
			System.out.print(" *");
			for(ComponentDeclaration cd : c.getComponents()) {
				System.out.print(" -> " + cd.getName());
			}
			System.out.println();
		}
	}

	private void printMissingDependencies(List<MissingDependency> missingDependencies) {
		for(MissingDependency m : missingDependencies) {
			System.out.println(" * " + m.getName() + " for bundle " + m.getBundleName());
		}
	}
	
	private void printMissingCustomDependencies(List<MissingDependency> missingDependencies) {
		for(MissingDependency m : missingDependencies) {
			System.out.println(" * " + m.getName() + "(" + m.getType() + ")" + " for bundle " + m.getBundleName());
		}
	}

    private void listResolvedBundles() {
        boolean areResolved = false;
        for (Bundle b : m_context.getBundles()) {
            if (b.getState() == Bundle.RESOLVED && !isFragment(b)) {
                areResolved = true;
                break;
            }
        }
        if (areResolved) {
            System.out.println("Please note that the following bundles are in the RESOLVED state:");
            for (Bundle b : m_context.getBundles()) {
                if (b.getState() == Bundle.RESOLVED && !isFragment(b)) {
                    System.out.println(" * [" + b.getBundleId() + "] " + b.getSymbolicName());
                }
            }
        }
    }
    
    private void listInstalledBundles() {
        boolean areResolved = false;
        for (Bundle b : m_context.getBundles()) {
            if (b.getState() == Bundle.INSTALLED) {
                areResolved = true;
                break;
            }
        }
        if (areResolved) {
            System.out.println("Please note that the following bundles are in the INSTALLED state:");
            for (Bundle b : m_context.getBundles()) {
                if (b.getState() == Bundle.INSTALLED) {
                    System.out.println(" * [" + b.getBundleId() + "] " + b.getSymbolicName());
                }
            }
        }
    }

    private boolean isFragment(Bundle b) {
        Dictionary<String, String> headers = b.getHeaders();
        return headers.get("Fragment-Host") != null;
    }
    
    public static class ComponentDeclarationComparator implements Comparator<ComponentDeclaration> {
		@Override
		public int compare(ComponentDeclaration cd1, ComponentDeclaration cd2) {
			long id1 = cd1.getBundleContext().getBundle().getBundleId();
			long id2 = cd2.getBundleContext().getBundle().getBundleId();
			if(id1 == id2) {
				// sort by component id
				long cid1 = cd1.getId();
				long cid2 = cd2.getId();
				return cid1 > cid2 ? 1 : -1;
			} 
			return id1 > id2 ? 1 : -1;
		}
    }
}
