/*
 * Copyright (c) 2003, 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 sun.awt.X11;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.AdjustmentEvent;
import java.util.ArrayList;
import java.util.Iterator;
import sun.util.logging.PlatformLogger;

// FIXME: implement multi-select
/*
 * Class to paint a list of items, possibly with scrollbars
 * This class paints all items with the same font
 * For now, this class manages the list of items and painting thereof, but not
 * posting of Item or ActionEvents
 */
final class ListHelper implements XScrollbarClient {
    private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.X11.ListHelper");

    private final int FOCUS_INSET = 1;

    private final int BORDER_WIDTH; // Width of border drawn around the list
                                    // of items
    private final int ITEM_MARGIN;  // Margin between the border of the list
                                    // of items and item's bg, and between
                                    // items
    private final int TEXT_SPACE;   // Space between the edge of an item and
                                    // the text

    private final int SCROLLBAR_WIDTH;  // Width of a scrollbar

    private java.util.List<String> items;        // List of items

    // TODO: maybe this would be better as a simple int[]
    private java.util.List<Integer> selected;     // List of selected items
    private boolean multiSelect;         // Can multiple items be selected
                                         // at once?
    private int focusedIndex;

    private int maxVisItems;             // # items visible without a vsb
    private XVerticalScrollbar vsb;      // null if unsupported
    private boolean vsbVis;
    private XHorizontalScrollbar hsb;    // null if unsupported
    private boolean hsbVis;

    private Font font;
    private FontMetrics fm;

    private XWindow peer;   // So far, only needed for painting
                            // on notifyValue()
    private Color[] colors; // Passed in for painting on notifyValue()

    // Holds the true if mouse is dragging outside of the area of the list
    // The flag is used at the moment of the dragging and releasing mouse
    // See 6243382 for more information
    private boolean mouseDraggedOutVertically = false;
    private volatile boolean vsbVisibilityChanged = false;

    /*
     * Comment
     */
    ListHelper(XWindow peer, Color[] colors, int initialSize,
               boolean multiSelect, boolean scrollVert, boolean scrollHoriz,
               Font font, int maxVisItems, int SPACE, int MARGIN, int BORDER,
               int SCROLLBAR) {
        this.peer = peer;
        this.colors = colors;
        this.multiSelect = multiSelect;
        items = new ArrayList<>(initialSize);
        selected = new ArrayList<>(1);
        selected.add(Integer.valueOf(-1));

        this.maxVisItems = maxVisItems;
        if (scrollVert) {
            vsb = new XVerticalScrollbar(this);
            vsb.setValues(0, 0, 0, 0, 1, maxVisItems - 1);
        }
        if (scrollHoriz) {
            hsb = new XHorizontalScrollbar(this);
            hsb.setValues(0, 0, 0, 0, 1, 1);
        }

        setFont(font);
        TEXT_SPACE = SPACE;
        ITEM_MARGIN = MARGIN;
        BORDER_WIDTH = BORDER;
        SCROLLBAR_WIDTH = SCROLLBAR;
    }

    @Override
    public Component getEventSource() {
        return peer.getEventSource();
    }

    /**********************************************************************/
    /* List management methods                                            */
    /**********************************************************************/

    void add(String item) {
        items.add(item);
        updateScrollbars();
    }

    void add(String item, int index) {
        items.add(index, item);
        updateScrollbars();
    }

    void remove(String item) {
        // FIXME: need to clean up select list, too?
        items.remove(item);
        updateScrollbars();
        // Is vsb visible now?
    }

    void remove(int index) {
        // FIXME: need to clean up select list, too?
        items.remove(index);
        updateScrollbars();
        // Is vsb visible now?
    }

    void removeAll() {
        items.removeAll(items);
        updateScrollbars();
    }

    void setMultiSelect(boolean ms) {
        multiSelect = ms;
    }

