/*
 * Copyright 2000-2013 Vaadin Ltd.
 * 
 * Licensed 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 com.vaadin.client.debug.internal;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.Profiler;
import com.vaadin.client.SimpleTree;
import com.vaadin.client.ValueMap;

/**
 * Debug window section for investigating {@link Profiler} data. This section is
 * only visible if the profiler is enabled ({@link Profiler#isEnabled()}).
 * 
 * @since 7.1
 * @author Vaadin Ltd
 * 
 * @see Profiler
 */
public class ProfilerSection implements Section {
    /**
     * Interface for getting data from the {@link Profiler}.
     * <p>
     * <b>Warning!</b> This interface is most likely to change in the future and
     * is therefore defined in this class in an internal package instead of
     * Profiler where it might seem more logical.
     * 
     * @since 7.1
     * @author Vaadin Ltd
     */
    public interface ProfilerResultConsumer {
        public void addProfilerData(Node rootNode, List<Node> totals);

        public void addBootstrapData(LinkedHashMap<String, Double> timings);
    }

    /**
     * A hierarchical representation of the time spent running a named block of
     * code.
     * <p>
     * <b>Warning!</b> This class is most likely to change in the future and is
     * therefore defined in this class in an internal package instead of
     * Profiler where it might seem more logical.
     */
    public static class Node {
        private final String name;
        private final LinkedHashMap<String, Node> children = new LinkedHashMap<String, Node>();
        private double time = 0;
        private int count = 0;
        private double enterTime = 0;
        private double minTime = 1000000000;
        private double maxTime = 0;

        /**
         * Create a new node with the given name.
         * 
         * @param name
         */
        public Node(String name) {
            this.name = name;
        }

        /**
         * Gets the name of the node
         * 
         * @return the name of the node
         */
        public String getName() {
            return name;
        }

        /**
         * Creates a new child node or retrieves and existing child and updates
         * its total time and hit count.
         * 
         * @param name
         *            the name of the child
         * @param timestamp
         *            the timestamp for when the node is entered
         * @return the child node object
         */
        public Node enterChild(String name, double timestamp) {
            Node child = children.get(name);
            if (child == null) {
                child = new Node(name);
                children.put(name, child);
            }
            child.enterTime = timestamp;
            child.count++;
            return child;
        }

        /**
         * Gets the total time spent in this node, including time spent in sub
         * nodes
         * 
         * @return the total time spent, in milliseconds
         */
        public double getTimeSpent() {
            return time;
        }

        /**
         * Gets the minimum time spent for one invocation of this node,
         * including time spent in sub nodes
         * 
         * @return the time spent for the fastest invocation, in milliseconds
         */
        public double getMinTimeSpent() {
            return minTime;
        }

        /**
         * Gets the maximum time spent for one invocation of this node,
         * including time spent in sub nodes
         * 
         * @return the time spent for the slowest invocation, in milliseconds
         */
        public double getMaxTimeSpent() {
            return maxTime;
        }

        /**
         * Gets the number of times this node has been entered
         * 
         * @return the number of times the node has been entered
         */
        public int getCount() {
            return count;
        }

        /**
         * Gets the total time spent in this node, excluding time spent in sub
         * nodes
         * 
         * @return the total time spent, in milliseconds
         */
        public double getOwnTime() {
            double time = getTimeSpent();
            for (Node node : children.values()) {
                time -= node.getTimeSpent();
            }
            return time;
        }

        /**
         * Gets the child nodes of this node
         * 
         * @return a collection of child nodes
         */
        public Collection<Node> getChildren() {
            return Collections.unmodifiableCollection(children.values());
        }

        private void buildRecursiveString(StringBuilder builder, String prefix) {
            if (getName() != null) {
                String msg = getStringRepresentation(prefix);
                builder.append(msg + '\n');
            }
            String childPrefix = prefix + "*";
            for (Node node : children.values()) {
                node.buildRecursiveString(builder, childPrefix);
            }
        }

        @Override
        public String toString() {
            return getStringRepresentation("");
        }

        public String getStringRepresentation(String prefix) {
            if (getName() == null) {
                return "";
            }
            String msg = prefix + " " + getName() + " in " + getTimeSpent()
                    + " ms.";
            if (getCount() > 1) {
                msg += " Invoked "
                        + getCount()
                        + " times ("
                        + roundToSignificantFigures(getTimeSpent() / getCount())
                        + " ms per time, min "
                        + roundToSignificantFigures(getMinTimeSpent())
                        + " ms, max "
                        + roundToSignificantFigures(getMaxTimeSpent())
                        + " ms).";
            }
            if (!children.isEmpty()) {
                double ownTime = getOwnTime();
                msg += " " + ownTime + " ms spent in own code";
                if (getCount() > 1) {
                    msg += " ("
                            + roundToSignificantFigures(ownTime / getCount())
                            + " ms per time)";
                }
                msg += '.';
            }
            return msg;
        }

