package org.flexdock.docking.drag;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;



import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.defaults.DefaultDockingPort;
import org.flexdock.docking.drag.effects.EffectsManager;
import org.flexdock.docking.drag.effects.RubberBand;
import org.flexdock.util.RootWindow;
import org.flexdock.util.SwingUtility;
import org.flexdock.util.Utilities;

public class DragPipeline {

    private GlassPaneMonitor paneMonitor;
    private RootWindow[] windows;
    private HashMap rootWindowsByBounds;
    private DragGlasspane currentGlasspane;
    private DragGlasspane newGlassPane;
    private Rectangle[] windowBounds;
    private boolean heavyweightDockableSupportted;

    private boolean open;
    private DragOperation dragToken;
    private RubberBand rubberBand;

    public DragPipeline() {
        paneMonitor = new GlassPaneMonitor();
        rubberBand = EffectsManager.getRubberBand();
    }

    public boolean isOpen() {
        return open;
    }

    public void open(DragOperation token) {
        if(token==null)
            throw new NullPointerException("'token' parameter cannot be null.");

        if(EventQueue.isDispatchThread()) {
            openImpl(token);
            return;
        }


        final DragOperation dToken = token;
        try {
            EventQueue.invokeAndWait(new Runnable() {
                public void run() {
                    openImpl(dToken);
                }
            });
        } catch(Exception e) {
            System.err.println("Exception: " +e.getMessage());
        }
    }

    private void openImpl(DragOperation operation) {
        // check to see if we're going to support heavyweight dockables for this operation
        heavyweightDockableSupportted = Boolean.getBoolean(DockingConstants.HEAVYWEIGHT_DOCKABLES);

        this.dragToken = operation;

        // turn the current drag operation on
        setCurrentDragOperation(operation);

        windows = RootWindow.getVisibleWindows();

        windowBounds = new Rectangle[windows.length];
        rootWindowsByBounds = new HashMap();

        for(int i=0; i<windows.length; i++) {
            applyGlassPane(windows[i], createGlassPane());
            windowBounds[i] = windows[i].getBounds();
            rootWindowsByBounds.put(windowBounds[i], windows[i]);
        }

        // kill the rubberband if floating is not allowed
        if(!DragManager.isFloatingAllowed(operation.getDockableReference()))
            rubberBand = null;

        operation.start();
        open = true;
    }

    private DragGlasspane createGlassPane() {
        DragGlasspane pane = new DragGlasspane();
        pane.addMouseListener(paneMonitor);
        return pane;
    }

    private void applyGlassPane(RootWindow win, DragGlasspane pane) {
        pane.setRootWindow(win);
        pane.setCachedGlassPane(win.getGlassPane());
        win.setGlassPane(pane);
        pane.setVisible(true);
    }



    public void close() {
        if(!open)
            return;

        clearRubberBand();
        for(int i=0; i<windows.length; i++) {
            Component cmp = windows[i].getGlassPane();
            if(cmp instanceof DragGlasspane) {
                DragGlasspane pane = (DragGlasspane)cmp;
                pane.setVisible(false);
                cmp = pane.getCachedGlassPane();
//				pane.dispose();
                windows[i].setGlassPane(cmp);
                windows[i] = null;
            }
        }

        windowBounds = null;
        rootWindowsByBounds.clear();
        // turn the current drag operation off
        setCurrentDragOperation(null);
        open = false;
    }

