/*
 * Copyright (c) 2012, 2016, 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 javafx.scene.control;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

/**
 * A package protected util class used by TableView and TreeTableView to reduce
 * the level of code duplication.
 */
class TableUtil {

    private TableUtil() {
        // no-op
    }

    static void removeTableColumnListener(List<? extends TableColumnBase> list,
                        final InvalidationListener columnVisibleObserver,
                        final InvalidationListener columnSortableObserver,
                        final InvalidationListener columnSortTypeObserver,
                        final InvalidationListener columnComparatorObserver) {

        if (list == null) return;
        for (TableColumnBase col : list) {
            col.visibleProperty().removeListener(columnVisibleObserver);
            col.sortableProperty().removeListener(columnSortableObserver);
            col.comparatorProperty().removeListener(columnComparatorObserver);

//            col.sortTypeProperty().removeListener(columnSortTypeObserver);
            if (col instanceof TableColumn) {
                ((TableColumn)col).sortTypeProperty().removeListener(columnSortTypeObserver);
            } else if (col instanceof TreeTableColumn) {
                ((TreeTableColumn)col).sortTypeProperty().removeListener(columnSortTypeObserver);
            }

            removeTableColumnListener(col.getColumns(),
                                      columnVisibleObserver,
                                      columnSortableObserver,
                                      columnSortTypeObserver,
                                      columnComparatorObserver);
        }
    }

    static void addTableColumnListener(List<? extends TableColumnBase> list,
                        final InvalidationListener columnVisibleObserver,
                        final InvalidationListener columnSortableObserver,
                        final InvalidationListener columnSortTypeObserver,
                        final InvalidationListener columnComparatorObserver) {

        if (list == null) return;
        for (TableColumnBase col : list) {
            col.visibleProperty().addListener(columnVisibleObserver);
            col.sortableProperty().addListener(columnSortableObserver);
            col.comparatorProperty().addListener(columnComparatorObserver);

            if (col instanceof TableColumn) {
                ((TableColumn)col).sortTypeProperty().addListener(columnSortTypeObserver);
            } else if (col instanceof TreeTableColumn) {
                ((TreeTableColumn)col).sortTypeProperty().addListener(columnSortTypeObserver);
            }

            addTableColumnListener(col.getColumns(),
                                   columnVisibleObserver,
                                   columnSortableObserver,
                                   columnSortTypeObserver,
                                   columnComparatorObserver);
        }
    }

    static void removeColumnsListener(List<? extends TableColumnBase> list, ListChangeListener cl) {
        if (list == null) return;

        for (TableColumnBase col : list) {
            col.getColumns().removeListener(cl);
            removeColumnsListener(col.getColumns(), cl);
        }
    }

    static void addColumnsListener(List<? extends TableColumnBase> list, ListChangeListener cl) {
        if (list == null) return;

        for (TableColumnBase col : list) {
            col.getColumns().addListener(cl);
            addColumnsListener(col.getColumns(), cl);
        }
    }

    static void handleSortFailure(ObservableList<? extends TableColumnBase> sortOrder,
            SortEventType sortEventType, final Object... supportInfo) {
        // if the sort event is consumed we need to back out the previous
        // action so that the UI is not in an incorrect state
        if (sortEventType == SortEventType.COLUMN_SORT_TYPE_CHANGE) {
            // go back to the previous sort type
            final TableColumnBase changedColumn = (TableColumnBase) supportInfo[0];
            revertSortType(changedColumn);
        } else if (sortEventType == SortEventType.SORT_ORDER_CHANGE) {
            // Revert the sortOrder list to what it was previously
            ListChangeListener.Change change = (ListChangeListener.Change) supportInfo[0];

            final List toRemove = new ArrayList();
            final List toAdd = new ArrayList();
            while (change.next()) {
                if (change.wasAdded()) {
                    toRemove.addAll(change.getAddedSubList());
                }

                if (change.wasRemoved()) {
                    toAdd.addAll(change.getRemoved());
                }
            }

            sortOrder.removeAll(toRemove);
            sortOrder.addAll(toAdd);
        } else if (sortEventType == SortEventType.COLUMN_SORTABLE_CHANGE) {
            // no-op - it is ok for the sortable type to remain as-is
        } else if (sortEventType == SortEventType.COLUMN_COMPARATOR_CHANGE) {
            // no-op - it is ok for the comparator to remain as-is
        }
    }

