package com.gpl.rpg.atcontentstudio.ui;

import javax.swing.*;
import java.awt.*;

public class ScrollablePanel extends JPanel implements Scrollable, SwingConstants {

    private static final long serialVersionUID = 6498229143202972325L;

    public enum ScrollableSizeHint {
        NONE,
        FIT,
        STRETCH
    }

    public enum IncrementType {
        PERCENT,
        PIXELS
    }

    private ScrollableSizeHint scrollableHeight = ScrollableSizeHint.NONE;
    private ScrollableSizeHint scrollableWidth = ScrollableSizeHint.NONE;

    private IncrementInfo horizontalBlock;
    private IncrementInfo horizontalUnit;
    private IncrementInfo verticalBlock;
    private IncrementInfo verticalUnit;

    /**
     * Default constructor that uses a FlowLayout
     */
    public ScrollablePanel() {
        this(new FlowLayout());
    }

    /**
     * Constuctor for specifying the LayoutManager of the panel.
     *
     * @param layout the LayountManger for the panel
     */
    public ScrollablePanel(LayoutManager layout) {
        super(layout);

        IncrementInfo block = new IncrementInfo(IncrementType.PERCENT, 100);
        IncrementInfo unit = new IncrementInfo(IncrementType.PERCENT, 10);

        setScrollableBlockIncrement(HORIZONTAL, block);
        setScrollableBlockIncrement(VERTICAL, block);
        setScrollableUnitIncrement(HORIZONTAL, unit);
        setScrollableUnitIncrement(VERTICAL, unit);
    }

    /**
     * Get the height ScrollableSizeHint enum
     *
     * @return the ScrollableSizeHint enum for the height
     */
    public ScrollableSizeHint getScrollableHeight() {
        return scrollableHeight;
    }

    /**
     * Set the ScrollableSizeHint enum for the height. The enum is used to
     * determine the boolean value that is returned by the
     * getScrollableTracksViewportHeight() method. The valid values are:
     * <p>
     * ScrollableSizeHint.NONE - return "false", which causes the height
     * of the panel to be used when laying out the children
     * ScrollableSizeHint.FIT - return "true", which causes the height of
     * the viewport to be used when laying out the children
     * ScrollableSizeHint.STRETCH - return "true" when the viewport height
     * is greater than the height of the panel, "false" otherwise.
     *
     * @param scrollableHeight as represented by the ScrollableSizeHint enum.
     */
    public void setScrollableHeight(ScrollableSizeHint scrollableHeight) {
        this.scrollableHeight = scrollableHeight;
        revalidate();
    }

    /**
     * Get the width ScrollableSizeHint enum
     *
     * @return the ScrollableSizeHint enum for the width
     */
    public ScrollableSizeHint getScrollableWidth() {
        return scrollableWidth;
    }

    /**
     * Set the ScrollableSizeHint enum for the width. The enum is used to
     * determine the boolean value that is returned by the
     * getScrollableTracksViewportWidth() method. The valid values are:
     * <p>
     * ScrollableSizeHint.NONE - return "false", which causes the width
     * of the panel to be used when laying out the children
     * ScrollableSizeHint.FIT - return "true", which causes the width of
     * the viewport to be used when laying out the children
     * ScrollableSizeHint.STRETCH - return "true" when the viewport width
     * is greater than the width of the panel, "false" otherwise.
     *
     * @param scrollableWidth as represented by the ScrollableSizeHint enum.
     */
    public void setScrollableWidth(ScrollableSizeHint scrollableWidth) {
        this.scrollableWidth = scrollableWidth;
        revalidate();
    }

    /**
     * Get the block IncrementInfo for the specified orientation
     *
     * @return the block IncrementInfo for the specified orientation
     */
    public IncrementInfo getScrollableBlockIncrement(int orientation) {
        return orientation == SwingConstants.HORIZONTAL ? horizontalBlock : verticalBlock;
    }

    /**
     * Specify the information needed to do block scrolling.
     *
     * @param orientation specify the scrolling orientation. Must be either:
     *                    SwingContants.HORIZONTAL or SwingContants.VERTICAL.
     * @param amount      a value used with the IncrementType to determine the
     *                    scrollable amount
     * @paran type  specify how the amount parameter in the calculation of
     * the scrollable amount. Valid values are:
     * IncrementType.PERCENT - treat the amount as a % of the viewport size
     * IncrementType.PIXEL - treat the amount as the scrollable amount
     */
    public void setScrollableBlockIncrement(int orientation, IncrementType type, int amount) {
        IncrementInfo info = new IncrementInfo(type, amount);
        setScrollableBlockIncrement(orientation, info);
    }