    /*
     * docs.....definitely docs
     * merely keeps internal track of which items are selected for painting
     * dealing with target Components happens elsewhere
     */
    void select(int index) {
        if (index > getItemCount() - 1) {
            index = (isEmpty() ? -1 : 0);
        }
        if (multiSelect) {
            assert false : "Implement ListHelper.select() for multiselect";
        }
        else if (getSelectedIndex() != index) {
            selected.remove(0);
            selected.add(Integer.valueOf(index));
            makeVisible(index);
        }
    }

    /* docs */
    void deselect(int index) {
        assert(false);
    }

    /* docs */
    /* if called for multiselect, return -1 */
    int getSelectedIndex() {
        if (!multiSelect) {
            Integer val = selected.get(0);
            return val.intValue();
        }
        return -1;
    }

    int[] getSelectedIndexes() { assert(false); return null;}

    /*
     * A getter method for XChoicePeer.
     * Returns vsbVisiblityChanged value and sets it to false.
     */
    boolean checkVsbVisibilityChangedAndReset(){
        boolean returnVal = vsbVisibilityChanged;
        vsbVisibilityChanged = false;
        return returnVal;
    }

    boolean isEmpty() {
        return items.isEmpty();
    }

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

    String getItem(int index) {
        return items.get(index);
    }

    /**********************************************************************/
    /* GUI-related methods                                                */
    /**********************************************************************/

    void setFocusedIndex(int index) {
        focusedIndex = index;
    }

    private boolean isFocusedIndex(int index) {
        return index == focusedIndex;
    }

    @SuppressWarnings("deprecation")
    void setFont(Font newFont) {
        if (newFont != font) {
            font = newFont;
            fm = Toolkit.getDefaultToolkit().getFontMetrics(font);
            // Also cache stuff like fontHeight?
        }
    }

    /*
     * Returns width of the text of the longest item
     */
    int getMaxItemWidth() {
        int m = 0;
        int end = getItemCount();
        for(int i = 0 ; i < end ; i++) {
            int l = fm.stringWidth(getItem(i));
            m = Math.max(m, l);
        }
        return m;
    }

    /*
     * Height of an item (this doesn't include ITEM_MARGIN)
     */
    int getItemHeight() {
        return fm.getHeight() + (2*TEXT_SPACE);
    }

    int y2index(int y) {
        if (log.isLoggable(PlatformLogger.Level.FINE)) {
            log.fine("y=" + y +", firstIdx=" + firstDisplayedIndex() +", itemHeight=" + getItemHeight()
                     + ",item_margin=" + ITEM_MARGIN);
        }
        // See 6243382 for more information
        int newIdx = firstDisplayedIndex() + ((y - 2*ITEM_MARGIN) / (getItemHeight() + 2*ITEM_MARGIN));
        return newIdx;
    }

    /* write these
    int index2y(int);
    public int numItemsDisplayed() {}
    */

    int firstDisplayedIndex() {
        if (vsbVis) {
            return vsb.getValue();
        }
        return 0;
    }

    int lastDisplayedIndex() {
        // FIXME: need to account for horiz scroll bar
        if (hsbVis) {
            assert false : "Implement for horiz scroll bar";
        }

        return vsbVis ? vsb.getValue() + maxVisItems - 1: getItemCount() - 1;
    }

    /*
     * If the given index is not visible in the List, scroll so that it is.
     */
    private void makeVisible(int index) {
        if (vsbVis) {
            if (index < firstDisplayedIndex()) {
                vsb.setValue(index);
            }
            else if (index > lastDisplayedIndex()) {
                vsb.setValue(index - maxVisItems + 1);
            }
        }
    }

    // FIXME: multi-select needs separate focused index
    void up() {
        int curIdx = getSelectedIndex();
        int numItems = getItemCount();
        int newIdx;

        assert curIdx >= 0;

        if (curIdx == 0) {
            newIdx = numItems - 1;
        }
        else {
            newIdx = --curIdx;
        }
        // focus(newIdx);
        select(newIdx);
    }

    void down() {
        int newIdx = (getSelectedIndex() + 1) % getItemCount();
        select(newIdx);
    }

