/*
 * Copyright (c) 2011, 2025, 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 com.apple.laf;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;

import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.*;

import com.apple.laf.AquaUtils.RecyclableSingleton;

import apple.laf.*;
import apple.laf.JRSUIConstants.*;
import apple.laf.JRSUIState.AnimationFrameState;

/**
 * AquaTreeUI supports the client property "value-add" system of customization See MetalTreeUI
 * This is heavily based on the 1.3.1 AquaTreeUI implementation.
 */
public final class AquaTreeUI extends BasicTreeUI {

    // Create PLAF
    public static ComponentUI createUI(final JComponent c) {
        return new AquaTreeUI();
    }

    // Begin Line Stuff from Metal

    private static final String LINE_STYLE = "JTree.lineStyle";

    private static final String LEG_LINE_STYLE_STRING = "Angled";
    private static final String HORIZ_STYLE_STRING = "Horizontal";
    private static final String NO_STYLE_STRING = "None";

    private static final int LEG_LINE_STYLE = 2;
    private static final int HORIZ_LINE_STYLE = 1;
    private static final int NO_LINE_STYLE = 0;

    private int lineStyle = HORIZ_LINE_STYLE;
    private final PropertyChangeListener lineStyleListener = new LineListener();

    // mouse tracking state
    protected TreePath fTrackingPath;
    protected boolean fIsPressed = false;
    protected boolean fIsInBounds = false;
    protected int fAnimationFrame = -1;
    protected TreeArrowMouseInputHandler fMouseHandler;

    protected final AquaPainter<AnimationFrameState> painter = AquaPainter.create(JRSUIStateFactory.getDisclosureTriangle());

    public AquaTreeUI() {

    }

    @Override
    public void installUI(final JComponent c) {
        super.installUI(c);

        final Object lineStyleFlag = c.getClientProperty(LINE_STYLE);
        decodeLineStyle(lineStyleFlag);
        c.addPropertyChangeListener(lineStyleListener);
    }

    @Override
    public void uninstallUI(final JComponent c) {
        c.removePropertyChangeListener(lineStyleListener);
        super.uninstallUI(c);
    }

    /**
     * Creates the focus listener to repaint the focus ring
     */
    @Override
    protected FocusListener createFocusListener() {
        return new AquaTreeUI.FocusHandler();
    }

    /**
     * this function converts between the string passed into the client property and the internal representation
     * (currently an int)
     */
    protected void decodeLineStyle(final Object lineStyleFlag) {
        if (lineStyleFlag == null || NO_STYLE_STRING.equals(lineStyleFlag)) {
            lineStyle = NO_LINE_STYLE; // default case
            return;
        }

        if (LEG_LINE_STYLE_STRING.equals(lineStyleFlag)) {
            lineStyle = LEG_LINE_STYLE;
        } else if (HORIZ_STYLE_STRING.equals(lineStyleFlag)) {
            lineStyle = HORIZ_LINE_STYLE;
        }
    }

    @Override
    public TreePath getClosestPathForLocation(final JTree treeLocal, final int x, final int y) {
        if (treeLocal == null || treeState == null) return null;

        Insets i = treeLocal.getInsets();
        if (i == null) i = new Insets(0, 0, 0, 0);
        return treeState.getPathClosestTo(x - i.left, y - i.top);
    }

    @Override
    public void paint(final Graphics g, final JComponent c) {
        super.paint(g, c);

        // Paint the lines
        if (lineStyle == HORIZ_LINE_STYLE && !largeModel) {
            paintHorizontalSeparators(g, c);
        }
    }