    private static void revertSortType(TableColumnBase changedColumn) {
        if (changedColumn instanceof TableColumn) {
            TableColumn tableColumn = (TableColumn)changedColumn;
            final TableColumn.SortType sortType = tableColumn.getSortType();
            if (sortType == TableColumn.SortType.ASCENDING) {
                tableColumn.setSortType(null);
            } else if (sortType == TableColumn.SortType.DESCENDING) {
                tableColumn.setSortType(TableColumn.SortType.ASCENDING);
            } else if (sortType == null) {
                tableColumn.setSortType(TableColumn.SortType.DESCENDING);
            }
        } else if (changedColumn instanceof TreeTableColumn) {
            TreeTableColumn tableColumn = (TreeTableColumn)changedColumn;
            final TreeTableColumn.SortType sortType = tableColumn.getSortType();
            if (sortType == TreeTableColumn.SortType.ASCENDING) {
                tableColumn.setSortType(null);
            } else if (sortType == TreeTableColumn.SortType.DESCENDING) {
                tableColumn.setSortType(TreeTableColumn.SortType.ASCENDING);
            } else if (sortType == null) {
                tableColumn.setSortType(TreeTableColumn.SortType.DESCENDING);
            }
        }
    }

    static enum SortEventType {
         SORT_ORDER_CHANGE,
         COLUMN_SORT_TYPE_CHANGE,
         COLUMN_SORTABLE_CHANGE,
         COLUMN_COMPARATOR_CHANGE
     }





