/*******************************************************************************
* Copyright (c)                                                                *
*           2005   Tristan Allwood,                                            *
*           2005   Daniel Burke,                                               *
*           2005   Marc Hull,                                                  *
*           2005   Ekaterina Itskova,                                          *
*           2005   Steve Zymler                                                *
*                                                                              *
* Permission is hereby granted, free of charge, to any person obtaining a      *
* copy of this software and associated documentation files (the                *
* "Software"), to deal in the Software without restriction, including          *
* without limitation the rights to use, copy, modify, merge, publish,          *
* distribute, sublicense, and/or sell copies of the Software, and to           *
* permit persons to whom the Software is furnished to do so, subject to        *
* the following conditions:                                                    *
*                                                                              *
* The above copyright notice and this permission notice shall be included      *
* in all copies or substantial portions of the Software.                       *
*                                                                              *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS      *
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF                   *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.       *
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY         *
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,         *
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE            *
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                       *
*******************************************************************************/
/*
 * Created on 20-Oct-2004
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package uk.ac.ic.doc.kevlar.gui.internal;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;

import uk.ac.ic.doc.kevlar.gui.internal.dragdrop.ADraggableObject;
import uk.ac.ic.doc.kevlar.gui.internal.dragdrop.IDropTarget;
import uk.ac.ic.doc.kevlar.gui.internal.taskpane.TaskPaneManager;
import uk.ac.ic.doc.kevlar.gui.internal.widgets.IDimensionListener;


/**
 * @author mfh02
 **/

public abstract class ADrawableObject {

    protected ADrawableObject parent;
	protected Rectangle area;
	protected Rectangle alignedArea;
	protected boolean alignRight;
	protected boolean alignBottom;
	protected boolean fillHorizontal;
	protected boolean fillVertical;
	protected Point parentSize;
	protected List<ADrawableObject> children;
	protected List<IDimensionListener> dimensionListeners;
	protected boolean visible;
	
	protected Image buffer;
	protected GC bufferGC;
	
	public ADrawableObject() {

	    initialise();
		
	}
	
	private void initialise() {
	    
		this.area = new Rectangle(0, 0, 0, 0);
		this.alignedArea = new Rectangle(0, 0, 0, 0);
		this.parentSize = new Point(0, 0);
		this.children = new ArrayList<ADrawableObject>();
		this.dimensionListeners = new ArrayList<IDimensionListener>();
		this.alignRight = false;
		this.alignBottom = false;
		this.fillHorizontal = false;
		this.fillVertical = false;
		this.visible = true;
		
	}
	
	public void setParent(ADrawableObject newParent) {
	    
	    if (parent != null) {
	        
	        parent.removeChild(this);
	        
	    }
	    
	    parent = newParent;
	    
	    if (parent != null) {
	    
	        parentResized(parent.getSize());
	        
	    }
	    
	}
	
	public void unsetParent() {
	    
	    parent = null;
	    
	}

	public ADrawableObject getParent() {
	    
	    return parent;
	    
	}
		
	public Rectangle getAlignedArea() {
	    
	    return alignedArea;
	    
	}
	
	public Rectangle getArea() {
	    
	    return area;
	    
	}
	
	public void setArea(Rectangle area) {
	    
	    this.alignedArea = area;
	    updateDimensions();
	    
	    for (int i=0; i<children.size(); i++) {
	    	
	    	children.get(i).parentResized(new Point(area.width, area.height));
	    	
	    }
	    
	    if (parent != null) {
	    	
	    	parent.childDimensionsChanged(this);
	    	
	    }
	    
	}
	
	public Point getPosition() {
	    
	    return new Point(alignedArea.x, alignedArea.y);
	    
	}
	
	public void setAlignment(boolean alignRight, boolean alignBottom) {
	    
	    this.alignRight = alignRight;
	    this.alignBottom = alignBottom;
	    updateDimensions();
	    
	}
	
	public boolean isAlignedRight() {
	    
	    return alignRight;
	    
	}
	
	public boolean isAlignedBottom() {
	    
	    return alignBottom;
	    
	}
	
	public boolean isFillHorizontal() {
	    
	    return fillHorizontal;
	    
	}
	
	public boolean isFillVertical() {
	    
	    return fillVertical;
	    
	}
	
