/*
 *  Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved.
 *  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * 
 *  This code is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License version 2 only, as
 *  published by the Free Software Foundation.  Oracle designates this
 *  particular file as subject to the "Classpath" exception as provided
 *  by Oracle in the LICENSE file that accompanied this code.
 * 
 *  This code 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
 *  version 2 for more details (a copy is included in the LICENSE file that
 *  accompanied this code).
 * 
 *  You should have received a copy of the GNU General Public License version
 *  2 along with this work; if not, write to the Free Software Foundation,
 *  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 * 
 *  Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 *  or visit www.oracle.com if you need additional information or have any
 *  questions.
 */

package org.graalvm.visualvm.modules.tracer.impl.timeline;

import java.awt.Rectangle;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.SwingUtilities;
import org.graalvm.visualvm.lib.charts.ChartContext;
import org.graalvm.visualvm.lib.charts.ChartItem;
import org.graalvm.visualvm.lib.charts.ChartItemChange;
import org.graalvm.visualvm.lib.charts.ChartSelectionModel;
import org.graalvm.visualvm.lib.charts.ItemPainter;
import org.graalvm.visualvm.lib.charts.PaintersModel;
import org.graalvm.visualvm.lib.charts.swing.LongRect;
import org.graalvm.visualvm.lib.charts.swing.Utils;
import org.graalvm.visualvm.lib.charts.xy.synchronous.SynchronousXYChart;
import org.graalvm.visualvm.lib.charts.xy.synchronous.SynchronousXYItem;
import org.graalvm.visualvm.lib.charts.xy.synchronous.SynchronousXYItemsModel;

/**
 *
 * @author Jiri Sedlacek
 */
final class TimelineChart extends SynchronousXYChart {

    static final int MIN_ROW_HEIGHT = 25;
    static final int MAX_ROW_HEIGHT = 500;
    static final int DEF_ROW_HEIGHT = 75;
    static final int ROW_RESIZE_STEP = MIN_ROW_HEIGHT;

    private static final int ROW_MARGIN_TOP = 3;
    private static final int ROW_MARGIN_BOTTOM = 3;

    private int currentRowHeight = DEF_ROW_HEIGHT;

    private final List<Row> rows;
    private final Map<ChartItem, Row> itemsToRows;

    private final Set selectedRows = new TreeSet(new RowComparator());
    private final Set selectionBlockers = new HashSet();
    private int lastHoverMode;
    private int lastMoveMode;

    private final Set<RowListener> rowListeners = new HashSet();


    // --- Constructors --------------------------------------------------------

    TimelineChart(SynchronousXYItemsModel itemsModel) {
        super(itemsModel, new PaintersModel.Default());

        rows = new ArrayList();
        itemsToRows = new HashMap();

        setBottomBased(false);

        setZoomMode(ZOOM_X);
        setMouseZoomingEnabled(false);
        setMousePanningEnabled(false);

        setAccelerationPriority(1f);
    }


    // --- Rows management -----------------------------------------------------

    Row addRow() {
        Row row = new Row();
        int rowIndex = rows.size();
        row.setIndex(rowIndex);
        rows.add(row);
        row.setHeight(currentRowHeight, true);
        row.updateOffset();
//        repaintRows(row.getIndex());
        updateChart();
        notifyRowsAdded(Collections.singletonList(row));
        return row;
    }

    Row addRow(int rowIndex) {
        Row row = new Row();
        row.setIndex(rowIndex);
        rows.add(rowIndex, row);
        row.setHeight(currentRowHeight, true);
        updateRowOffsets(rowIndex);
        updateRowIndexes(rowIndex + 1);
//        repaintRows(rowIndex);
        updateChart();
        notifyRowsAdded(Collections.singletonList(row));
        return row;
    }

    Row removeRow(int rowIndex) {
        return removeRow(rows.get(rowIndex));
    }

    Row removeRow(Row row) {
        row.clearItems();
        rows.remove(row);
        int rowIndex = row.getIndex();
        updateRowIndexes(rowIndex);
        updateRowOffsets(rowIndex);
//        repaintRows(row.getIndex());
        updateChart();
        notifyRowsRemoved(Collections.singletonList(row));
        return row;
    }


    // --- Rows access ---------------------------------------------------------