    /**
     * Specify the information needed to do block scrolling.
     *
     * @param orientation specify the scrolling orientation. Must be either:
     *                    SwingContants.HORIZONTAL or SwingContants.VERTICAL.
     * @param info        An IncrementInfo object containing information of how to
     *                    calculate the scrollable amount.
     */
    public void setScrollableBlockIncrement(int orientation, IncrementInfo info) {
        switch (orientation) {
            case SwingConstants.HORIZONTAL:
                horizontalBlock = info;
                break;
            case SwingConstants.VERTICAL:
                verticalBlock = info;
                break;
            default:
                throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    }

    /**
     * Get the unit IncrementInfo for the specified orientation
     *
     * @return the unit IncrementInfo for the specified orientation
     */
    public IncrementInfo getScrollableUnitIncrement(int orientation) {
        return orientation == SwingConstants.HORIZONTAL ? horizontalUnit : verticalUnit;
    }

    /**
     * Specify the information needed to do unit scrolling.
     *
     * @param orientation specify the scrolling orientation. Must be either:
     *                    SwingContants.HORIZONTAL or SwingContants.VERTICAL.
     * @param amount      a value used with the IncrementType to determine the
     *                    scrollable amount
     * @paran type  specify how the amount parameter in the calculation of
     * the scrollable amount. Valid values are:
     * IncrementType.PERCENT - treat the amount as a % of the viewport size
     * IncrementType.PIXEL - treat the amount as the scrollable amount
     */
    public void setScrollableUnitIncrement(int orientation, IncrementType type, int amount) {
        IncrementInfo info = new IncrementInfo(type, amount);
        setScrollableUnitIncrement(orientation, info);
    }

    /**
     * Specify the information needed to do unit scrolling.
     *
     * @param orientation specify the scrolling orientation. Must be either:
     *                    SwingContants.HORIZONTAL or SwingContants.VERTICAL.
     * @param info        An IncrementInfo object containing information of how to
     *                    calculate the scrollable amount.
     */
    public void setScrollableUnitIncrement(int orientation, IncrementInfo info) {
        switch (orientation) {
            case SwingConstants.HORIZONTAL:
                horizontalUnit = info;
                break;
            case SwingConstants.VERTICAL:
                verticalUnit = info;
                break;
            default:
                throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    }

    //Implement Scrollable interface

    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    public int getScrollableUnitIncrement(
            Rectangle visible, int orientation, int direction) {
        switch (orientation) {
            case SwingConstants.HORIZONTAL:
                return getScrollableIncrement(horizontalUnit, visible.width);
            case SwingConstants.VERTICAL:
                return getScrollableIncrement(verticalUnit, visible.height);
            default:
                throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    }

    public int getScrollableBlockIncrement(
            Rectangle visible, int orientation, int direction) {
        switch (orientation) {
            case SwingConstants.HORIZONTAL:
                return getScrollableIncrement(horizontalBlock, visible.width);
            case SwingConstants.VERTICAL:
                return getScrollableIncrement(verticalBlock, visible.height);
            default:
                throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    }

    protected int getScrollableIncrement(IncrementInfo info, int distance) {
        if (info.getIncrement() == IncrementType.PIXELS)
            return info.getAmount();
        else
            return distance * info.getAmount() / 100;
    }

    public boolean getScrollableTracksViewportWidth() {
        if (scrollableWidth == ScrollableSizeHint.NONE)
            return false;

        if (scrollableWidth == ScrollableSizeHint.FIT)
            return true;

        //  STRETCH sizing, use the greater of the panel or viewport width

        if (getParent() instanceof JViewport) {
            return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
        }

        return false;
    }

    public boolean getScrollableTracksViewportHeight() {
        if (scrollableHeight == ScrollableSizeHint.NONE)
            return false;

        if (scrollableHeight == ScrollableSizeHint.FIT)
            return true;

        //  STRETCH sizing, use the greater of the panel or viewport height


        if (getParent() instanceof JViewport) {
            return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
        }

        return false;
    }

    /**
     * Helper class to hold the information required to calculate the scroll amount.
     */
    static class IncrementInfo {
        private IncrementType type;
        private int amount;

        public IncrementInfo(IncrementType type, int amount) {
            this.type = type;
            this.amount = amount;
        }

        public IncrementType getIncrement() {
            return type;
        }

        public int getAmount() {
            return amount;
        }

        public String toString() {
            return
                    "ScrollablePanel[" +
                            type + ", " +
                            amount + "]";
        }
    }
}