	public void setPosition(Point position) {
	    
	    alignedArea.x = position.x;
	    alignedArea.y = position.y;
	    updateDimensions();
	    
	}
	
	public Point getSize() {
	    
	    return new Point(area.width, area.height);
	    
	}
	
	public void setSizeFillBoth(Point size) {
	    
	    alignedArea.width = size.x;
	    alignedArea.height = size.y; 
	    fillHorizontal = true;
	    fillVertical = true;
	    updateDimensions();
	    applyRedraw();
	    
	}
	    
	public void setSizeFillHorizontal(Point size) {
	    
	    alignedArea.width = size.x;
	    alignedArea.height = size.y; 
	    fillHorizontal = true;
	    updateDimensions();
	    applyRedraw();
	    
	}
	
	public void setSizeFillVertical(Point size) {
	    
	    alignedArea.width = size.x;
	    alignedArea.height = size.y;
	    fillVertical = true;
	    updateDimensions();
	    applyRedraw();
	    
	}
	
	public void setSize(Point size) {
	    
	    alignedArea.width = size.x;
	    alignedArea.height = size.y;
	    fillHorizontal = false;
	    fillVertical = false;
	    updateDimensions();
	    
/*	    if (parent != null) {
	    	
	    	parent.childDimensionsChanged(this);
	    	
	    } */
	    
	    applyRedraw();
	    
	}
		
	public void updateDimensions() {

	    redraw();
	    
	    Rectangle oldArea = new Rectangle(area.x, area.y, area.width, area.height);
	    
        if (alignRight) {
	        
            area.x = parentSize.x - alignedArea.x;
        
        } else {
            
            area.x = alignedArea.x;
            
        }
	    
        if (alignBottom) {
	        
            area.y = parentSize.y - alignedArea.y;
	        
        } else {
            
            area.y = alignedArea.y;
            
        }
	    
	    if (fillHorizontal) {
	        
	        area.width = parentSize.x - area.x - alignedArea.width;
	        
	    } else {
	        
	        area.width = alignedArea.width;
	        
	    }
	    
	    if (fillVertical) {
	        
	        area.height = parentSize.y - area.y - alignedArea.height;
	        
	    } else {
	        
	        area.height = alignedArea.height;
	        
	    }

	    if (area.x != oldArea.x || area.y != oldArea.y || area.width != oldArea.width || area.height != oldArea.height) {
	    
	        for (int i=0; i<children.size(); i++) {
	        
	            children.get(i).parentResized(new Point(area.width, area.height));
	        
	        }
	    
	        redraw();
	    
	        if (parent != null) {
	        
	            parent.childDimensionsChanged(this);
	        
	        }
	    
	        onDimensionsChanged();
	        
	    }
	    
	}
	
	public List<ADrawableObject> getChildren() {
	    
	    return children;
	    
	}
	
	public void show() {
	    
	    visible = true;
	    redraw();
	    
/*	    for (ADrawableObject child : children) {
	        
	        child.show();
	        
	    } */
	    
	}
	
	public void hide() {
	    
	    visible = false;
	    redraw();
	    
/*	    for (ADrawableObject child : children) {
	        
	        child.hide();
	        
	    } */
	    
	}
	
	public boolean isVisible() {
	    
	    return visible;
	    
	}
	
	public Rectangle getAbsoluteArea() {
	    
	    /**
	     * Returns the area of this drawable relative to the canvas' coordinate system.
	     * 
	     * NOTE: This ideally should not be used by any drawable, since the implementation
	     * of drawables should only know about their closest relatives in the hierarchy.
	     **/
	    
	    if (parent == null) {
	        
	        return area;
	        
	    } else {
	        
	        Rectangle parentArea = parent.getAbsoluteArea();
	        return new Rectangle(area.x+parentArea.x, area.y+parentArea.y, area.width, area.height);
	        
	    }
	    
	}
	
	public Point localToAbsolute(Point local) {

	    /**
	     * Converts an x,y point relative to this object's coordinate system to a point 
	     * relative to the canvas' coordinate system, by recursively passing the point to 
	     * each parent which translates it to their coordinate system.
	     **/
	    
	    if (parent != null) {
	        
	        return parent.localToAbsolute(new Point(local.x+area.x, local.y+area.y));
	        
	    } else {
	        
	        return new Point(local.x+area.x, local.y+area.y);
	        
	    }
	    
	}
	
