/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.main;

import icy.gui.frame.IcyInternalFrame;
import icy.gui.util.ComponentUtil;
import icy.gui.util.LookAndFeelUtil;
import icy.gui.viewer.Viewer;
import icy.main.Icy;
import icy.math.HungarianAlgorithm;
import icy.resource.ResourceUtil;
import icy.util.GraphicsUtil;
import icy.util.Random;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.image.ImageObserver;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;

/**
 * Icy {@link JDesktopPane} class.<br>
 * This is the main container of the application.<br>
 * It support overlays so we can use to display message, notification or logo in
 * background. First added overlays is painted first, so take care of that. Call
 * the IcyDesktopPane.repaint() method to update overlays.
 * 
 * @author Fabrice & Stephane
 */
public class IcyDesktopPane extends JDesktopPane implements ContainerListener, MouseListener, MouseMotionListener,
        MouseWheelListener
{
    public static interface DesktopOverlay extends MouseListener, MouseMotionListener, MouseWheelListener
    {
        public void paint(Graphics g, int width, int height);
    }

    public static class AbstractDesktopOverlay extends MouseAdapter implements DesktopOverlay
    {
        @Override
        public void paint(Graphics g, int width, int height)
        {
            // nothing by default
        }
    }

    /**
     * Background overlay.
     */
    public class BackgroundDesktopOverlay extends AbstractDesktopOverlay implements ImageObserver
    {
        private final static String BACKGROUND_PATH = "background/";

        private final Image backGround;
        private final Image icyLogo;
        private final Color textColor;
        private final Color bgTextColor;

        public BackgroundDesktopOverlay()
        {
            super();

            // load random background (nor really random as we have only one right now)
            backGround = ResourceUtil.getImage(BACKGROUND_PATH + Integer.toString(Random.nextInt(1)) + ".jpg");
            // load Icy logo
            icyLogo = ResourceUtil.getImage("logoICY.png");

            // default text colors
            textColor = new Color(0, 0, 0, 0.5f);
            bgTextColor = new Color(1, 1, 1, 0.5f);
        }

        @Override
        public void paint(Graphics g, int width, int height)
        {
            final IcyDesktopPane desktop = Icy.getMainInterface().getDesktopPane();
            final Color bgColor;

            if (desktop != null)
                bgColor = LookAndFeelUtil.getBackground(desktop);
            else
                bgColor = Color.lightGray;

            final int bgImgWidth = backGround.getWidth(this);
            final int bgImgHeight = backGround.getHeight(this);

            // compute image scaling
            final double scale = Math.max((double) width / (double) bgImgWidth, (double) height / (double) bgImgHeight) * 1.5d;
            final Graphics2D g2 = (Graphics2D) g.create();

            // fill background color
            g2.setBackground(bgColor);
            g2.clearRect(0, 0, width, height);

            // paint image over background in transparency
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.2f));
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2.drawImage(backGround, 0, 0, (int) (scale * bgImgWidth), (int) (scale * bgImgHeight), bgColor, this);

            final String text = "Version " + Icy.version;
            final int textWidth = (int) GraphicsUtil.getStringBounds(g, text).getWidth();

            // draw version text
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.7f));
            g2.setColor(bgTextColor);
            g2.drawString(text, width - (textWidth + 31), height - 8);
            g2.setColor(textColor);
            g2.drawString(text, width - (textWidth + 30), height - 9);
            // and draw Icy text logo
            g2.drawImage(icyLogo, width - 220, height - 130, this);

            g2.dispose();
        }

        @Override
        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
        {
            if ((infoflags & ImageObserver.ALLBITS) != 0)
            {
                repaint();
                return false;
            }

            return true;
        }
    }

    private static final long serialVersionUID = 7914161180763257329L;

    private final ComponentAdapter componentAdapter;
    // private final InternalFrameAdapter internalFrameAdapter;

    private final ArrayList<DesktopOverlay> overlays;

    public IcyDesktopPane()
    {
        super();

        overlays = new ArrayList<DesktopOverlay>();

        // setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);

        componentAdapter = new ComponentAdapter()
        {
            @Override
            public void componentResized(ComponentEvent e)
            {
                checkPosition((JInternalFrame) e.getSource());
            }

            @Override
            public void componentMoved(ComponentEvent e)
            {
                checkPosition((JInternalFrame) e.getSource());
            }
        };

        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
        addContainerListener(this);

        // add the background overlay
        overlays.add(new BackgroundDesktopOverlay());
    }

    // int i = 0;

    @Override
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);

        final int w = getWidth();
        final int h = getHeight();

        // paint overlays
        for (DesktopOverlay overlay : overlays)
            overlay.paint(g, w, h);
    }

    private void registerFrame(JInternalFrame frame)
    {
        frame.addComponentListener(componentAdapter);
    }

    void unregisterFrame(JInternalFrame frame)
    {
        frame.removeComponentListener(componentAdapter);
    }

    void checkPosition(JInternalFrame frame)
    {
        final Rectangle rect = frame.getBounds();

        if (fixPosition(rect))
            frame.setBounds(rect);
    }

    boolean fixPosition(Rectangle rect)
    {
        final int limit = getY();
        if (rect.y < limit)
        {
            rect.y = limit;
            return true;
        }

        return false;
    }

    /**
     * Returns the list of internal viewers.
     * 
     * @param wantNotVisible
     *        Also return not visible viewers
     * @param wantIconized
     *        Also return iconized viewers
     */
    public static Viewer[] getInternalViewers(boolean wantNotVisible, boolean wantIconized)
    {
        final List<Viewer> result = new ArrayList<Viewer>();

        for (Viewer viewer : Icy.getMainInterface().getViewers())
        {
            if (viewer.isInternalized())
            {
                final IcyInternalFrame internalFrame = viewer.getIcyInternalFrame();

                if ((wantNotVisible || internalFrame.isVisible()) && (wantIconized || !internalFrame.isIcon()))
                    result.add(viewer);
            }
        }

        return result.toArray(new Viewer[result.size()]);
    }

    /**
     * Organize all internal viewers in cascade
     */
    public void organizeCascade()
    {
        // get internal viewers
        final Viewer[] viewers = getInternalViewers(false, false);

        // available space (always keep 32 available pixels at south)
        final int w = getWidth();
        final int h = getHeight() - 32;

        final int fw = (int) (w * 0.6f);
        final int fh = (int) (h * 0.6f);

        final int xMax = w - 0;
        final int yMax = h - 0;

        int x = 0 + 32;
        int y = 0 + 32;

        for (Viewer v : viewers)
        {
            final IcyInternalFrame internalFrame = v.getIcyInternalFrame();

            if (internalFrame.isMaximized())
                internalFrame.setMaximized(false);
            internalFrame.setBounds(x, y, fw, fh);
            internalFrame.toFront();

            x += 30;
            y += 20;
            if ((x + fw) > xMax)
                x = 32;
            if ((y + fh) > yMax)
                y = 32;
        }
    }

    /**
     * Organize all internal viewers in tile.
     * 
     * @param type
     *        tile type<br>
     *        MainFrame.TILE_HORIZONTAL, MainFrame.TILE_VERTICAL or
     *        MainFrame.TILE_GRID
     */
    public void organizeTile(int type)
    {
        // get internal viewers
        final Viewer[] viewers = getInternalViewers(false, false);

        final int numFrames = viewers.length;

        // nothing to do
        if (numFrames == 0)
            return;

        // available space (always keep 32 available pixels at south)
        final int w = getWidth();
        final int h = getHeight() - 32;

        int numCol;
        int numLine;

        switch (type)
        {
            case MainFrame.TILE_HORIZONTAL:
                numCol = 1;
                numLine = numFrames;
                break;

            case MainFrame.TILE_VERTICAL:
                numCol = numFrames;
                numLine = 1;
                break;

            default:
                numCol = (int) Math.sqrt(numFrames);
                if (numFrames != (numCol * numCol))
                    numCol++;
                numLine = numFrames / numCol;
                if (numFrames > (numCol * numLine))
                    numLine++;
                break;
        }

        final double[][] framesDistances = new double[numCol * numLine][numFrames];

        final int dx = w / numCol;
        final int dy = h / numLine;
        int k = 0;

        for (int i = 0; i < numLine; i++)
        {
            for (int j = 0; j < numCol; j++, k++)
            {
                final double[] distances = framesDistances[k];
                final double x = (j * dx) + (dx / 2d);
                final double y = (i * dy) + (dy / 2d);

                for (int f = 0; f < numFrames; f++)
                {
                    final Point2D.Double center = ComponentUtil.getCenter(viewers[f].getInternalFrame());
                    distances[f] = Point2D.distanceSq(center.x, center.y, x, y);
                }
            }
        }

        final int[] framePos = new HungarianAlgorithm(framesDistances).resolve();

        k = 0;
        for (int i = 0; i < numLine; i++)
        {
            for (int j = 0; j < numCol; j++, k++)
            {
                final int f = framePos[k];

                if (f < numFrames)
                {
                    final IcyInternalFrame internalFrame = viewers[f].getIcyInternalFrame();

                    if (internalFrame.isMaximized())
                        internalFrame.setMaximized(false);
                    internalFrame.setBounds(j * dx, i * dy, dx, dy);
                    internalFrame.toFront();
                }
            }
        }
    }

    /**
     * @deprecated Use {@link #organizeTile(int)} instead.
     */
    @Deprecated
    public void organizeTile()
    {
        organizeTile(MainFrame.TILE_GRID);
    }

    /**
     * Add the specified overlay to the desktop.
     */
    public void addOverlay(DesktopOverlay overlay)
    {
        if (!overlays.contains(overlay))
            overlays.add(overlay);
    }

    /**
     * remove the specified overlay from the desktop.
     */
    public boolean removeOverlay(DesktopOverlay overlay)
    {
        return overlays.remove(overlay);
    }

    @Override
    public void componentAdded(ContainerEvent e)
    {
        final Component comp = e.getChild();

        if (comp instanceof JInternalFrame)
            registerFrame((JInternalFrame) comp);
    }

    @Override
    public void componentRemoved(ContainerEvent e)
    {
        final Component comp = e.getChild();

        if (comp instanceof JInternalFrame)
            unregisterFrame((JInternalFrame) comp);
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseWheelMoved(e);
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseDragged(e);
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseMoved(e);
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseClicked(e);
    }

    @Override
    public void mousePressed(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mousePressed(e);
    }

    @Override
    public void mouseReleased(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseReleased(e);
    }

    @Override
    public void mouseEntered(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseEntered(e);
    }

    @Override
    public void mouseExited(MouseEvent e)
    {
        // send to overlays
        for (DesktopOverlay overlay : overlays)
            overlay.mouseExited(e);
    }
}