    public void processDragEvent(MouseEvent me) {
        if(!open)
            return;

        if(EventQueue.isDispatchThread()) {
            processDragEventImpl(me);
            return;
        }

        final MouseEvent evt = me;
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                processDragEventImpl(evt);
            }
        });
    }

    private void processDragEventImpl(MouseEvent me) {
        dragToken.updateMouse(me);

        if(heavyweightDockableSupportted)
            preprocessHeavyweightDockables();

        me.consume();

        // hide the rubber band
        clearRubberBand();

        // track whether or not we're currently over a window
        dragToken.setOverWindow(newGlassPane!=null);

        // if the glasspane hasn't changed, then reprocess on the current glasspane
        if(newGlassPane==currentGlasspane) {
            dontSwitchGlassPanes();
            return;
        }

        // process transitions from a glasspane to a null area
        if(newGlassPane==null) {
            transitionToNullArea();
            return;
        }

        // process transitions from null area to a glasspane
        if(currentGlasspane==null) {
            transitionFromNullArea(newGlassPane);
            return;
        }

        // otherwise, transition from one glasspane to another
        // clear out the old glasspane
        currentGlasspane.clear();
        // reassign to the new glasspane
        currentGlasspane = newGlassPane;
        // now process the new glasspane and redraw the rubberband
        Rectangle screenRect = dragToken.getDragRect(true);
        currentGlasspane.setPostPainter(getPostPainter(screenRect));
        currentGlasspane.processDragEvent(dragToken);
    }

    private void dontSwitchGlassPanes() {
        // just redraw the rubberband if there's no current glasspane
        Rectangle screenRect = dragToken.getDragRect(true);
        if(currentGlasspane==null) {
            drawRubberBand(screenRect);
            return;
        }


        // otherwise, process the drag event on the current glasspane
        // and repaint it.
        // TODO: Fix post-painter on unchanged glasspane.
//		currentGlasspane.setPostPainter(getPostPainter(screenRect));
        currentGlasspane.setPostPainter(null);
        currentGlasspane.processDragEvent(dragToken);
    }

    private void transitionToNullArea() {
        // set the new glasspane reference
        DragGlasspane pane = currentGlasspane;
        currentGlasspane = null;

        // clear out the old glasspane and redraw the rubberband
        Rectangle screenRect = dragToken.getDragRect(true);
        pane.setPostPainter(null);
        pane.clear();
    }

    private void transitionFromNullArea(DragGlasspane newGlassPane) {
        // set the new glasspane reference
        currentGlasspane = newGlassPane;

        // process the new glasspane
        Rectangle screenRect = dragToken.getDragRect(true);
        currentGlasspane.setPostPainter(null);
        currentGlasspane.processDragEvent(dragToken);
    }


    private void setCurrentGlassPane(DragGlasspane gp) {
        newGlassPane = gp;
    }


    private Runnable getPostPainter(final Rectangle rect) {
//		if(!ResourceManager.isWindowsPlatform())
//			return null;

        return new Runnable() {
            public void run() {
                deferRubberBandDrawing(rect);
//				drawRubberBand(rect);
            }
        };
    }
    private void deferRubberBandDrawing(final Rectangle rect) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                drawRubberBand(rect);
            }
        });
    }

    private void drawRubberBand(Rectangle rect) {
        paintRubberBand(rect);
    }


    private class GlassPaneMonitor extends MouseAdapter {
        public void mouseEntered(MouseEvent me) {
            Object obj = me.getSource();
            if(obj instanceof DragGlasspane) {
                setCurrentGlassPane((DragGlasspane)obj);
            }
        }

        public void mouseExited(MouseEvent me) {
            setCurrentGlassPane(null);
        }
    }

    public DragOperation getDragToken() {
        return dragToken;
    }

    private void clearRubberBand() {
        if(rubberBand!=null)
            rubberBand.clear();
    }

    private void paintRubberBand(Rectangle rect) {
        if(rubberBand!=null)
            rubberBand.paint(rect);
    }

    private void setCurrentDragOperation(DragOperation operation) {
        DragOperation current = DragManager.getCurrentDragOperation();
        if(operation==current)
            return;

        DockingPort srcPort = operation==null? current.getSourcePort(): operation.getSourcePort();
        DragManager.setCurrentDragOperation(operation);
        if(srcPort instanceof Component)
            SwingUtility.repaint((Component)srcPort);

        // TODO: We want to get rid of this code in the future.  I don't like
        // having a public setDragInProgress() method on the default docking port
        // and having to know to call it at this level.  If the default docking port
        // is interested in whether a drag is currently in progress, it should
        // register some type of listener and handle its personal business internally
        // with its own code.
        if(srcPort instanceof DefaultDockingPort) {
            DefaultDockingPort port = (DefaultDockingPort)srcPort;
            port.setDragInProgress(operation!=null);
        }
    }

    private void preprocessHeavyweightDockables() {
        RootWindow targetWindow = getTargetWindow();

        if(newGlassPane==null && targetWindow!=null) {
            Component gp = targetWindow.getGlassPane();
            if(gp instanceof DragGlasspane) {
                setCurrentGlassPane((DragGlasspane)gp);
            }
        }
    }

    private RootWindow getTargetWindow() {
        Point screenLoc = dragToken.getCurrentMouse(true);
        for(int i=0; i<windowBounds.length; i++) {
            if(windowBounds[i].contains(screenLoc))
                return (RootWindow)rootWindowsByBounds.get(windowBounds[i]);
        }
        return null;
    }


}