	public Point absoluteToLocal(Point absolute) {
	    
	    /**
	     * Converts an x,y point relative to the canvas' coordinate system (i.e top-left
	     * corner of the form) to a point relative to this object's coordinate system,
	     * by recursively passing the point to each parent which translates it to their
	     * coordinate system.
	     **/
	    
	    if (parent != null) {
	        
	        Point parentPos = parent.absoluteToLocal(absolute);
	        return new Point(parentPos.x-area.x, parentPos.y-area.y);
	        
	    } else {
	        
	        return new Point(absolute.x-area.x, absolute.y-area.y);
	        
	    }
	    
	}
	
	public Rectangle getAreaRelativeTo(ADrawableObject obj) {
	    
	    /**
	     * Returns this drawable's area relative to the supplied drawable's coordinate 
	     * system. The supplied drawable must be a parent of this object.
	     **/
	    
	    if (parent != null) {
	        
	        if (parent == obj) {
	            
	            return area; 
	            
	        } else {
	            
	            Rectangle parentArea = parent.getAreaRelativeTo(obj);
	            return new Rectangle(area.x+parentArea.x, area.y+parentArea.y, area.width, area.height);
	            
	        }
	        
	    }
	    
	    return new Rectangle(0, 0, 0, 0);
	    
	}
	
	public void remove() {
	    
	    if (parent != null) {
	    
	        redraw();
	        parent.removeChild(this);
	        parent = null;
	        
	    }
	    
	}
	
	public void addChild(ADrawableObject child) {
	    
	    children.add(child);
	    child.setParent(this);
//	    child.show();
	    
	}
	
	public void removeChild(ADrawableObject child) {
	    
	    if (children.remove(child)) {
	        
	        child.unsetParent();
//	        child.hide();
	        
	    }
	    
	}
	
	public void removeAllChildren() {
	    
	    while (children.size() > 0) {
	        
	        //children.get(0).setParent(null);
	        removeChild(children.get(0));
	        
	    }
	    
	}
	
	public void redraw() {
		
	    /**
	     * A method allowing drawables to notify the canvas that they
	     * need to be redrawn.
	     * 
	     * NOTE: This passes the area of the drawable to the canvas manager,
	     * which will then be included in the area to be redrawn when the next
	     * redraw takes place. This redraw will NOT occur immediately, and the
	     * actual redrawn area may include many other areas of the canvas
	     * that need to be redrawn at the same time. Also, ALL objects that
	     * lie under the redraw area will have their draw methods invoked,
	     * so it's best to call redraw on the smallest possible drawable that
	     * needs to be redrawn.
	     **/
	    
//	    System.out.println("Redraw: "+this.getClass().getName()+" area: ("+area.x+", "+area.y+", "+area.width+", "+area.height+")");
//		CanvasManager.getSingleton().redraw(getAbsoluteArea());
	    
	    if (parent != null) {
	    
	        parent.redraw(new Rectangle(area.x, area.y, area.width, area.height));
	        
	    } else {

			CanvasManager.getSingleton().scheduleRedraw(area);
	        
	    }
		
	}
	
	public void redraw(Rectangle redrawArea) {

//	    System.out.println("Redraw: ("+redrawArea.x+", "+redrawArea.y+", "+redrawArea.width+", "+redrawArea.height+")");
	    
	    if (redrawArea.x < 0) {
	        
	        redrawArea.width += redrawArea.x;
	        redrawArea.x = 0;
	        
	    }
	    
	    if (redrawArea.y < 0) {
	        
	        redrawArea.height += redrawArea.y;
	        redrawArea.y = 0;
	        
	    }

	    if (redrawArea.x+redrawArea.width > area.width) {
	        
	        redrawArea.width = area.width-redrawArea.x;
	        
	    }
	    
	    if (redrawArea.y+redrawArea.height > area.height) {
	        
	        redrawArea.height = area.height-redrawArea.y;
	        
	    }
	    
	    if (redrawArea.width > 0 && redrawArea.height > 0) {
	        
//	        setAsDirty();
	        
	        redrawArea.x += area.x;
	        redrawArea.y += area.y;
	        
	        if (redrawArea.x<area.x || redrawArea.y<area.y || redrawArea.x+redrawArea.width>area.x+area.width || redrawArea.y+redrawArea.height>area.y+area.height) {
	            
	            System.err.println("CHILD IS DRAWING OUTSIDE OF ITS AREA!!!");
	            
	        }
	
//		    System.out.println("Parent: "+this.getClass().getName()+" area: ("+area.x+", "+area.y+", "+area.width+", "+area.height+") redraw: ("+redrawArea.x+", "+redrawArea.y+", "+redrawArea.width+", "+redrawArea.height+")");
		    
	        if (parent != null) {
	            
	            parent.redraw(redrawArea);
	            
	        } else {
	            
				CanvasManager.getSingleton().scheduleRedraw(redrawArea);	            
	            
	        }
	        
	    }
	    
	}
	
