/*
 * @(#)DefaultOverlayable.java 7/31/2007
 *
 * Copyright 2002 - 2007 JIDE Software Inc. All rights reserved.
 */

package com.jidesoft.swing;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * <code>DefaultOverlayable</code> is the default implementation of <code>Overlayable</code> using JPanel as the base
 * component.
 */
public class DefaultOverlayable extends JPanel implements Overlayable, ComponentListener {
    private JComponent _actualComponent;
    private Insets _overlayLocationInsets = new Insets(0, 0, 0, 0);
    private List<JComponent> _overlayComponents;
    private Map<JComponent, Integer> _overlayLocations;

    public DefaultOverlayable() {
        initComponents();
    }

    public DefaultOverlayable(JComponent component) {
        initComponents();
        setActualComponent(component);
    }

    public DefaultOverlayable(JComponent actualComponent, JComponent overlayComponent, int overlayLocation) {
        initComponents();
        setActualComponent(actualComponent);
        addOverlayComponent(overlayComponent, overlayLocation);
    }

    public DefaultOverlayable(JComponent actualComponent, JComponent overlayComponent) {
        initComponents();
        setActualComponent(actualComponent);
        addOverlayComponent(overlayComponent, SwingConstants.CENTER);
    }

    private void initComponents() {
        setLayout(null);
        _overlayComponents = new Vector<JComponent>();
        _overlayLocations = new Hashtable<JComponent, Integer>();
    }

    /**
     * Override to consider the overlayLocationInsets. If the overlayLocationInsets's edges are positive number, we will
     * increase the preferred size so that the overlayo component can be shown. If they are negative, we will still keep
     * the super.getPreferredSize.
     *
     * @return the preferred size of the DefaultOverlayable.
     */
    @Override
    public Dimension getPreferredSize() {
        Dimension size = getActualComponent() == null ? new Dimension(0, 0) : getActualComponent().getPreferredSize();
        Insets insets = getOverlayLocationInsets();
        if (insets != null) {
            size.width += Math.max(0, insets.left) + Math.max(0, insets.right);
            size.height += Math.max(0, insets.top) + Math.max(0, insets.bottom);
        }
        return size;
    }

    @Override
    public void setPreferredSize(Dimension preferredSize) {
        super.setPreferredSize(preferredSize);
        if (getActualComponent() != null) {
            if (preferredSize != null) {
                Insets insets = getOverlayLocationInsets();
                if (insets != null) {
                    preferredSize.width -= Math.max(0, insets.left) + Math.max(0, insets.right);
                    preferredSize.width = Math.max(0, preferredSize.width);
                    preferredSize.height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
                    preferredSize.height = Math.max(0, preferredSize.height);
                }
            }
            getActualComponent().setPreferredSize(preferredSize);
        }
    }

    @Override
    public Dimension getMinimumSize() {
        Dimension size = getActualComponent() == null ? new Dimension(0, 0) : getActualComponent().getMinimumSize();
        Insets insets = getOverlayLocationInsets();
        if (insets != null) {
            size.width += Math.max(0, insets.left) + Math.max(0, insets.right);
            size.height += Math.max(0, insets.top) + Math.max(0, insets.bottom);
        }
        return size;
    }

    @Override
    public void setMinimumSize(Dimension minimumSize) {
        super.setMinimumSize(minimumSize);
        if (getActualComponent() != null) {
            if (minimumSize != null) {
                Insets insets = getOverlayLocationInsets();
                if (insets != null) {
                    minimumSize.width -= Math.max(0, insets.left) + Math.max(0, insets.right);
                    minimumSize.width = Math.max(0, minimumSize.width);
                    minimumSize.height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
                    minimumSize.height = Math.max(0, minimumSize.height);
                }
            }
            getActualComponent().setMinimumSize(minimumSize);
        }
    }

    @Override
    public void setBounds(int x, int y, int width, int height) {
        super.setBounds(x, y, width, height);

        Insets insets = getOverlayLocationInsets();
        if (insets != null) {
            x = Math.max(0, insets.left);
            y = Math.max(0, insets.top);
            width -= Math.max(0, insets.left) + Math.max(0, insets.right);
            height -= Math.max(0, insets.top) + Math.max(0, insets.bottom);
        }
        if (getActualComponent() != null) {
            getActualComponent().setBounds(x, y, width, height);
        }

        updateLocation();
    }

    private void updateLocation() {
        JComponent[] components = getOverlayComponents();
        for (JComponent c : components) {
            if (c == null) {
                return;
            }

            Rectangle r = getOverlayComponentBounds(c);
            c.setBounds(r);
        }
    }

