/**
 * Copyright 2012-2015 Rafal Lewczuk <rafal.lewczuk@jitlogic.com>
 * <p/>
 * This is free software. You can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * <p/>
 * This software is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * <p/>
 * You should have received a copy of the GNU General Public License
 * along with this software. If not, see <http://www.gnu.org/licenses/>.
 */
package com.jitlogic.zico.client.views.traces;

import com.jitlogic.zico.widgets.client.*;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.ActionCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.DataGrid;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy;
import com.google.gwt.user.cellview.client.IdentityColumn;
import com.google.gwt.user.client.ui.*;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SingleSelectionModel;
import com.google.inject.assistedinject.Assisted;
import com.jitlogic.zico.client.ClientUtil;
import com.jitlogic.zico.client.api.TraceDataService;
import com.jitlogic.zico.client.resources.Resources;
import com.jitlogic.zico.client.inject.PanelFactory;
import com.jitlogic.zico.shared.data.TraceInfo;
import com.jitlogic.zico.shared.data.TraceRecordInfo;
import com.jitlogic.zico.shared.data.TraceRecordListQuery;
import com.jitlogic.zico.widgets.client.MenuItem;
import org.fusesource.restygwt.client.Method;
import org.fusesource.restygwt.client.MethodCallback;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class TraceCallTreePanel extends Composite {
    interface TraceCallTreePanelUiBinder extends UiBinder<Widget, TraceCallTreePanel> { }
    private static TraceCallTreePanelUiBinder ourUiBinder = GWT.create(TraceCallTreePanelUiBinder.class);

    @UiField
    DockLayoutPanel panel;

    @UiField(provided = true)
    Resources resources;

    @UiField
    ToolButton btnParentMethod;

    @UiField
    ToolButton btnSlowestMethod;

    @UiField
    ToolButton btnErrorMethod;

    @UiField
    ToolButton btnExpandAll;

    @UiField
    ToolButton btnSearch;

    @UiField
    ToolButton btnSearchPrev;

    @UiField
    ToolButton btnSearchNext;

    @UiField(provided = true)
    DataGrid<TraceRecordInfo> grid;

    private static final ProvidesKey<TraceRecordInfo> KEY_PROVIDER = new ProvidesKey<TraceRecordInfo>() {
        @Override
        public Object getKey(TraceRecordInfo item) {
            return item.getPath();
        }
    };

    private TraceInfo trace;

    private TraceDataService traceDataService;

    private TraceRecordSearchDialog searchDialog;
    private PanelFactory panelFactory;

    private SingleSelectionModel<TraceRecordInfo> selection;
    private ListDataProvider<TraceRecordInfo> data;
    private TraceCallTableBuilder rowBuilder;

    private Set<String> expandedDetails = new HashSet<String>();

    private PopupMenu contextMenu;

    private boolean fullyExpanded;

    private List<TraceRecordInfo> searchResults = new ArrayList<TraceRecordInfo>();
    private int curentSearchResultIdx = -1;

    private MessageDisplay md;
    private final String MDS;

    @Inject
    public TraceCallTreePanel(TraceDataService traceDataService, PanelFactory panelFactory,
                              MessageDisplay md, @Assisted TraceInfo trace) {
        this.md = md;
        this.traceDataService = traceDataService;
        this.panelFactory = panelFactory;
        this.trace = trace;

        this.MDS = "TraceCallTree:" + trace.getHostName() + ":" + trace.getDataOffs();

        this.resources = Resources.INSTANCE;

        createCallTreeGrid();
        ourUiBinder.createAndBindUi(this);

        initWidget(panel);

        btnSearchPrev.setEnabled(false);
        btnSearchNext.setEnabled(false);

        createContextMenu();

        loadData(false, null);
    }


    private final static String SMALL_CELL_CSS = Resources.INSTANCE.zicoCssResources().traceSmallCell();

    private final static String SMALL_CELL_CSS_R = Resources.INSTANCE.zicoCssResources().traceSmallCellR();

    private void createCallTreeGrid() {
        grid = new DataGrid<TraceRecordInfo>(1024*1024, ZicoDataGridResources.INSTANCE, KEY_PROVIDER);
        selection = new SingleSelectionModel<TraceRecordInfo>(KEY_PROVIDER);
        grid.setSelectionModel(selection);

        Column<TraceRecordInfo, TraceRecordInfo> colExpander
                = new IdentityColumn<TraceRecordInfo>(DETAIL_EXPANDER_CELL);
        grid.addColumn(colExpander, "#");
        grid.setColumnWidth(colExpander, 32, Style.Unit.PX);

        Column<TraceRecordInfo, TraceRecordInfo> colMethodName
                = new IdentityColumn<TraceRecordInfo>(METHOD_TREE_CELL);
        grid.addColumn(colMethodName, new ResizableHeader<TraceRecordInfo>("Method", grid, colMethodName));
        grid.setColumnWidth(colMethodName, 100, Style.Unit.PCT);


        Column<TraceRecordInfo, TraceRecordInfo> colTime =
            new IdentityColumn<TraceRecordInfo>(METHOD_TIME_CELL);
        grid.addColumn(colTime, new ResizableHeader<TraceRecordInfo>("Time", grid, colTime));
        grid.setColumnWidth(colTime, 60, Style.Unit.PX);
        colTime.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);

        Column<TraceRecordInfo, TraceRecordInfo> colCTime =
                new IdentityColumn<TraceRecordInfo>(METHOD_CTIME_CELL);
        grid.addColumn(colCTime, new ResizableHeader<TraceRecordInfo>("CTime", grid, colTime));
        grid.setColumnWidth(colCTime, 60, Style.Unit.PX);
        colCTime.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);

        Column<TraceRecordInfo, TraceRecordInfo> colCalls =
            new IdentityColumn<TraceRecordInfo>(METHOD_CALLS_CELL);
        grid.addColumn(colCalls, new ResizableHeader<TraceRecordInfo>("Calls", grid, colCalls));
        grid.setColumnWidth(colCalls, 60, Style.Unit.PX);


        Column<TraceRecordInfo, TraceRecordInfo> colErrors =
            new IdentityColumn<TraceRecordInfo>(METHOD_ERRORS_CELL);
        grid.addColumn(colErrors, new ResizableHeader<TraceRecordInfo>("Errors", grid, colErrors));
        grid.setColumnWidth(colErrors, 60, Style.Unit.PX);


        Column<TraceRecordInfo, TraceRecordInfo> colPct =
            new IdentityColumn<TraceRecordInfo>(METHOD_PCT_CELL);
        grid.addColumn(colPct, new ResizableHeader<TraceRecordInfo>("Pct", grid, colPct));
        grid.setColumnWidth(colPct, 60, Style.Unit.PX);

        rowBuilder = new TraceCallTableBuilder(grid, expandedDetails);
        grid.setTableBuilder(rowBuilder);

        // Disable hovering "features" overall to improve performance on big trees.
        grid.setSkipRowHoverStyleUpdate(true);
        grid.setSkipRowHoverFloatElementCheck(true);
        grid.setSkipRowHoverCheck(true);
        grid.setKeyboardSelectionPolicy(HasKeyboardSelectionPolicy.KeyboardSelectionPolicy.DISABLED);

        grid.addCellPreviewHandler(new CellPreviewEvent.Handler<TraceRecordInfo>() {
            @Override
            public void onCellPreview(CellPreviewEvent<TraceRecordInfo> event) {
                NativeEvent nev = event.getNativeEvent();
                String eventType = nev.getType();
                if ((BrowserEvents.KEYDOWN.equals(eventType) && nev.getKeyCode() == KeyCodes.KEY_ENTER)
                        || BrowserEvents.DBLCLICK.equals(nev.getType())) {
                    TraceRecordInfo tr = event.getValue();
                    panelFactory.methodAttrsDialog(trace.getHostName(), trace.getDataOffs(), tr.getPath(), 0L).asPopupWindow().show();
                }
                if (BrowserEvents.CONTEXTMENU.equals(eventType)) {
                    selection.setSelected(event.getValue(), true);
                    contextMenu.setPopupPosition(
                            event.getNativeEvent().getClientX(),
                            event.getNativeEvent().getClientY());
                    contextMenu.show();
                }
            }
        });

        grid.addDomHandler(new DoubleClickHandler() {
            @Override
            public void onDoubleClick(DoubleClickEvent event) {
                event.preventDefault();
            }
        }, DoubleClickEvent.getType());

        grid.addDomHandler(new ContextMenuHandler() {
            @Override
            public void onContextMenu(ContextMenuEvent event) {
                event.preventDefault();
            }
        }, ContextMenuEvent.getType());

        data = new ListDataProvider<TraceRecordInfo>();
        data.addDataDisplay(grid);
    }


    private void createContextMenu() {
        contextMenu = new PopupMenu();


        MenuItem mnuMethodAttrs = new MenuItem("Trace Attributes", Resources.INSTANCE.methodAttrsIcon(),
                new Scheduler.ScheduledCommand() {
                    @Override
                    public void execute() {
                        TraceRecordInfo tr = selection.getSelectedObject();
                        if (tr != null) {
                            panelFactory.methodAttrsDialog(trace.getHostName(), trace.getDataOffs(), tr.getPath(), 0L).asPopupWindow().show();
                        }
                    }
                });
        contextMenu.addItem(mnuMethodAttrs);
    }


    @UiHandler("btnSearch")
    void doSearch(ClickEvent e) {
        if (searchDialog == null) {
            searchDialog = panelFactory.traceRecordSearchDialog(this, trace);
        }
        searchDialog.asPopupWindow().show();
    }


    public void setResults(List<TraceRecordInfo> results, int idx) {
        this.searchResults = results;
        goToResult(idx);
    }

    private void goToResult(final int idx) {

        // Tree has to be fully expanded in order to search results
        if (!fullyExpanded) {
            this.loadData(true, new Runnable() {
                @Override
                public void run() {
                    goToResult(idx);
                }
            });
            return;
        }

        String path = searchResults.get(idx).getPath();

        if (path.length() == 0) {
            path = "/";
        }

        List<TraceRecordInfo> lst = data.getList();

        for (int i = 0; i < lst.size(); i++) {
            TraceRecordInfo tr = lst.get(i);
            String trPath = "/"+tr.getPath();
            if (trPath.equals(path)) {
                selection.setSelected(tr, true);
                grid.getRowElement(i).scrollIntoView();
                break;
            }
        }

        curentSearchResultIdx = idx;
        btnSearchPrev.setEnabled(idx > 0);
        btnSearchNext.setEnabled(idx < searchResults.size()-1);
    }


    private void
    loadData(final boolean recursive, final Runnable action) {

        if (recursive) {
            btnExpandAll.setEnabled(false);
        }
        data.getList().clear();
        if (recursive) {
            fullyExpanded = true;
        }

        md.info(MDS, "Loading data. Please wait ...");

        TraceRecordListQuery q = new TraceRecordListQuery();
        q.setHostName(trace.getHostName());
        q.setTraceOffs(trace.getDataOffs());
        q.setMinTime(0);
        q.setPath(null);
        q.setRecursive(recursive);

        traceDataService.listRecords(q, new MethodCallback<List<TraceRecordInfo>>() {
            @Override
            public void onFailure(Method method, Throwable e) {
                md.error(MDS, "Error loading trace data", e);
            }

            @Override
            public void onSuccess(Method method, List<TraceRecordInfo> response) {
                data.setList(response);
                if (action != null) {
                    action.run();
                }
                if (response.size() > 1) {
                    grid.getRowElement(0).scrollIntoView();
                }
                md.clear(MDS);
            }
        });
    }


    @UiHandler("btnParentMethod")
    void findParent(ClickEvent e) {
        TraceRecordInfo rec = selection.getSelectedObject();

        if (rec == null) {
            return;
        }

        List<TraceRecordInfo> recList = data.getList();

        int nsegs = rec.getPath().split("/").length;

        TraceRecordInfo prec = rec;
        int idx = 0;

        for (idx = recList.indexOf(rec)-1; idx >= 0; idx--) {
            prec = recList.get(idx);
            if (prec.getPath().split("/").length < nsegs) {
                break;
            }
        }

        selection.setSelected(prec, true);
        grid.getRowElement(idx >= 0 ? idx : 0).scrollIntoView();
    }


    @UiHandler("btnErrorMethod")
    void findErrorMethod(ClickEvent e) {

        if (!fullyExpanded) {
            this.loadData(true, new Runnable() {
                @Override
                public void run() {
                    findErrorMethod(null);
                }
            });
            return;
        }

        TraceRecordInfo rec = selection.getSelectedObject();
        List<TraceRecordInfo> recList = data.getList();
        if (rec == null) {
            rec = recList.get(0);
        }

        for (int i = recList.indexOf(rec)+1; i < recList.size(); i++) {
            TraceRecordInfo tr = recList.get(i);
            if (tr.getExceptionInfo() != null) {
                selection.setSelected(tr, true);
                grid.getRowElement(i).scrollIntoView();
                break;
            }
        }

    }


    @UiHandler("btnSlowestMethod")
    void findSlowestMethod(ClickEvent e) {
        TraceRecordInfo rec = selection.getSelectedObject();
        List<TraceRecordInfo> recList = data.getList();
        if (rec == null) {
            rec = recList.get(0);
        }

        TraceRecordInfo lrec = null;
        int lidx = -1, startIdx = recList.indexOf(rec);

        if (rec.getChildren() > 0 && !isExpanded(startIdx)) {
            doExpand(rec);
            return;
        }

        if (startIdx+1 < recList.size()) {
            for (int idx = startIdx+1; idx < recList.size(); idx++) {
                TraceRecordInfo r = recList.get(idx);
                if (r.getPath().startsWith(rec.getPath())) {
                    if (lrec == null || r.getTime() > lrec.getTime()) {
                        lrec = r;
                        lidx = idx;
                    }
                } else {
                    break;
                }
            }
        } else {
            return;
        }

        if (lrec != null) {
            selection.setSelected(lrec, true);
            grid.getRowElement(lidx).scrollIntoView();

            if (!isExpanded(lidx)) {
                doExpand(lrec);
            }
        }

    }


    @UiHandler("btnExpandAll")
    void expandAll(ClickEvent e) {
        loadData(true, null);
    }


    @UiHandler("btnSearchPrev")
    void goPrevResult(ClickEvent e) {
        goToResult(curentSearchResultIdx-1);
    }


    @UiHandler("btnSearchNext")
    void goNextResult(ClickEvent e) {
        goToResult(curentSearchResultIdx+1);
    }


    private void doExpand(final TraceRecordInfo rec) {
        md.info(MDS, "Loading trace data...");
        TraceRecordListQuery q = new TraceRecordListQuery();
        q.setHostName(trace.getHostName());;
        q.setTraceOffs(trace.getDataOffs());
        q.setMinTime(0);
        q.setPath(rec.getPath());
        q.setRecursive(false);

        traceDataService.listRecords(q, new MethodCallback<List<TraceRecordInfo>>() {
            @Override
            public void onFailure(Method method, Throwable e) {
                md.error(MDS, "Error loading trace data", e);
            }

            @Override
            public void onSuccess(Method method, List<TraceRecordInfo> records) {
                List<TraceRecordInfo> list = data.getList();
                int idx = list.indexOf(rec)+1;
                for (int i = 0; i < records.size(); i++) {
                    list.add(idx+i, records.get(i));
                }
                md.clear(MDS);
            }
        });
    }


    private void doCollapse(TraceRecordInfo rec) {
        List<TraceRecordInfo> list = data.getList();
        int idx = list.indexOf(rec) + 1;
        while (idx < list.size() && list.get(idx).getPath().startsWith(rec.getPath())) {
            list.remove(idx);
        }
    }


    private void toggleSubtree(TraceRecordInfo rec) {
        if (rec.getChildren() > 0) {
            if (isExpanded(rec)) {
                doCollapse(rec);
                btnExpandAll.setEnabled(true);
            } else {
                doExpand(rec);
            }
        }
        grid.redrawRow(data.getList().indexOf(rec));
    }


    private boolean isExpanded(TraceRecordInfo rec) {
        int idx = data.getList().indexOf(rec);
        return idx >= 0 ? isExpanded(idx) : false;
    }


    private boolean isExpanded(int idx) {
        List<TraceRecordInfo> lst = data.getList();
        if (idx < lst.size()-1) {
            TraceRecordInfo tr = lst.get(idx), ntr = lst.get(idx+1);
            return tr != null && ntr != null && ntr.getPath().startsWith(tr.getPath());
        } else {
            return false;
        }
    }


    private void toggleDetails(TraceRecordInfo rec) {
        String path = rec.getPath();
        if (expandedDetails.contains(path)) {
            expandedDetails.remove(path);
        } else {
            expandedDetails.add(path);
        }
        grid.redrawRow(data.getList().indexOf(rec));
    }


    private static final String PLUS_HTML = AbstractImagePrototype.create(Resources.INSTANCE.treePlusSlimIcon()).getHTML();
    private static final String MINUS_HTML = AbstractImagePrototype.create(Resources.INSTANCE.treeMinusSlimIcon()).getHTML();

    private static final String EXPANDER_EXPAND = AbstractImagePrototype.create(Resources.INSTANCE.expanderExpand()).getHTML();
    private static final String EXPANDER_COLLAPSE = AbstractImagePrototype.create(Resources.INSTANCE.expanderCollapse()).getHTML();


    private final Cell<TraceRecordInfo> METHOD_TREE_CELL = new AbstractCell<TraceRecordInfo>("click") {

        @Override
        public void render(Context context, TraceRecordInfo tr, SafeHtmlBuilder sb) {
            String path = tr.getPath();
            int offs = path != null && path.length() > 0 ? (path.split("/").length) * 24 : 0;
            String color = tr.getExceptionInfo() != null ? "red" : tr.getAttributes() != null ? "blue" : "black";

            sb.appendHtmlConstant("<div style=\"margin-left: " + offs + "px; color: " + color + ";margin-top: 3px;\">");
            if (tr.getChildren() > 0) {
                sb.appendHtmlConstant("<span style=\"cursor: pointer;\">");
                sb.appendHtmlConstant(isExpanded(context.getIndex()) ? MINUS_HTML : PLUS_HTML);
                sb.appendHtmlConstant("</span>");
            }
            sb.appendHtmlConstant("<span style=\"vertical-align: top;\">");
            sb.append(SafeHtmlUtils.fromString(tr.getMethod()));
            sb.appendHtmlConstant("</span></div>");
        }

        @Override
        public void onBrowserEvent(Context context, Element parent, TraceRecordInfo rec,
                                   NativeEvent event, ValueUpdater<TraceRecordInfo> valueUpdater) {
            super.onBrowserEvent(context, parent, rec, event, valueUpdater);
            EventTarget eventTarget = event.getEventTarget();
            if (Element.is(eventTarget)) {
                Element target = eventTarget.cast();
                if ("IMG".equalsIgnoreCase(target.getTagName())) {
                    toggleSubtree(rec);
                }
            }
        }
    };

    private AbstractCell<TraceRecordInfo> METHOD_TIME_CELL = new AbstractCell<TraceRecordInfo>() {
        @Override
        public void render(Context context, TraceRecordInfo rec, SafeHtmlBuilder sb) {
            sb.appendHtmlConstant("<div class=\"" + SMALL_CELL_CSS_R + "\">");
            sb.append(SafeHtmlUtils.fromString(ClientUtil.formatDuration(rec.getTime())));
            sb.appendHtmlConstant("</div>");
        }
    };

    private AbstractCell<TraceRecordInfo> METHOD_CTIME_CELL = new AbstractCell<TraceRecordInfo>() {
        @Override
        public void render(Context context, TraceRecordInfo rec, SafeHtmlBuilder sb) {
            sb.appendHtmlConstant("<div class=\"" + SMALL_CELL_CSS_R + "\">");
            sb.append(SafeHtmlUtils.fromString(ClientUtil.formatDuration(rec.getCtime())));
            sb.appendHtmlConstant("</div>");
        }
    };

    private AbstractCell<TraceRecordInfo> METHOD_CALLS_CELL = new AbstractCell<TraceRecordInfo>() {
        @Override
        public void render(Context context, TraceRecordInfo rec, SafeHtmlBuilder sb) {
            sb.appendHtmlConstant("<div class=\"" + SMALL_CELL_CSS_R + "\">");
            sb.append(SafeHtmlUtils.fromString("" + rec.getCalls()));
            sb.appendHtmlConstant("</div>");
        }
    };

    private AbstractCell<TraceRecordInfo> METHOD_ERRORS_CELL = new AbstractCell<TraceRecordInfo>() {
        @Override
        public void render(Context context, TraceRecordInfo rec, SafeHtmlBuilder sb) {
            sb.appendHtmlConstant("<div class=\"" + SMALL_CELL_CSS_R + "\">");
            sb.append(SafeHtmlUtils.fromString("" + rec.getErrors()));
            sb.appendHtmlConstant("</div>");
        }
    };

    private AbstractCell<TraceRecordInfo> METHOD_PCT_CELL = new AbstractCell<TraceRecordInfo>() {
        @Override
        public void render(Context context, TraceRecordInfo rec, SafeHtmlBuilder sb) {
            double pct = 100.0 * rec.getTime() / trace.getExecutionTime();
            String color = "rgb(" + ((int) (pct * 2.49)) + ",0,0)";
            sb.appendHtmlConstant("<div class=\"" + SMALL_CELL_CSS + "\" style=\"color: " + color + ";\">");
            sb.append(SafeHtmlUtils.fromString(NumberFormat.getFormat("###.0").format(pct) + "%"));
            sb.appendHtmlConstant("</div>");
        }
    };


    private final Cell<TraceRecordInfo> DETAIL_EXPANDER_CELL = new ActionCell<TraceRecordInfo>("",
        new ActionCell.Delegate<TraceRecordInfo>() {
        @Override
        public void execute(TraceRecordInfo rec) {
            toggleDetails(rec);
        }
    }) {
        @Override
        public void render(Cell.Context context, TraceRecordInfo tr, SafeHtmlBuilder sb) {
            if ((tr.getAttributes() != null && tr.getAttributes().size() > 0)||tr.getExceptionInfo() != null) {
                sb.appendHtmlConstant("<span style=\"cursor: pointer;\">");
                sb.appendHtmlConstant(expandedDetails.contains(tr.getPath()) ? EXPANDER_COLLAPSE : EXPANDER_EXPAND);
                sb.appendHtmlConstant("</span>");
            }
        }
    };

}