        private static double roundToSignificantFigures(double num) {
            // Number of significant digits
            int n = 3;
            if (num == 0) {
                return 0;
            }

            final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
            final int power = n - (int) d;

            final double magnitude = Math.pow(10, power);
            final long shifted = Math.round(num * magnitude);
            return shifted / magnitude;
        }

        public void sumUpTotals(Map<String, Node> totals) {
            String name = getName();
            if (name != null) {
                Node totalNode = totals.get(name);
                if (totalNode == null) {
                    totalNode = new Node(name);
                    totals.put(name, totalNode);
                }

                totalNode.time += getOwnTime();
                totalNode.count += getCount();
                totalNode.minTime = Math.min(totalNode.minTime,
                        getMinTimeSpent());
                totalNode.maxTime = Math.max(totalNode.maxTime,
                        getMaxTimeSpent());
            }
            for (Node node : children.values()) {
                node.sumUpTotals(totals);
            }
        }

        /**
         * @param timestamp
         */
        public void leave(double timestamp) {
            double elapsed = (timestamp - enterTime);
            time += elapsed;
            enterTime = 0;
            if (elapsed < minTime) {
                minTime = elapsed;
            }
            if (elapsed > maxTime) {
                maxTime = elapsed;
            }
        }
    }

    private static final int MAX_ROWS = 10;

    private final DebugButton tabButton = new DebugButton(Icon.RESET_TIMER,
            "Profiler");

    private final HorizontalPanel controls = new HorizontalPanel();
    private final FlowPanel content = new FlowPanel();

    public ProfilerSection() {
        Profiler.setProfilerResultConsumer(new ProfilerResultConsumer() {
            @Override
            public void addProfilerData(Node rootNode, List<Node> totals) {
                double totalTime = 0;
                int eventCount = 0;
                for (Node node : totals) {
                    totalTime += node.getTimeSpent();
                    eventCount += node.getCount();
                }

                SimpleTree drillDownTree = (SimpleTree) buildTree(rootNode);
                drillDownTree.setText("Drill down");

                SimpleTree offendersTree = new SimpleTree("Longest events");
                for (int i = 0; i < totals.size() && i < 20; i++) {
                    Node node = totals.get(i);
                    offendersTree.add(new Label(node
                            .getStringRepresentation("")));
                }

                SimpleTree root = new SimpleTree(eventCount
                        + " profiler events using " + totalTime + " ms");
                root.add(drillDownTree);
                root.add(offendersTree);
                root.open(false);

                content.add(root);
                applyLimit();
            }

            @Override
            public void addBootstrapData(LinkedHashMap<String, Double> timings) {
                SimpleTree tree = new SimpleTree(
                        "Time since window.performance.timing events");
                Set<Entry<String, Double>> entrySet = timings.entrySet();
                for (Entry<String, Double> entry : entrySet) {
                    tree.add(new Label(entry.getValue() + " " + entry.getKey()));
                }

                tree.open(false);
                content.add(tree);
                applyLimit();
            }
        });
    }

    private Widget buildTree(Node node) {
        String message = node.getStringRepresentation("");

        Collection<Node> children = node.getChildren();
        if (node.getName() == null || !children.isEmpty()) {
            SimpleTree tree = new SimpleTree(message);
            for (Node childNode : children) {
                Widget child = buildTree(childNode);
                tree.add(child);
            }
            return tree;
        } else {
            return new Label(message);
        }
    }

    private void applyLimit() {
        while (content.getWidgetCount() > MAX_ROWS) {
            content.remove(0);
        }
    }

    @Override
    public DebugButton getTabButton() {
        return tabButton;
    }

    @Override
    public Widget getControls() {
        return controls;
    }

    @Override
    public Widget getContent() {
        return content;
    }

    @Override
    public void show() {
        // Nothing to do
    }

    @Override
    public void hide() {
        // Nothing to do
    }

    @Override
    public void meta(ApplicationConnection ac, ValueMap meta) {
        // Nothing to do
    }

    @Override
    public void uidl(ApplicationConnection ac, ValueMap uidl) {
        // Nothing to do
    }

}