    boolean hasRows() {
        return !rows.isEmpty();
    }

    int getRowsCount() {
        return rows.size();
    }

    Row getRow(int rowIndex) {
        return rows.get(rowIndex);
    }

    Row getRow(ChartItem item) {
        return itemsToRows.get(item);
    }


    // --- Row appearance ------------------------------------------------------

    void setRowHeight(int rowIndex, int rowHeight) {
        setRowHeight(rowIndex, rowHeight, true);
    }

    void setRowHeight(int rowIndex, int rowHeight, boolean checkStep) {
        Row row = rows.get(rowIndex);
        boolean changed = row.setHeight(rowHeight, checkStep);
        updateRowOffsets(rowIndex + 1);
        if (changed) notifyRowsResized(Collections.singletonList(row));
        updateChart(); // TODO: update only affected rows!
    }

    int getRowHeight(int rowIndex) {
        return rows.get(rowIndex).getHeight();
    }

    void increaseRowHeights(boolean step) {
        if (rows.isEmpty()) return;
        int incr = step ? ROW_RESIZE_STEP : 1;
        List<Row> resized = new ArrayList(rows.size());
        for (Row row : rows)
            if (row.setHeight(row.getHeight() + incr, step))
                resized.add(row);
        updateRowOffsets(0);
        if (!resized.isEmpty()) notifyRowsResized(resized);
        updateChart(); // TODO: update only affected rows!
        currentRowHeight += incr;
    }

    void decreaseRowHeights(boolean step) {
        if (rows.isEmpty()) return;
        int decr = step ? ROW_RESIZE_STEP : 1;
        List<Row> resized = new ArrayList(rows.size());
        for (Row row : rows)
            if (row.setHeight(row.getHeight() - decr, step))
                resized.add(row);
        updateRowOffsets(0);
        if (!resized.isEmpty()) notifyRowsResized(resized);
        updateChart(); // TODO: update only affected rows!
        currentRowHeight = Math.max(currentRowHeight - decr, MIN_ROW_HEIGHT);
    }

    void resetRowHeights() {
        if (rows.isEmpty()) return;
        List<Row> resized = new ArrayList(rows.size());
        for (Row row : rows)
            if (row.setHeight(DEF_ROW_HEIGHT, true))
                resized.add(row);
        updateRowOffsets(0);
        if (!resized.isEmpty()) notifyRowsResized(new ArrayList(rows));
        updateChart(); // TODO: update only affected rows!
        currentRowHeight = DEF_ROW_HEIGHT;
    }

    Row getRowAt(int ypos) {
        ypos += getOffsetY();
        for (Row row : rows) {
            int pos = row.getOffset();
            if (ypos < pos) return null;
            pos += row.getHeight();
            if (ypos <= pos) return row;
        }
        return null;
    }

    Row getNearestRow(int ypos, int range, boolean noFirst) {
        if (rows.size() == 0) return null;
        
        ypos += getOffsetY();

        if (noFirst) {
            Row row = rows.get(0);
            int pos = row.getOffset() + row.getHeight();
            if (ypos < pos - range) return null;
        }

        for (Row row : rows) {
            int pos = row.getOffset();
            if (ypos < pos - range) return null;
            if (ypos <= pos + range) return row;
            pos += row.getHeight();
            if (ypos < pos - range) return null;
            if (ypos <= pos + range) return row;
        }
        return null;
    }

    private void updateRowOffsets(int rowIndex) {
        int rowsCount = rows.size();
        if (rowIndex >= rowsCount) return;
        for (int i = rowIndex; i < rowsCount; i++)
            rows.get(i).updateOffset();
    }


    // --- Row events ----------------------------------------------------------

    void addRowListener(RowListener listener) {
        rowListeners.add(listener);
    }

    void removeRowListener(RowListener listener) {
        rowListeners.remove(listener);
    }