    void pageUp() {
        // FIXME: for multi-select, move the focused item, not the selected item
        if (vsbVis && firstDisplayedIndex() > 0) {
            if (multiSelect) {
                assert false : "Implement pageUp() for multiSelect";
            }
            else {
                int selectionOffset = getSelectedIndex() - firstDisplayedIndex();
                // the vsb does bounds checking
                int newIdx = firstDisplayedIndex() - vsb.getBlockIncrement();
                vsb.setValue(newIdx);
                select(firstDisplayedIndex() + selectionOffset);
            }
        }
    }
    void pageDown() {
        if (vsbVis && lastDisplayedIndex() < getItemCount() - 1) {
            if (multiSelect) {
                assert false : "Implement pageDown() for multiSelect";
            }
            else {
                int selectionOffset = getSelectedIndex() - firstDisplayedIndex();
                // the vsb does bounds checking
                int newIdx = lastDisplayedIndex();
                vsb.setValue(newIdx);
                select(firstDisplayedIndex() + selectionOffset);
            }
        }
    }
    void home() {}
    void end() {}


    boolean isVSBVisible() { return vsbVis; }
    boolean isHSBVisible() { return hsbVis; }

    XVerticalScrollbar getVSB() { return vsb; }
    XHorizontalScrollbar getHSB() { return hsb; }

    boolean isInVertSB(Rectangle bounds, int x, int y) {
        if (vsbVis) {
            assert vsb != null : "Vert scrollbar is visible, yet is null?";
            int sbHeight = hsbVis ? bounds.height - SCROLLBAR_WIDTH : bounds.height;
            return (x <= bounds.width) &&
                   (x >= bounds.width - SCROLLBAR_WIDTH) &&
                   (y >= 0) &&
                   (y <= sbHeight);
        }
        return false;
    }

    boolean isInHorizSB(Rectangle bounds, int x, int y) {
        if (hsbVis) {
            assert hsb != null : "Horiz scrollbar is visible, yet is null?";

            int sbWidth = vsbVis ? bounds.width - SCROLLBAR_WIDTH : bounds.width;
            return (x <= sbWidth) &&
                   (x >= 0) &&
                   (y >= bounds.height - SCROLLBAR_WIDTH) &&
                   (y <= bounds.height);
        }
        return false;
    }
    @SuppressWarnings("deprecation")
    void handleVSBEvent(MouseEvent e, Rectangle bounds, int x, int y) {
        int sbHeight = hsbVis ? bounds.height - SCROLLBAR_WIDTH : bounds.height;

        vsb.handleMouseEvent(e.getID(),
                             e.getModifiers(),
                             x - (bounds.width - SCROLLBAR_WIDTH),
                             y);
    }

    /*
     * Called when items are added/removed.
     * Update whether the scrollbar is visible or not, scrollbar values
     */
    private void updateScrollbars() {
        boolean oldVsbVis = vsbVis;
        vsbVis = vsb != null && items.size() > maxVisItems;
        if (vsbVis) {
            vsb.setValues(vsb.getValue(), getNumItemsDisplayed(),
                          vsb.getMinimum(), items.size());
        }

        // 6405689. If Vert Scrollbar gets disappeared from the dropdown menu we should repaint whole dropdown even if
        // no actual resize gets invoked. This is needed because some painting artifacts remained between dropdown items
        // but draw3DRect doesn't clear the area inside. Instead it just paints lines as borders.
        vsbVisibilityChanged = (vsbVis != oldVsbVis);
        // FIXME: check if added item makes a hsb necessary (if supported, that of course)
    }

    private int getNumItemsDisplayed() {
        return items.size() > maxVisItems ? maxVisItems : items.size();
    }

    @Override
    public void repaintScrollbarRequest(XScrollbar sb) {
        Graphics g = peer.getGraphics();
        Rectangle bounds = peer.getBounds();
        if ((sb == vsb) && vsbVis) {
            paintVSB(g, XComponentPeer.getSystemColors(), bounds);
        }
        else if ((sb == hsb) && hsbVis) {
            paintHSB(g, XComponentPeer.getSystemColors(), bounds);
        }
        g.dispose();
    }