    /**
     * The constrained resize algorithm used by TableView and TreeTableView.
     * @param prop
     * @param isFirstRun
     * @param tableWidth
     * @param visibleLeafColumns
     * @return
     */
    static boolean constrainedResize(ResizeFeaturesBase prop,
                                     boolean isFirstRun,
                                     double tableWidth,
                                     List<? extends TableColumnBase<?,?>> visibleLeafColumns) {
        TableColumnBase<?,?> column = prop.getColumn();
        double delta = prop.getDelta();

        /*
         * There are two phases to the constrained resize policy:
         *   1) Ensuring internal consistency (i.e. table width == sum of all visible
         *      columns width). This is often called when the table is resized.
         *   2) Resizing the given column by __up to__ the given delta.
         *
         * It is possible that phase 1 occur and there be no need for phase 2 to
         * occur.
         */

        boolean isShrinking;
        double target;
        double totalLowerBound = 0;
        double totalUpperBound = 0;

        if (tableWidth == 0) return false;

        /*
         * PHASE 1: Check to ensure we have internal consistency. Based on the
         *          Swing JTable implementation.
         */
        // determine the width of all visible columns, and their preferred width
        double colWidth = 0;
        for (TableColumnBase<?,?> col : visibleLeafColumns) {
            colWidth += col.getWidth();
        }

        if (Math.abs(colWidth - tableWidth) > 1) {
            isShrinking = colWidth > tableWidth;
            target = tableWidth;

            if (isFirstRun) {
                // if we are here we have an inconsistency - these two values should be
                // equal when this resizing policy is being used.
                for (TableColumnBase<?,?> col : visibleLeafColumns) {
                    totalLowerBound += col.getMinWidth();
                    totalUpperBound += col.getMaxWidth();
                }

                // We run into trouble if the numbers are set to infinity later on
                totalUpperBound = totalUpperBound == Double.POSITIVE_INFINITY ?
                    Double.MAX_VALUE :
                    (totalUpperBound == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : totalUpperBound);

                for (TableColumnBase col : visibleLeafColumns) {
                    double lowerBound = col.getMinWidth();
                    double upperBound = col.getMaxWidth();

                    // Check for zero. This happens when the distribution of the delta
                    // finishes early due to a series of "fixed" entries at the end.
                    // In this case, lowerBound == upperBound, for all subsequent terms.
                    double newSize;
                    if (Math.abs(totalLowerBound - totalUpperBound) < .0000001) {
                        newSize = lowerBound;
                    } else {
                        double f = (target - totalLowerBound) / (totalUpperBound - totalLowerBound);
                        newSize = Math.round(lowerBound + f * (upperBound - lowerBound));
                    }

                    double remainder = resize(col, newSize - col.getWidth());

                    target -= newSize + remainder;
                    totalLowerBound -= lowerBound;
                    totalUpperBound -= upperBound;
                }

                isFirstRun = false;
            } else {
                double actualDelta = tableWidth - colWidth;
                List<? extends TableColumnBase<?,?>> cols = visibleLeafColumns;
                resizeColumns(cols, actualDelta);
            }
        }

        // At this point we can be happy in the knowledge that we have internal
        // consistency, i.e. table width == sum of the width of all visible
        // leaf columns.

        /*
         * Column may be null if we just changed the resize policy, and we
         * just wanted to enforce internal consistency, as mentioned above.
         */
        if (column == null) {
            return false;
        }

        /*
         * PHASE 2: Handling actual column resizing (by the user). Based on my own
         *          implementation (based on the UX spec).
         */

        isShrinking = delta < 0;

        // need to find the last leaf column of the given column - it is this
        // column that we actually resize from. If this column is a leaf, then we
        // use it.
        TableColumnBase<?,?> leafColumn = column;
        while (leafColumn.getColumns().size() > 0) {
            leafColumn = leafColumn.getColumns().get(leafColumn.getColumns().size() - 1);
        }

        int colPos = visibleLeafColumns.indexOf(leafColumn);
        int endColPos = visibleLeafColumns.size() - 1;

        // we now can split the observableArrayList into two subobservableArrayLists, representing all
        // columns that should grow, and all columns that should shrink
        //    var growingCols = if (isShrinking)
        //        then table.visibleLeafColumns[colPos+1..endColPos]
        //        else table.visibleLeafColumns[0..colPos];
        //    var shrinkingCols = if (isShrinking)
        //        then table.visibleLeafColumns[0..colPos]
        //        else table.visibleLeafColumns[colPos+1..endColPos];


        double remainingDelta = delta;
        while (endColPos > colPos && remainingDelta != 0) {
            TableColumnBase<?,?> resizingCol = visibleLeafColumns.get(endColPos);
            endColPos--;

            // if the column width is fixed, break out and try the next column
            if (! resizingCol.isResizable()) continue;

            // for convenience we discern between the shrinking and growing columns
            TableColumnBase<?,?> shrinkingCol = isShrinking ? leafColumn : resizingCol;
            TableColumnBase<?,?> growingCol = !isShrinking ? leafColumn : resizingCol;

            //        (shrinkingCol.width == shrinkingCol.minWidth) or (growingCol.width == growingCol.maxWidth)

            if (growingCol.getWidth() > growingCol.getPrefWidth()) {
                // growingCol is willing to be generous in this case - it goes
                // off to find a potentially better candidate to grow
                List<? extends TableColumnBase> seq = visibleLeafColumns.subList(colPos + 1, endColPos + 1);
                for (int i = seq.size() - 1; i >= 0; i--) {
                    TableColumnBase<?,?> c = seq.get(i);
                    if (c.getWidth() < c.getPrefWidth()) {
                        growingCol = c;
                        break;
                    }
                }
            }
            //
            //        if (shrinkingCol.width < shrinkingCol.prefWidth) {
            //            for (c in reverse table.visibleLeafColumns[colPos+1..endColPos]) {
            //                if (c.width > c.prefWidth) {
            //                    shrinkingCol = c;
            //                    break;
            //                }
            //            }
            //        }



            double sdiff = Math.min(Math.abs(remainingDelta), shrinkingCol.getWidth() - shrinkingCol.getMinWidth());

//                System.out.println("\tshrinking " + shrinkingCol.getText() + " and growing " + growingCol.getText());
//                System.out.println("\t\tMath.min(Math.abs("+remainingDelta+"), "+shrinkingCol.getWidth()+" - "+shrinkingCol.getMinWidth()+") = " + sdiff);

            double delta1 = resize(shrinkingCol, -sdiff);
            double delta2 = resize(growingCol, sdiff);
            remainingDelta += isShrinking ? sdiff : -sdiff;
        }
        return remainingDelta == 0;
    }