    protected void paintHorizontalSeparators(final Graphics g, final JComponent c) {
        g.setColor(UIManager.getColor("Tree.line"));

        final Rectangle clipBounds = g.getClipBounds();

        final int beginRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y));
        final int endRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y + clipBounds.height - 1));

        if (beginRow <= -1 || endRow <= -1) { return; }

        for (int i = beginRow; i <= endRow; ++i) {
            final TreePath path = getPathForRow(tree, i);

            if (path != null && path.getPathCount() == 2) {
                final Rectangle rowBounds = getPathBounds(tree, getPathForRow(tree, i));

                // Draw a line at the top
                if (rowBounds != null) g.drawLine(clipBounds.x, rowBounds.y, clipBounds.x + clipBounds.width, rowBounds.y);
            }
        }
    }

    @Override
    protected void paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final TreePath path) {
        if (lineStyle == LEG_LINE_STYLE) {
            super.paintVerticalPartOfLeg(g, clipBounds, insets, path);
        }
    }

    @Override
    protected void paintHorizontalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) {
        if (lineStyle == LEG_LINE_STYLE) {
            super.paintHorizontalPartOfLeg(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
        }
    }

    /** This class listens for changes in line style */
    final class LineListener implements PropertyChangeListener {
        @Override
        public void propertyChange(final PropertyChangeEvent e) {
            final String name = e.getPropertyName();
            if (name.equals(LINE_STYLE)) {
                decodeLineStyle(e.getNewValue());
            }
        }
    }

    /**
     * Paints the expand (toggle) part of a row. The receiver should NOT modify {@code clipBounds}, or
     * {@code insets}.
     */
    @Override
    protected void paintExpandControl(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) {
        final Object value = path.getLastPathComponent();

        // Draw icons if not a leaf and either hasn't been loaded,
        // or the model child count is > 0.
        if (isLeaf || (hasBeenExpanded && treeModel.getChildCount(value) <= 0)) return;

        final boolean isLeftToRight = AquaUtils.isLeftToRight(tree); // Basic knows, but keeps it private

        final State state = getState(path);

        // if we are not animating, do the expected thing, and use the icon
        // also, if there is a custom (non-LaF defined) icon - just use that instead
        if (fAnimationFrame == -1 && state != State.PRESSED) {
            super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
            return;
        }

        // Both icons are the same size
        final Icon icon = isExpanded ? getExpandedIcon() : getCollapsedIcon();
        if (!(icon instanceof UIResource)) {
            super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
            return;
        }

        // if painting a right-to-left knob, we ensure that we are only painting when
        // the clipbounds rect is set to the exact size of the knob, and positioned correctly
        // (this code is not the same as metal)
        int middleXOfKnob;
        if (isLeftToRight) {
            middleXOfKnob = bounds.x - (getRightChildIndent() - 1);
        } else {
            middleXOfKnob = clipBounds.x + clipBounds.width / 2;
        }

        // Center vertically
        final int middleYOfKnob = bounds.y + (bounds.height / 2);

        final int x = middleXOfKnob - icon.getIconWidth() / 2;
        final int y = middleYOfKnob - icon.getIconHeight() / 2;
        final int height = icon.getIconHeight(); // use the icon height so we don't get drift  we modify the bounds (by changing row height)
        final int width = 20; // this is a hardcoded value from our default icon (since we are only at this point for animation)

        setupPainter(state, isExpanded, isLeftToRight);
        painter.paint(g, tree, x, y, width, height);
    }

    @Override
    public Icon getCollapsedIcon() {
        final Icon icon = super.getCollapsedIcon();
        if (AquaUtils.isLeftToRight(tree)) return icon;
        if (!(icon instanceof UIResource)) return icon;
        return UIManager.getIcon("Tree.rightToLeftCollapsedIcon");
    }

    protected void setupPainter(State state, final boolean isExpanded, final boolean leftToRight) {
        if (!fIsInBounds && state == State.PRESSED) state = State.ACTIVE;

        painter.state.set(state);
        if (JRSUIUtils.Tree.useLegacyTreeKnobs()) {
            if (fAnimationFrame == -1) {
                painter.state.set(isExpanded ? Direction.DOWN : Direction.RIGHT);
            } else {
                painter.state.set(Direction.NONE);
                painter.state.setAnimationFrame(fAnimationFrame - 1);
            }
        } else {
            painter.state.set(getDirection(isExpanded, leftToRight));
            painter.state.setAnimationFrame(fAnimationFrame);
        }
    }

    protected Direction getDirection(final boolean isExpanded, final boolean isLeftToRight) {
        if (isExpanded && (fAnimationFrame == -1)) return Direction.DOWN;
        return isLeftToRight ? Direction.RIGHT : Direction.LEFT;
    }

    protected State getState(final TreePath path) {
        if (!tree.isEnabled()) return State.DISABLED;
        if (fIsPressed) {
            if (fTrackingPath.equals(path)) return State.PRESSED;
        }
        return State.ACTIVE;
    }

    /**
     * Misnamed - this is called on mousePressed Macs shouldn't react till mouseReleased
     * We install a motion handler that gets removed after.
     * See super.MouseInputHandler & super.startEditing for why
     */
    @Override
    protected void handleExpandControlClick(final TreePath path, final int mouseX, final int mouseY) {
        fMouseHandler = new TreeArrowMouseInputHandler(path);
    }

    /**
     * Returning true signifies a mouse event on the node should toggle the selection of only the row under mouse.
     */
    @Override
    protected boolean isToggleSelectionEvent(final MouseEvent event) {
        return SwingUtilities.isLeftMouseButton(event) && event.isMetaDown();
    }

    final class FocusHandler extends BasicTreeUI.FocusHandler {
        @Override
        public void focusGained(final FocusEvent e) {
            super.focusGained(e);
            AquaBorder.repaintBorder(tree);
        }

        @Override
        public void focusLost(final FocusEvent e) {
            super.focusLost(e);
            AquaBorder.repaintBorder(tree);
        }
    }

    @Override
    protected PropertyChangeListener createPropertyChangeListener() {
        return new MacPropertyChangeHandler();
    }

    public final class MacPropertyChangeHandler extends PropertyChangeHandler {
        @Override
        public void propertyChange(final PropertyChangeEvent e) {
            final String prop = e.getPropertyName();
            if (prop.equals(AquaFocusHandler.FRAME_ACTIVE_PROPERTY)) {
                AquaBorder.repaintBorder(tree);
                AquaFocusHandler.swapSelectionColors("Tree", tree, e.getNewValue());
            } else {
                super.propertyChange(e);
            }
        }
    }

    /**
     * TreeArrowMouseInputHandler handles passing all mouse events the way a Mac should - hilite/dehilite on enter/exit,
     * only perform the action if released in arrow.
     *
     * Just like super.MouseInputHandler, this is removed once it's not needed, so they won't clash with each other
     */
    // The Adapters take care of defining all the empties
    final class TreeArrowMouseInputHandler extends MouseInputAdapter {
        protected Rectangle fPathBounds = new Rectangle();

        // Values needed for paintOneControl
        protected boolean fIsLeaf, fIsExpanded, fHasBeenExpanded;
        protected Rectangle fBounds, fVisibleRect;
        int fTrackingRow;
        Insets fInsets;
        Color fBackground;

        TreeArrowMouseInputHandler(final TreePath path) {
            fTrackingPath = path;
            fIsPressed = true;
            fIsInBounds = true;
            this.fPathBounds = getPathArrowBounds(path);
            tree.addMouseListener(this);
            tree.addMouseMotionListener(this);
            fBackground = tree.getBackground();
            if (!tree.isOpaque()) {
                final Component p = tree.getParent();
                if (p != null) fBackground = p.getBackground();
            }

            // Set up values needed to paint the triangle - see
            // BasicTreeUI.paint
            fVisibleRect = tree.getVisibleRect();
            fInsets = tree.getInsets();

            if (fInsets == null) fInsets = new Insets(0, 0, 0, 0);
            fIsLeaf = treeModel.isLeaf(path.getLastPathComponent());
            if (fIsLeaf) fIsExpanded = fHasBeenExpanded = false;
            else {
                fIsExpanded = treeState.getExpandedState(path);
                fHasBeenExpanded = tree.hasBeenExpanded(path);
            }
            final Rectangle boundsBuffer = new Rectangle();
            fBounds = treeState.getBounds(fTrackingPath, boundsBuffer);
            fBounds.x += fInsets.left;
            fBounds.y += fInsets.top;
            fTrackingRow = getRowForPath(fTrackingPath);

            paintOneControl();
        }

        @Override
        public void mouseDragged(final MouseEvent e) {
            fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
                paintOneControl();
            }

        @Override
        public void mouseExited(MouseEvent e) {
            fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
            paintOneControl();
        }

        @Override
        public void mouseReleased(final MouseEvent e) {
            if (tree == null) return;

            if (fIsPressed) {
                final boolean wasInBounds = fIsInBounds;

                fIsPressed = false;
                fIsInBounds = false;

                if (wasInBounds) {
                    fIsExpanded = !fIsExpanded;
                    paintAnimation(fIsExpanded);
                    if (e.isAltDown()) {
                        if (fIsExpanded) {
                            expandNode(fTrackingRow, true);
                        } else {
                            collapseNode(fTrackingRow, true);
                        }
                    } else {
                        toggleExpandState(fTrackingPath);
                    }
                }
            }
            fTrackingPath = null;
            removeFromSource();
        }

        protected void paintAnimation(final boolean expanding) {
            if (expanding) {
                paintAnimationFrame(1);
                paintAnimationFrame(2);
                paintAnimationFrame(3);
            } else {
                paintAnimationFrame(3);
                paintAnimationFrame(2);
                paintAnimationFrame(1);
            }
            fAnimationFrame = -1;
        }

        protected void paintAnimationFrame(final int frame) {
            fAnimationFrame = frame;
            paintOneControl();
            try { Thread.sleep(20); } catch (final InterruptedException e) { }
        }

        // Utility to paint just one widget while it's being tracked
        // Just doing "repaint" runs into problems if someone does "translate" on the graphics
        // (ie, Sun's JTreeTable example, which is used by Moneydance - see Radar 2697837)
        void paintOneControl() {
            if (tree == null) return;
            final Graphics g = tree.getGraphics();
            if (g == null) {
                // i.e. source is not displayable
                return;
            }

            try {
                g.setClip(fVisibleRect);
                // If we ever wanted a callback for drawing the arrow between
                // transition stages
                // the code between here and paintExpandControl would be it
                g.setColor(fBackground);
                g.fillRect(fPathBounds.x, fPathBounds.y, fPathBounds.width, fPathBounds.height);

                // if there is no tracking path, we don't need to paint anything
                if (fTrackingPath == null) return;

                // draw the vertical line to the parent
                final TreePath parentPath = fTrackingPath.getParentPath();
                if (parentPath != null) {
                    paintVerticalPartOfLeg(g, fPathBounds, fInsets, parentPath);
                    paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
                } else if (isRootVisible() && fTrackingRow == 0) {
                    paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
                }
                paintExpandControl(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
            } finally {
                g.dispose();
            }
        }

        protected void removeFromSource() {
            tree.removeMouseListener(this);
            tree.removeMouseMotionListener(this);
            }
        }

    protected int getRowForPath(final TreePath path) {
        return treeState.getRowForPath(path);
    }

    /**
     * see isLocationInExpandControl for bounds calc
     */
    protected Rectangle getPathArrowBounds(final TreePath path) {
        final Rectangle bounds = getPathBounds(tree, path); // Gives us the y values, but x is adjusted for the contents
        final Insets i = tree.getInsets();

        if (getExpandedIcon() != null) bounds.width = getExpandedIcon().getIconWidth();
        else bounds.width = 8;

        int boxLeftX = (i != null) ? i.left : 0;
        if (AquaUtils.isLeftToRight(tree)) {
            boxLeftX += (((path.getPathCount() + depthOffset - 2) * totalChildIndent) + getLeftChildIndent()) - bounds.width / 2;
        } else {
            boxLeftX += tree.getWidth() - 1 - ((path.getPathCount() - 2 + depthOffset) * totalChildIndent) - getLeftChildIndent() - bounds.width / 2;
        }
        bounds.x = boxLeftX;
        return bounds;
    }

    @Override
    protected void installKeyboardActions() {
        super.installKeyboardActions();
        tree.getActionMap().put("aquaExpandNode", new KeyboardExpandCollapseAction(true, false));
        tree.getActionMap().put("aquaCollapseNode", new KeyboardExpandCollapseAction(false, false));
        tree.getActionMap().put("aquaFullyExpandNode", new KeyboardExpandCollapseAction(true, true));
        tree.getActionMap().put("aquaFullyCollapseNode", new KeyboardExpandCollapseAction(false, true));
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    final class KeyboardExpandCollapseAction extends AbstractAction {
        /**
         * Determines direction to traverse, 1 means expand, -1 means collapse.
         */
        final boolean expand;
        final boolean recursive;

        /**
         * True if the selection is reset, false means only the lead path changes.
         */
        public KeyboardExpandCollapseAction(final boolean expand, final boolean recursive) {
            this.expand = expand;
            this.recursive = recursive;
        }

        @Override
        public void actionPerformed(final ActionEvent e) {
            if (tree == null || 0 > getRowCount(tree)) return;

            final TreePath[] selectionPaths = tree.getSelectionPaths();
            if (selectionPaths == null) return;

            for (int i = selectionPaths.length - 1; i >= 0; i--) {
                final TreePath path = selectionPaths[i];

                /*
                 * Try and expand the node, otherwise go to next node.
                 */
                if (expand) {
                    expandNode(tree.getRowForPath(path), recursive);
                    continue;
                }
                // else collapse

                // in the special case where there is only one row selected,
                // we want to do what the Cocoa does, and select the parent
                if (selectionPaths.length == 1 && tree.isCollapsed(path)) {
                    final TreePath parentPath = path.getParentPath();
                    if (parentPath != null && (!(parentPath.getParentPath() == null) || tree.isRootVisible())) {
                        tree.scrollPathToVisible(parentPath);
                        tree.setSelectionPath(parentPath);
                    }
                    continue;
                }

                collapseNode(tree.getRowForPath(path), recursive);
            }
        }

        @Override
        public boolean isEnabled() {
            return (tree != null && tree.isEnabled());
        }
    }

    void expandNode(final int row, final boolean recursive) {
        final TreePath path = getPathForRow(tree, row);
        if (path == null) return;

        tree.expandPath(path);
        if (!recursive) return;

        expandAllNodes(path, row + 1);
    }

    void expandAllNodes(final TreePath parent, final int initialRow) {
        for (int i = initialRow; true; i++) {
            final TreePath path = getPathForRow(tree, i);
            if (!parent.isDescendant(path)) return;

            tree.expandPath(path);
        }
    }

    void collapseNode(final int row, final boolean recursive) {
        final TreePath path = getPathForRow(tree, row);
        if (path == null) return;

        if (recursive) {
            collapseAllNodes(path, row + 1);
        }

        tree.collapsePath(path);
    }

    void collapseAllNodes(final TreePath parent, final int initialRow) {
        int lastRow = -1;
        for (int i = initialRow; lastRow == -1; i++) {
            final TreePath path = getPathForRow(tree, i);
            if (!parent.isDescendant(path)) {
                lastRow = i - 1;
            }
        }

        for (int i = lastRow; i >= initialRow; i--) {
            final TreePath path = getPathForRow(tree, i);
            tree.collapsePath(path);
        }
    }
}