    @Override
    public void notifyValue(XScrollbar obj, int type, int v, boolean isAdjusting) {
        if (obj == vsb) {
            int oldScrollValue = vsb.getValue();
            vsb.setValue(v);
            boolean needRepaint = (oldScrollValue != vsb.getValue());
            // See 6243382 for more information
            if (mouseDraggedOutVertically){
                int oldItemValue = getSelectedIndex();
                int newItemValue = getSelectedIndex() + v - oldScrollValue;
                select(newItemValue);
                needRepaint = needRepaint || (getSelectedIndex() != oldItemValue);
            }

            // FIXME: how are we going to paint!?
            Graphics g = peer.getGraphics();
            Rectangle bounds = peer.getBounds();
            int first = v;
            int last = Math.min(getItemCount() - 1,
                                v + maxVisItems);
            if (needRepaint) {
                paintItems(g, colors, bounds, first, last);
            }
            g.dispose();

        }
        else if ((XHorizontalScrollbar)obj == hsb) {
            hsb.setValue(v);
            // FIXME: how are we going to paint!?
        }
    }

    void updateColors(Color[] newColors) {
        colors = newColors;
    }

    /*
    public void paintItems(Graphics g,
                           Color[] colors,
                           Rectangle bounds,
                           Font font,
                           int first,
                           int last,
                           XVerticalScrollbar vsb,
                           XHorizontalScrollbar hsb) {
    */
    void paintItems(Graphics g,
                           Color[] colors,
                           Rectangle bounds) {
        // paint border
        // paint items
        // paint scrollbars
        // paint focus?

    }
    void paintAllItems(Graphics g,
                           Color[] colors,
                           Rectangle bounds) {
        paintItems(g, colors, bounds,
                   firstDisplayedIndex(), lastDisplayedIndex());
    }
    private void paintItems(Graphics g, Color[] colors, Rectangle bounds,
                            int first, int last) {
        peer.flush();
        int x = BORDER_WIDTH + ITEM_MARGIN;
        int width = bounds.width - 2*ITEM_MARGIN - 2*BORDER_WIDTH - (vsbVis ? SCROLLBAR_WIDTH : 0);
        int height = getItemHeight();
        int y = BORDER_WIDTH + ITEM_MARGIN;

        for (int i = first; i <= last ; i++) {
            paintItem(g, colors, getItem(i),
                      x, y, width, height,
                      isItemSelected(i),
                      isFocusedIndex(i));
            y += height + 2*ITEM_MARGIN;
        }

        if (vsbVis) {
            paintVSB(g, XComponentPeer.getSystemColors(), bounds);
        }
        if (hsbVis) {
            paintHSB(g, XComponentPeer.getSystemColors(), bounds);
        }
        peer.flush();
        // FIXME: if none of the items were focused, paint focus around the
        // entire list.  This is how java.awt.List should work.
    }

    /*
     * comment about what is painted (i.e. the focus rect
     */
    private void paintItem(Graphics g, Color[] colors, String string, int x,
                           int y, int width, int height, boolean selected,
                           boolean focused) {
        //System.out.println("LP.pI(): x="+x+" y="+y+" w="+width+" h="+height);
        //g.setColor(colors[BACKGROUND_COLOR]);

        // FIXME: items shouldn't draw into the scrollbar

        if (selected) {
            g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
        }
        else {
            g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
        }
        g.fillRect(x, y, width, height);

        if (focused) {
            //g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
            g.setColor(Color.BLACK);
            g.drawRect(x + FOCUS_INSET,
                       y + FOCUS_INSET,
                       width - 2*FOCUS_INSET,
                       height - 2*FOCUS_INSET);
        }

        if (selected) {
            g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
        }
        else {
            g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
        }
        g.setFont(font);
        //Rectangle clip = g.getClipBounds();
        //g.clipRect(x, y, width, height);
        //g.drawString(string, x + TEXT_SPACE, y + TEXT_SPACE + ITEM_MARGIN);

        int fontAscent = fm.getAscent();
        int fontDescent = fm.getDescent();

        g.drawString(string, x + TEXT_SPACE, y + (height + fm.getMaxAscent() - fm.getMaxDescent())/2);
        //g.clipRect(clip.x, clip.y, clip.width, clip.height);
    }