    private void notifyRowsAdded(final List<Row> rows) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                for (RowListener listener : rowListeners)
                    listener.rowsAdded(rows);
            }
        });
    }

    private void notifyRowsRemoved(final List<Row> rows) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                for (RowListener listener : rowListeners)
                    listener.rowsRemoved(rows);
            }
        });
    }

    private void notifyRowsResized(final List<Row> rows) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                for (RowListener listener : rowListeners)
                    listener.rowsResized(rows);
            }
        });
    }


    // --- Selection support ---------------------------------------------------

    boolean selectRow(Row row) {
        if (!selectedRows.add(row)) return false;
        repaintRows();
        return true;
    }

    boolean unselectRow(Row row) {
        if (!selectedRows.remove(row)) return false;
        repaintRows();
        return true;
    }

    boolean setSelectedRow(Row row) {
        if (row == null) {
            return clearRowsSelection();
        } else {
            if (selectedRows.size() == 1 && selectedRows.contains(row)) return false;
            selectedRows.clear();
            selectedRows.add(row);
            repaintRows();
            return true;
        }
    }

    boolean toggleRowSelection(Row row) {
        if (selectedRows.contains(row)) return unselectRow(row);
        else return selectRow(row);
    }

    boolean clearRowsSelection() {
        if (selectedRows.isEmpty()) return false;
        selectedRows.clear();
        repaintRows();
        return true;
    }

    boolean isRowSelected(Row row) {
        return selectedRows.contains(row);
    }

    boolean isRowSelection() {
        return !selectedRows.isEmpty();
    }

    List<Row> getSelectedRows() {
        return new ArrayList(selectedRows);
    }
    

    void updateSelection(boolean enable, Object source) {
        int blockersSize = selectionBlockers.size();
        if (enable) selectionBlockers.remove(source);
        else selectionBlockers.add(source);
        if (selectionBlockers.size() == blockersSize) return;

        ChartSelectionModel selectionModel = getSelectionModel();
        if (selectionModel == null) return;

        if (selectionBlockers.isEmpty()) {
            selectionModel.setHoverMode(lastHoverMode);
            selectionModel.setMoveMode(lastMoveMode);
        } else {
            lastHoverMode = selectionModel.getHoverMode();
            lastMoveMode = selectionModel.getMoveMode();
            selectionModel.setHoverMode(ChartSelectionModel.HOVER_NONE);
            selectionModel.setMoveMode(ChartSelectionModel.SELECTION_NONE);
        }
    }


    // --- Internal API to access protected methods ----------------------------

    long maxOffsetX() {
        return super.getMaxOffsetX();
    }

    double viewWidth(double d) {
        return super.getViewWidth(d);
    }

    protected void processMouseWheelEvent(MouseWheelEvent e) {
        super.processMouseWheelEvent(e);
    }


    // --- Protected implementation --------------------------------------------

    protected ChartContext getChartContext(ChartItem item) {
        if (item == null) return super.getChartContext(null);
        else return itemsToRows.get(item).getContext();
    }

    protected void computeDataBounds() {
        LongRect.clear(dataBounds);

        if (rows == null) return;

        for (Row row : rows) {
            RowContext context = (RowContext)row.getContext();
            if (LongRect.isClear(dataBounds)) LongRect.set(dataBounds, context.bounds);
            else LongRect.add(dataBounds, context.bounds);
        }

        dataBounds.y = 0;
        Row lastRow = rows.size() > 0 ? rows.get(rows.size() - 1) : null;
        dataBounds.height = lastRow != null ? lastRow.getOffset() + lastRow.getHeight() : 0;
    }

    protected void updateChart() {
        updateRowBounds();
        super.updateChart();
    }


    protected void itemsAdded(List<ChartItem> addedItems) {
        updateRowBounds();
        super.itemsAdded(addedItems);
    }

    protected void itemsRemoved(List<ChartItem> removedItems) {
        updateRowBounds();
        super.itemsRemoved(removedItems);
    }

    protected void itemsChanged(List<ChartItemChange> itemChanges) {
        updateRowBounds(); // NOTE: should be computed from itemChanges!!!
        super.itemsChanged(itemChanges);
    }

    protected void paintersChanged(List<ItemPainter> changedPainters) {
        updateRowBounds();
        super.paintersChanged(changedPainters);
    }


    // --- Internal implementation ---------------------------------------------

    void addItemsImpl(SynchronousXYItem[] addedItems, ItemPainter[] addedPainters, Row row) {
        for (SynchronousXYItem item : addedItems) itemsToRows.put(item, row);
        paintersModel().addPainters(addedItems, addedPainters);
        itemsModel().addItems(addedItems);
    }

    void removeItemsImpl(SynchronousXYItem[] removedItems) {
        itemsModel().removeItems(removedItems);
        paintersModel().removePainters(removedItems);
        for (SynchronousXYItem item : removedItems) itemsToRows.remove(item);
    }


    void invalidateRepaint() {
        invalidateImage();
        repaintDirty();
    }


    // --- Private implementation ----------------------------------------------

    private SynchronousXYItemsModel itemsModel() {
        return (SynchronousXYItemsModel)getItemsModel();
    }

    private PaintersModel.Default paintersModel() {
        return (PaintersModel.Default)getPaintersModel();
    }

    private void updateRowIndexes(int startIndex) {
        for (int i = startIndex; i < rows.size(); i++)
            rows.get(i).setIndex(i);
    }

    private void repaintRows() {
        invalidateImage();
        repaintDirty();
    }

    private void repaintRows(final int startIndex) {
//        SwingUtilities.invokeLater(new Runnable() {
//            public void run() {
                for (int i = startIndex; i < rows.size(); i++) {
                    ChartContext rowContext = rows.get(i).getContext();
                    invalidateImage(new Rectangle(0, Utils.checkedInt(rowContext.
                                   getViewportOffsetY()), getWidth(), rowContext.
                                   getViewportHeight()));
                }
                repaintDirty();
//            }
//        });
    }

    private void updateRowBounds() {
        if (rows == null) return; // Happens when called from constructor
        for (Row row : rows) ((RowContext)row.getContext()).updateBounds();
    }


    // --- Row definition ------------------------------------------------------

    class Row {

        private int rowIndex;
        private int rowOffset;
        private int rowHeight;
        private final List<SynchronousXYItem> items;
        private final RowContext context;


        // --- Constructors ----------------------------------------------------

        Row() {
            items = new ArrayList();
            context = new RowContext(this);
        }


        // --- Row telemetry ---------------------------------------------------

        int getIndex() {
            return rowIndex;
        }

        private void updateOffset() {
            if (rowIndex != 0) {
                Row previousRow = rows.get(rowIndex - 1);
                rowOffset = previousRow.rowOffset + previousRow.rowHeight;
            } else {
                rowOffset = 0;
            }
        }

        int getOffset() {
            return rowOffset;
        }

        private boolean setHeight(int height, boolean checkStep) {
            height = Math.max(MIN_ROW_HEIGHT, height);
            height = Math.min(MAX_ROW_HEIGHT, height);
            if (checkStep) height = height / ROW_RESIZE_STEP * ROW_RESIZE_STEP;
            boolean changed = rowHeight != height;
            rowHeight = height;
            return changed;
        }

        int getHeight() {
            return rowHeight;
        }


        // --- Items management ------------------------------------------------

        void addItems(SynchronousXYItemsModel addedItems, PaintersModel addedPainters) {
            int itemsCount = addedItems.getItemsCount();

            SynchronousXYItem[] addedItemsArr = new SynchronousXYItem[itemsCount];
            for (int i = 0; i < itemsCount; i++)
                addedItemsArr[i] = addedItems.getItem(i);

            ItemPainter[] addedPaintersArr = new ItemPainter[itemsCount];
            for (int i = 0; i < itemsCount; i++)
                addedPaintersArr[i] = addedPainters.getPainter(addedItemsArr[i]);

            addItems(addedItemsArr, addedPaintersArr);
        }

        void addItems(SynchronousXYItem[] addedItems, ItemPainter[] addedPainters) {
            for (SynchronousXYItem item : addedItems) items.add(item);
            addItemsImpl(addedItems, addedPainters, this);
        }

        void removeItems(SynchronousXYItemsModel removedItems) {
            int itemsCount = removedItems.getItemsCount();

            SynchronousXYItem[] removedItemsArr = new SynchronousXYItem[itemsCount];
            for (int i = 0; i < itemsCount; i++)
                removedItemsArr[i] = removedItems.getItem(i);

            removeItems(removedItemsArr);
        }

        void removeItems(SynchronousXYItem[] removedItems) {
            removeItemsImpl(removedItems);
            for (SynchronousXYItem item : removedItems) items.remove(item);
        }


        // --- Items access ----------------------------------------------------

        int getItemsCount() {
            return items.size();
        }

        ChartItem getItem(int itemIndex) {
            return items.get(itemIndex);
        }

        SynchronousXYItem[] getItems() {
            return items.toArray(new SynchronousXYItem[items.size()]);
        }

        @SuppressWarnings("element-type-mismatch")
        boolean containsItem(ChartItem item) {
            return items.contains(item);
        }


        // --- Row context -----------------------------------------------------

        ChartContext getContext() {
            return context;
        }


        // --- Internal interface ----------------------------------------------

        private void setIndex(int rowIndex) {
            this.rowIndex = rowIndex;
        }

        private void clearItems() {
            if (items.size() == 0) return;
            removeItemsImpl(getItems());
        }

    }


    // --- RowContext implementation -------------------------------------------

    private class RowContext extends SynchronousXYChart.Context {

        private final Row row;

        private final LongRect bounds;
        private double scaleY;

        private int marginTop;
        private int marginBottom;


        RowContext(Row row) {
            super(TimelineChart.this);
            this.row = row;

            marginTop = ROW_MARGIN_TOP;
            marginBottom = ROW_MARGIN_BOTTOM;

            bounds = new LongRect();
        }


        protected void updateBounds() {
            LongRect.clear(bounds);

            PaintersModel painters = paintersModel();
            int itemsCount = row.getItemsCount();

            for (int i = 0; i < itemsCount; i++) {
                ChartItem item = row.getItem(i);
                ItemPainter painter = painters.getPainter(item);
                LongRect itemBounds = painter.getItemBounds(item);
                if (LongRect.isClear(bounds)) {
                    LongRect.set(bounds, itemBounds);
                } else if (LongRect.isEmpty(itemBounds)) { // Zero height (constant value)
                    LongRect.add(bounds, itemBounds.x, itemBounds.height);
                } else {
                    LongRect.add(bounds, itemBounds);
                }
            }

            double oldScaleY = scaleY;
            scaleY = (double)(row.getHeight() - marginTop - marginBottom) /
                     (double)(bounds.height == 0 ? 1 : bounds.height);

            if (scaleY != oldScaleY) invalidateImage(Utils.checkedRectangle(
                                                     getViewRect(bounds)));
        }


        public boolean isBottomBased() { return true; }

        public boolean fitsHeight() { return true; }

        public long getDataOffsetY() { return bounds.y; }

        public long getDataHeight() { return bounds.height; }

        public long getViewHeight() { return row.getHeight(); }

        public long getViewportOffsetY() { return row.getOffset() - getOffsetY(); }

        public int getViewportHeight() { return row.getHeight(); }

        public double getViewY(double dataY) { return getViewY(dataY, false); }

        public double getReversedViewY(double dataY) { return getViewY(dataY, true); }

        public double getViewHeight(double dataHeight) { return dataHeight * scaleY; }

        public double getDataY(double viewY) { return getDataY(viewY, false); }

        public double getReversedDataY(double viewY) { return getDataY(viewY, true); }

        public double getDataHeight(double viewHeight) { return viewHeight / scaleY; }


        private double getViewY(double dataY, boolean reverse) {
            if (isBottomBased() && !reverse || !isBottomBased() && reverse) {
                return row.getHeight() - (dataY - bounds.y) * scaleY - getOffsetY() +
                       getViewInsets().top - marginBottom + row.getOffset();
            } else {
                return (dataY - bounds.y) * scaleY - getOffsetY() +
                       getViewInsets().top + marginTop + row.getOffset();
            }
        }

        private double getDataY(double viewY, boolean reverse) {
            if ((isBottomBased() && !reverse) || (!isBottomBased() && reverse)) {
                return bounds.y - (viewY + getViewInsets().bottom - marginBottom -
                       getOffsetY() - getHeight()) / scaleY;
            } else {
                return (viewY + getOffsetY() - getViewInsets().top - marginTop) /
                       scaleY + bounds.y;
            }
        }

    }


    private static class RowComparator implements Comparator<Row> {

        public int compare(Row r1, Row r2) {
            int r1i = r1.getIndex();
            int r2i = r2.getIndex();
            return (r1i < r2i ? -1 : (r1i == r2i ? 0 : 1));
        }

    }


    public static interface RowListener {

        public void rowsAdded(List<Row> rows);

        public void rowsRemoved(List<Row> rows);

        public void rowsResized(List<Row> rows);

    }

}