    /**
     * Gets the bounds of an overlay component.
     * @param component an overlay component
     * @return the bounds of the overlay component.
     */
    protected Rectangle getOverlayComponentBounds(JComponent component) {
        Component relativeComponent = getActualComponent();

        Rectangle bounds = relativeComponent.getBounds();
        if (relativeComponent != getActualComponent()) {
            bounds = SwingUtilities.convertRectangle(relativeComponent.getParent(), bounds, getActualComponent());
        }
        Rectangle overlayBounds = new Rectangle(bounds);
        Insets insets = getOverlayLocationInsets();
        overlayBounds.x -= insets.left;
        overlayBounds.y -= insets.top;
        overlayBounds.width += insets.left + insets.right;
        overlayBounds.height += insets.top + insets.bottom;

        int cx = 0;
        int cy = 0;

        Dimension size = component.getPreferredSize();
        int cw = size.width;
        int ch = size.height;

        int overlayLocation = getOverlayLocation(component);

        // adjust the location based on the orientation
        if (overlayLocation == LEADING) {
            if (component.getComponentOrientation() == ComponentOrientation.LEFT_TO_RIGHT) {
                overlayLocation = WEST;
            }
            else if (component.getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) {
                overlayLocation = EAST;
            }
        }
        else if (overlayLocation == TRAILING) {
            if (component.getComponentOrientation() == ComponentOrientation.LEFT_TO_RIGHT) {
                overlayLocation = EAST;
            }
            else if (component.getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) {
                overlayLocation = WEST;
            }
        }

        switch (overlayLocation) {
            case CENTER:
                cx = bounds.x + (bounds.width - cw) / 2;
                cy = bounds.y + (bounds.height - ch) / 2;
                break;
            case NORTH:
                cx = bounds.x + (bounds.width - cw) / 2;
                cy = overlayBounds.y;
                break;
            case SOUTH:
                cx = bounds.x + (bounds.width - cw) / 2;
                cy = overlayBounds.y + overlayBounds.height - ch;
                break;
            case WEST:
                cx = overlayBounds.x;
                cy = bounds.y + (bounds.height - ch) / 2;
                break;
            case EAST:
                cx = overlayBounds.x + overlayBounds.width - cw;
                cy = bounds.y + (bounds.height - ch) / 2;
                break;
            case NORTH_WEST:
                cx = overlayBounds.x;
                cy = overlayBounds.y;
                break;
            case NORTH_EAST:
                cx = overlayBounds.x + overlayBounds.width - cw;
                cy = overlayBounds.y;
                break;
            case SOUTH_WEST:
                cx = overlayBounds.x;
                cy = overlayBounds.y + overlayBounds.height - ch;
                break;
            case SOUTH_EAST:
                cx = overlayBounds.x + overlayBounds.width - cw;
                cy = overlayBounds.y + overlayBounds.height - ch;
                break;
        }

        return new Rectangle(cx, cy, cw, ch);
    }

    public int getOverlayLocation(JComponent component) {
        Integer location = _overlayLocations.get(component);
        if (location != null) {
            return location;
        }
        else {
            return -1;
        }
    }

    public void setOverlayLocation(JComponent component, int location) {
        setOverlayLocation(component, null, location);
    }

    @SuppressWarnings({"UnusedDeclaration"})
    private void setOverlayLocation(JComponent component, Component relativeComponent, int location) {
        boolean updated = false;
        int old = getOverlayLocation(component);
        if (old != location) {
            _overlayLocations.put(component, location);
            updated = true;
        }
        if (updated) {
            updateLocation();
        }
    }

    public void addOverlayComponent(JComponent component) {
        addOverlayComponent(component, SwingConstants.CENTER, -1);
    }

    public void addOverlayComponent(JComponent component, int location) {
        addOverlayComponent(component, location, -1);
    }

    public void addOverlayComponent(JComponent component, int location, int index) {
        addOverlayComponent(component, null, location, index);
    }

    private void addOverlayComponent(JComponent component, Component relativeComponent, int location, int index) {
        if (_overlayComponents.contains(component)) {
            _overlayComponents.remove(component);
        }
        if (index == -1) {
            _overlayComponents.add(component);
            add(component, getComponentCount() - 1); // add it before the the actual component
        }
        else {
            _overlayComponents.add(index, component);
            add(component, index);
        }
        setOverlayLocation(component, relativeComponent, location);
    }

    public void removeOverlayComponent(JComponent component) {
        if (_overlayComponents.contains(component)) {
            _overlayComponents.remove(component);
            _overlayLocations.remove(component);
            remove(component);
        }
    }

    public JComponent[] getOverlayComponents() {
        return _overlayComponents.toArray(new JComponent[_overlayComponents.size()]);
    }

    public JComponent getActualComponent() {
        return _actualComponent;
    }

    public void setActualComponent(JComponent actualComponent) {
        if (_actualComponent != null) {
            remove(_actualComponent);
            _actualComponent.putClientProperty(CLIENT_PROPERTY_OVERLAYABLE, null);
        }
        _actualComponent = actualComponent;
        _actualComponent.putClientProperty(CLIENT_PROPERTY_OVERLAYABLE, this);
        add(_actualComponent);
        Container container = getParent();
        if (container != null) {
            invalidate();
            container.validate();
        }
    }

    public Insets getOverlayLocationInsets() {
        return _overlayLocationInsets;
    }

    public void setOverlayLocationInsets(Insets overlayLocationInsets) {
        _overlayLocationInsets = overlayLocationInsets;
        Container container = getParent();
        if (container != null) {
            invalidate();
            container.validate();
        }
    }

    public void setOverlayVisible(boolean visible) {
        JComponent[] components = getOverlayComponents();
        for (JComponent component : components) {
            component.setVisible(visible);
        }
    }

    public void componentResized(ComponentEvent e) {
        updateLocation();
    }

    public void componentMoved(ComponentEvent e) {
        updateLocation();
    }

    public void componentShown(ComponentEvent e) {
        updateLocation();
    }

    public void componentHidden(ComponentEvent e) {
        updateLocation();
    }
}