    // function used to actually perform the resizing of the given column,
    // whilst ensuring it stays within the min and max bounds set on the column.
    // Returns the remaining delta if it could not all be applied.
    static double resize(TableColumnBase column, double delta) {
        if (delta == 0) return 0.0F;
        if (! column.isResizable()) return delta;

        final boolean isShrinking = delta < 0;
        final List<TableColumnBase<?,?>> resizingChildren = getResizableChildren(column, isShrinking);

        if (resizingChildren.size() > 0) {
            return resizeColumns(resizingChildren, delta);
        } else {
            double newWidth = column.getWidth() + delta;

            if (newWidth > column.getMaxWidth()) {
                column.doSetWidth(column.getMaxWidth());
                return newWidth - column.getMaxWidth();
            } else if (newWidth < column.getMinWidth()) {
                column.doSetWidth(column.getMinWidth());
                return newWidth - column.getMinWidth();
            } else {
                column.doSetWidth(newWidth);
                return 0.0F;
            }
        }
    }

    // Returns all children columns of the given column that are able to be
    // resized. This is based on whether they are visible, resizable, and have
    // not space before they hit the min / max values.
    private static List<TableColumnBase<?,?>> getResizableChildren(TableColumnBase<?,?> column, boolean isShrinking) {
        if (column == null || column.getColumns().isEmpty()) {
            return Collections.emptyList();
        }

        List<TableColumnBase<?,?>> tablecolumns = new ArrayList<TableColumnBase<?,?>>();
        for (TableColumnBase c : column.getColumns()) {
            if (! c.isVisible()) continue;
            if (! c.isResizable()) continue;

            if (isShrinking && c.getWidth() > c.getMinWidth()) {
                tablecolumns.add(c);
            } else if (!isShrinking && c.getWidth() < c.getMaxWidth()) {
                tablecolumns.add(c);
            }
        }
        return tablecolumns;
    }

    private static double resizeColumns(List<? extends TableColumnBase<?,?>> columns, double delta) {
        // distribute space between all visible children who can be resized.
        // To do this we need to work out if we're shrinking or growing the
        // children, and then which children can be resized based on their
        // min/pref/max/fixed properties. The results of this are in the
        // resizingChildren observableArrayList above.
        final int columnCount = columns.size();

        // work out how much of the delta we should give to each child. It should
        // be an equal amount (at present), although perhaps we'll allow for
        // functions to calculate this at a later date.
        double colDelta = delta / columnCount;

        // we maintain a count of the amount of delta remaining to ensure that
        // the column resize operation accurately reflects the location of the
        // mouse pointer. Every time this value is not 0, the UI is a teeny bit
        // more inaccurate whilst the user continues to resize.
        double remainingDelta = delta;

        // We maintain a count of the current column that we're on in case we
        // need to redistribute the remainingDelta among remaining sibling.
        int col = 0;

        // This is a bit hacky - often times the leftOverDelta is zero, but
        // remainingDelta doesn't quite get down to 0. In these instances we
        // short-circuit and just return 0.0.
        boolean isClean = true;
        for (TableColumnBase<?,?> childCol : columns) {
            col++;

            // resize each child column
            double leftOverDelta = resize(childCol, colDelta);

            // calculate the remaining delta if the was anything left over in
            // the last resize operation
            remainingDelta = remainingDelta - colDelta + leftOverDelta;

            //      println("\tResized {childCol.text} with {colDelta}, but {leftOverDelta} was left over. RemainingDelta is now {remainingDelta}");

            if (leftOverDelta != 0) {
                isClean = false;
                // and recalculate the distribution of the remaining delta for
                // the remaining siblings.
                colDelta = remainingDelta / (columnCount - col);
            }
        }

        // see isClean above for why this is done
        return isClean ? 0.0 : remainingDelta;
    }

}