    private boolean isItemSelected(int index) {
        Iterator<Integer> itr = selected.iterator();
        while (itr.hasNext()) {
            Integer val = itr.next();
            if (val.intValue() == index) {
                return true;
            }
        }
        return false;
    }

    private void paintVSB(Graphics g, Color[] colors, Rectangle bounds) {
        int height = bounds.height - 2*BORDER_WIDTH - (hsbVis ? (SCROLLBAR_WIDTH-2) : 0);
        Graphics ng = g.create();

        g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
        try {
            ng.translate(bounds.width - BORDER_WIDTH - SCROLLBAR_WIDTH,
                         BORDER_WIDTH);
            // Update scrollbar's size
            vsb.setSize(SCROLLBAR_WIDTH, bounds.height);
            vsb.paint(ng, colors, true);
        } finally {
            ng.dispose();
        }
    }

    private void paintHSB(Graphics g, Color[] colors, Rectangle bounds) {

    }

    /*
     * Helper method for Components with integrated scrollbars.
     * Pass in the vertical and horizontal scroll bar (or null for none/hidden)
     * and the MouseWheelEvent, and the appropriate scrollbar will be scrolled
     * correctly.
     * Returns whether or not scrolling actually took place.  This will indicate
     * whether or not repainting is required.
     */
    static boolean doWheelScroll(XVerticalScrollbar vsb,
                                     XHorizontalScrollbar hsb,
                                     MouseWheelEvent e) {
        XScrollbar scroll = null;
        int wheelRotation;

        // Determine which, if any, sb to scroll
        if (vsb != null) {
            scroll = vsb;
        }
        else if (hsb != null) {
            scroll = hsb;
        }
        else { // Neither scrollbar is showing
            return false;
        }

        wheelRotation = e.getWheelRotation();

        // Check if scroll is necessary
        if ((wheelRotation < 0 && scroll.getValue() > scroll.getMinimum()) ||
            (wheelRotation > 0 && scroll.getValue() < scroll.getMaximum()) ||
            wheelRotation != 0) {

            int type = e.getScrollType();
            int incr;
            if (type == MouseWheelEvent.WHEEL_BLOCK_SCROLL) {
                incr = wheelRotation * scroll.getBlockIncrement();
            }
            else { // type is WHEEL_UNIT_SCROLL
                incr = e.getUnitsToScroll() * scroll.getUnitIncrement();
            }
            scroll.setValue(scroll.getValue() + incr);
            return true;
        }
        return false;
    }

    /*
     * Helper method for XChoicePeer with integrated vertical scrollbar.
     * Start or stop vertical scrolling when mouse dragged in / out the area of the list if it's required
     * Restoring Motif behavior
     * See 6243382 for more information
     */
    void trackMouseDraggedScroll(int mouseX, int mouseY, int listWidth, int listHeight){

        if (!mouseDraggedOutVertically){
            if (vsb.beforeThumb(mouseX, mouseY)) {
                vsb.setMode(AdjustmentEvent.UNIT_DECREMENT);
            } else {
                vsb.setMode(AdjustmentEvent.UNIT_INCREMENT);
            }
        }

        if(!mouseDraggedOutVertically && (mouseY < 0 || mouseY >= listHeight)){
            mouseDraggedOutVertically = true;
            vsb.startScrollingInstance();
        }

        if (mouseDraggedOutVertically && mouseY >= 0 && mouseY < listHeight && mouseX >= 0 && mouseX < listWidth){
            mouseDraggedOutVertically = false;
            vsb.stopScrollingInstance();
        }
    }

    /*
     * Helper method for XChoicePeer with integrated vertical scrollbar.
     * Stop vertical scrolling when mouse released in / out the area of the list if it's required
     * Restoring Motif behavior
     * see 6243382 for more information
     */
    void trackMouseReleasedScroll(){

        if (mouseDraggedOutVertically){
            mouseDraggedOutVertically = false;
            vsb.stopScrollingInstance();
        }

    }
}