	public void applyRedraw() {
	    
	    /**
	     * A shortcut way of allowing any drawable to force a redraw.
	     * 
	     * NOTE: This is actually incorrect! Drawables should not be able
	     * to force redraws, they should only be able to notify the canvas
	     * that they want to be redrawn (which is done using the redraw
	     * method). The actual redraw should be called from CanvasManager
	     * once all drawables have processed the last event.
	     **/
	    
	    CanvasManager.getSingleton().applyRedraw();
	    
	}
	
	public final void paint(GC gc, Rectangle redrawArea, Image parent) {
		
	    /**
	     * Takes a paint event from the parent, along with the parent's image
	     * and GC, and creates a new image of the intersection between the redraw
	     * area and this object, then invokes this object's draw method passing it
	     * a GC to this image and the redraw area local to this object's coordinate 
	     * system.
	     * 
	     * NOTE: This method should not be overridden - instead the draw method
	     * should be overridden to provide custom drawing code for each subclass.
	     **/
	    
/*		if (visible && redrawArea.intersects(area)) {
		
		    // Has the size of this object changed? If so, adjust the size of the buffer accordingly
		    
		    if (buffer == null || buffer.getBounds().x != area.width || buffer.getBounds().y != area.height) {
		        
		        Image newBuffer = new Image(Display.getDefault(), area.width, area.height);
		        GC newBufferGC = new GC(newBuffer);
		        
		        if (buffer != null) {
		        
		            newBufferGC.drawImage(buffer, 0, 0);
			        buffer.dispose();
			        bufferGC.dispose();
			        		            
		        }
		        
		        buffer = newBuffer;
		        bufferGC = newBufferGC;
		        
		    }
		    
		    // Calculate the portion of this object that needs to be redrawn
		    
		    Rectangle intersection = redrawArea.intersection(area);
		    
		    if (intersection.width > 0 && intersection.height > 0) {

		        // Has the object itself changed? If so, update the changed portion
		        // of the buffer
		        
		        if (dirty) {
		            
		            System.err.println(this.getClass().getName()+" is dirty; cleaning...");
		        		            
		            Image changeBuffer = new Image(Display.getDefault(), intersection.width, intersection.height);
		            GC changeBufferGC = new GC(changeBuffer);		        
		            changeBufferGC.drawImage(parent, intersection.x-redrawArea.x, intersection.y-redrawArea.y, intersection.width, intersection.height, 0, 0, intersection.width, intersection.height);		            
		            Rectangle relativeRedrawArea = new Rectangle(intersection.x - area.x, intersection.y - area.y, intersection.width, intersection.height);
		            drawBefore(changeBufferGC, relativeRedrawArea);
		        
		            for (int i=0; i<children.size(); i++) {
		            
		                children.get(i).paint(changeBufferGC, relativeRedrawArea, changeBuffer);
		            
		            }
		        
		            drawAfter(changeBufferGC, relativeRedrawArea);
		            bufferGC.drawImage(changeBuffer, intersection.x-area.x, intersection.y-area.y);
//		            gc.drawImage(changeBuffer, intersection.x-redrawArea.x, intersection.y-redrawArea.y);
		            changeBuffer.dispose();
		            changeBufferGC.dispose();
		            dirty = false;
		            
		        }

		        try {
		        
		            gc.drawImage(buffer, intersection.x-area.x, intersection.y-area.y, intersection.width, intersection.height, intersection.x-redrawArea.x, intersection.y-redrawArea.y, intersection.width, intersection.height);
		            
		        } catch (IllegalArgumentException e) {
		            
		            System.err.println(buffer.getBounds().width+", "+buffer.getBounds().height);
		            System.err.println((intersection.x-area.x)+", "+(intersection.y-area.y)+", "+intersection.width+", "+intersection.height+", "+(intersection.x-redrawArea.x)+", "+(intersection.y-redrawArea.y)+", "+intersection.width+", "+intersection.height);
		            
		        }
		        
		    }
			
		} */
		
//		The old code in case something goes very wrong...
 
 		if (visible && redrawArea.intersects(area)) {
			
			    Rectangle intersection = redrawArea.intersection(area);
			    
			    if (intersection.width > 0 && intersection.height > 0) {
			    
			        Image buffer = new Image(Display.getDefault(), intersection.width, intersection.height);
			        GC bufferGC = new GC(buffer);
		            bufferGC.drawImage(parent, intersection.x-redrawArea.x, intersection.y-redrawArea.y, intersection.width, intersection.height, 0, 0, intersection.width, intersection.height);		            
			        Rectangle relativeRedrawArea = new Rectangle(intersection.x - area.x, intersection.y - area.y, intersection.width, intersection.height);
			        drawBefore(bufferGC, relativeRedrawArea);
			        
			        for (int i=0; i<children.size(); i++) {
			            
			            children.get(i).paint(bufferGC, relativeRedrawArea, buffer);
			            
			        }
			        
			        drawAfter(bufferGC, relativeRedrawArea);
			        gc.drawImage(buffer, intersection.x-redrawArea.x, intersection.y-redrawArea.y);
			        buffer.dispose();
			        bufferGC.dispose();
			        
			    }
				
			} 
			
	}

	public void parentResized(Point size) {

	    parentSize = size;
	    updateDimensions();
	    applyRedraw();
	    
	}
		
	public IDropTarget getDropTarget(Point position) {
	    
	    if (area.contains(position)) {
	        
	        IDropTarget childTarget = null;
	        int i = children.size()-1;
	        
	        while (childTarget == null && i >= 0) {
	            
	            childTarget = children.get(i).getDropTarget(new Point(position.x-area.x, position.y-area.y));
	        	i--;
	            
	        }
	    
	    	if (childTarget == null && this instanceof IDropTarget) {
	    	    
	    	    return (IDropTarget)this;
	    	    
	    	}
	    	 
	    	return childTarget;
	        
	    } else {
	        
	        return null;
	        
	    }
	    
	}
	
/*	public void drop(ADraggableObject o, int x, int y) {
	    
		if (x<TaskPaneManager.getSingleton().area.x) {
		    
			CanvasManager.getSingleton().onDrop(o,x,y);
			
		} else {
		    
			TaskPaneManager.getSingleton().onDrop(o,x,y);
			
		}
		
	} */
	
	public void addDimensionListener(IDimensionListener l) {
	    
	    dimensionListeners.add(l);
	    l.objectDimensionsChanged(this, getArea());
	    
	}
	
	public void removeDimensionListener(IDimensionListener l) {
	    
	    dimensionListeners.remove(l);
	    
	}
	
	private void onDimensionsChanged() {
	    
	    for (int i=0; i<dimensionListeners.size(); i++) {
	        
	        dimensionListeners.get(i).objectDimensionsChanged(this, getArea());
	        
	    }
	    
	}
	
	public void bringToFront(ADrawableObject child) {
	    
	    if (children.remove(child)) {
	        
	        children.add(child);
	        
	    }
	    
	}
	
	public boolean childVisible(ADrawableObject child) {
	    
	    Rectangle childArea = child.getAreaRelativeTo(this);
	    return (childArea.x >= 0 && childArea.y >= 0 && childArea.x+childArea.width < area.width && childArea.y+childArea.height < area.height);
	    
	}
	
	public void childDimensionsChanged(ADrawableObject child) {
	    
	    /* Default implementation for childDimensionsChanged */
	    
	}
	
	public void drawBefore(GC gc, Rectangle redrawArea) {
	    
	    /* Default implementation for draw */
	    
	}
	
	public void drawAfter(GC gc, Rectangle redrawArea) {
	    
	    /* Default implementation for draw */
	    
	}
	
}
