/*
 * {{{ header & license
 * Copyright (c) 2005 Wisconsin Court System
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * }}}
 */
package org.xhtmlrenderer.layout;

import org.xhtmlrenderer.css.style.CssContext;
import org.xhtmlrenderer.render.Box;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.render.LineBox;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A class that manages all floated boxes in a given block formatting context.
 * It is responsible for positioning floats and calculating clearance for 
 * non-floated (block) boxes.
 */
public class FloatManager {
    private static final int LEFT = 1;
    private static final int RIGHT = 2;

    private List _leftFloats = new ArrayList();
    private List _rightFloats = new ArrayList();

    private Box _master;

    public void floatBox(LayoutContext c, Layer layer, BlockFormattingContext bfc, BlockBox box) {
        if (box.getStyle().isFloatedLeft()) {
            position(c, bfc, box, LEFT);
            save(box, layer, bfc, LEFT);
        } else if (box.getStyle().isFloatedRight()) {
            position(c, bfc, box, RIGHT);
            save(box, layer, bfc, RIGHT);
        }
    }

    public void clear(CssContext cssCtx, BlockFormattingContext bfc, Box box) {
        if (box.getStyle().isClearLeft()) {
            moveClear(cssCtx, bfc, box, getFloats(LEFT));
        }
        if (box.getStyle().isClearRight()) {
            moveClear(cssCtx, bfc, box, getFloats(RIGHT));
        }
    }

    private void save(BlockBox current, Layer layer, BlockFormattingContext bfc, int direction) {
        Point p = bfc.getOffset();
        getFloats(direction).add(new BoxOffset(current, p.x, p.y));
        layer.addFloat(current, bfc);
        current.getFloatedBoxData().setManager(this);
        
        current.calcCanvasLocation();
        current.calcChildLocations();
    }

    private void position(CssContext cssCtx, BlockFormattingContext bfc,
                          BlockBox current, int direction) {
        moveAllTheWayOver(current, direction);

        alignToLastOpposingFloat(cssCtx, bfc, current, direction);
        alignToLastFloat(cssCtx, bfc, current, direction);

        if (!fitsInContainingBlock(current) ||
                overlaps(cssCtx, bfc, current, getFloats(direction))) {
            moveAllTheWayOver(current, direction);
            moveFloatBelow(cssCtx, bfc, current, getFloats(direction));
        }

        if (overlaps(cssCtx, bfc, current, getOpposingFloats(direction))) {
            moveAllTheWayOver(current, direction);
            moveFloatBelow(cssCtx, bfc, current, getFloats(direction));
            moveFloatBelow(cssCtx, bfc, current, getOpposingFloats(direction));
        }

        if (current.getStyle().isCleared()) {
            if (current.getStyle().isClearLeft() && direction == LEFT) {
                moveAllTheWayOver(current, LEFT);
            } else if (current.getStyle().isClearRight() && direction == RIGHT) {
                moveAllTheWayOver(current, RIGHT);
            }
            moveFloatBelow(cssCtx, bfc, current, getFloats(direction));
        }
    }

    private List getFloats(int direction) {
        return direction == LEFT ? _leftFloats : _rightFloats;
    }

    private List getOpposingFloats(int direction) {
        return direction == LEFT ? _rightFloats : _leftFloats;
    }

    private void alignToLastFloat(CssContext cssCtx,
                                  BlockFormattingContext bfc, BlockBox current, int direction) {

        List floats = getFloats(direction);
        if (floats.size() > 0) {
            Point offset = bfc.getOffset();
            BoxOffset lastOffset = (BoxOffset) floats.get(floats.size() - 1);
            BlockBox last = (BlockBox) lastOffset.getBox();

            Rectangle currentBounds = current.getMarginEdge(cssCtx, -offset.x, -offset.y);

            Rectangle lastBounds = last.getMarginEdge(cssCtx, -lastOffset.getX(), -lastOffset.getY());

            boolean moveOver = false;

            if (currentBounds.y < lastBounds.y) {
                currentBounds.translate(0, lastBounds.y - currentBounds.y);
                moveOver = true;
            }

            if (currentBounds.y >= lastBounds.y && currentBounds.y < lastBounds.y + lastBounds.height) {
                moveOver = true;
            }

            if (moveOver) {
                if (direction == LEFT) {
                    currentBounds.x = lastBounds.x + last.getWidth();
                } else if (direction == RIGHT) {
                    currentBounds.x = lastBounds.x - current.getWidth();
                }

                currentBounds.translate(offset.x, offset.y);

                current.setX(currentBounds.x);
                current.setY(currentBounds.y);
            }
        }
    }

    private void alignToLastOpposingFloat(CssContext cssCtx,
                                          BlockFormattingContext bfc, BlockBox current, int direction) {

        List floats = getOpposingFloats(direction);
        if (floats.size() > 0) {
            Point offset = bfc.getOffset();
            BoxOffset lastOffset = (BoxOffset) floats.get(floats.size() - 1);

            Rectangle currentBounds = current.getMarginEdge(cssCtx, -offset.x, -offset.y);

            Rectangle lastBounds = lastOffset.getBox().getMarginEdge(cssCtx,
                    -lastOffset.getX(), -lastOffset.getY());

            if (currentBounds.y < lastBounds.y) {
                currentBounds.translate(0, lastBounds.y - currentBounds.y);

                currentBounds.translate(offset.x, offset.y);

                current.setY(currentBounds.y);
            }
        }
    }

    private void moveAllTheWayOver(BlockBox current, int direction) {
        if (direction == LEFT) {
            current.setX(0);
        } else if (direction == RIGHT) {
            current.setX(current.getContainingBlock().getContentWidth() - current.getWidth());
        }
    }

    private boolean fitsInContainingBlock(BlockBox current) {
        return current.getX() >= 0 &&
                (current.getX() + current.getWidth()) <= current.getContainingBlock().getContentWidth();
    }

    private int findLowestY(CssContext cssCtx, List floats) {
        int result = 0;

        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset floater = (BoxOffset) i.next();

            Rectangle bounds = floater.getBox().getMarginEdge(
                    cssCtx, -floater.getX(), -floater.getY());
            if (bounds.y + bounds.height > result) {
                result = bounds.y + bounds.height;
            }
        }

        return result;
    }
    
    public int getClearDelta(CssContext cssCtx, int bfcRelativeY) {
        int lowestLeftY = findLowestY(cssCtx, getFloats(LEFT));
        int lowestRightY = findLowestY(cssCtx, getFloats(RIGHT));
        
        int lowestY = Math.max(lowestLeftY, lowestRightY);
        
        return lowestY - bfcRelativeY;
    }

    private boolean overlaps(CssContext cssCtx, BlockFormattingContext bfc,
                             BlockBox current, List floats) {
        Point offset = bfc.getOffset();
        Rectangle bounds = current.getMarginEdge(cssCtx, -offset.x, -offset.y);

        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset floater = (BoxOffset) i.next();
            Rectangle floaterBounds = floater.getBox().getMarginEdge(cssCtx,
                    -floater.getX(), -floater.getY());

            if (floaterBounds.intersects(bounds)) {
                return true;
            }
        }

        return false;
    }
    
    private void moveFloatBelow(CssContext cssCtx, BlockFormattingContext bfc,
                                   Box current, List floats) {
        if (floats.size() == 0) {
            return;
        }
        
        Point offset = bfc.getOffset();
        int boxY = current.getY() - offset.y;
        int floatY = findLowestY(cssCtx, floats);
        
        if (floatY - boxY > 0) {
            current.setY(current.getY() + (floatY - boxY));
        }
    }

    private void moveClear(CssContext cssCtx, BlockFormattingContext bfc,
                           Box current, List floats) {
        if (floats.size() == 0) {
            return;
        }
        
        // Translate from box coords to BFC coords
        Point offset = bfc.getOffset();
        Rectangle bounds = current.getBorderEdge(
                current.getX()-offset.x, current.getY()-offset.y, cssCtx);
        
        int y = findLowestY(cssCtx, floats);
        
        if (bounds.y < y) {
            // Translate bottom margin edge of lowest float back to box coords
            // and set the box's border edge to that value
            bounds.y = y;

            bounds.translate(offset.x, offset.y);

            current.setY(bounds.y - (int)current.getMargin(cssCtx).top());
        }
    }

    public void removeFloat(BlockBox floater) {
        removeFloat(floater, getFloats(LEFT));
        removeFloat(floater, getFloats(RIGHT));
    }
    
    private void removeFloat(BlockBox floater, List floats) {
        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset boxOffset = (BoxOffset) i.next();
            if (boxOffset.getBox().equals(floater)) {
                i.remove();
                floater.getFloatedBoxData().setManager(null);
            }
        }
    }
    
    public void calcFloatLocations() {
        calcFloatLocations(getFloats(LEFT));
        calcFloatLocations(getFloats(RIGHT));
    }
    
    private void calcFloatLocations(List floats) {
        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset boxOffset = (BoxOffset) i.next();
            boxOffset.getBox().calcCanvasLocation();
            boxOffset.getBox().calcChildLocations();
        } 
    }

    private void applyLineHeightHack(CssContext cssCtx, Box line, Rectangle bounds) {
        // this is a hack to deal with lines w/o width or height. is this valid?
        // possibly, since the line doesn't know how long it should be until it's already
        // done float adjustments
        if (line.getHeight() == 0) {
            bounds.height = (int)line.getStyle().getLineHeight(cssCtx);
        }
    }

    public int getLeftFloatDistance(CssContext cssCtx, BlockFormattingContext bfc, 
            LineBox line, int containingBlockContentWidth) {
        return getFloatDistance(cssCtx, bfc, line, containingBlockContentWidth, _leftFloats, LEFT);
    }

    public int getRightFloatDistance(CssContext cssCtx, BlockFormattingContext bfc, 
            LineBox line, int containingBlockContentWidth) {
        return getFloatDistance(cssCtx, bfc, line, containingBlockContentWidth, _rightFloats, RIGHT);
    }

    private int getFloatDistance(CssContext cssCtx, BlockFormattingContext bfc,
                                 LineBox line, int containingBlockContentWidth,
                                 List floatsList, int direction) {
        if (floatsList.size() == 0) {
            return 0;
        }

        Point offset = bfc.getOffset();
        Rectangle lineBounds = line.getMarginEdge(cssCtx, -offset.x, -offset.y);
        lineBounds.width = containingBlockContentWidth;
        
        int farthestOver = direction == LEFT ? lineBounds.x : lineBounds.x + lineBounds.width;

        applyLineHeightHack(cssCtx, line, lineBounds);

        for (int i = 0; i < floatsList.size(); i++) {
            BoxOffset floater = (BoxOffset) floatsList.get(i);
            Rectangle fr = floater.getBox().getMarginEdge(cssCtx, -floater.getX(), -floater.getY());
            if (lineBounds.intersects(fr)) {
                if (direction == LEFT && fr.x + fr.width > farthestOver) {
                    farthestOver = fr.x + fr.width;
                } else if (direction == RIGHT && fr.x < farthestOver) {
                    farthestOver = fr.x;
                }
            }
        }
        
        if (direction == LEFT) {
            return farthestOver - lineBounds.x;
        } else {
            return lineBounds.x + lineBounds.width - farthestOver;
        }
    }

    public void setMaster(Box owner) {
        _master = owner;
    }

    public Box getMaster() {
        return _master;
    }
    
    public Point getOffset(BlockBox floater) {
        // FIXME inefficient (but probably doesn't matter)
        return getOffset(floater,  
                floater.getStyle().isFloatedLeft() ? getFloats(LEFT) : getFloats(RIGHT));
    }
    
    private Point getOffset(BlockBox floater, List floats) {
        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset boxOffset = (BoxOffset) i.next();
            BlockBox box = boxOffset.getBox();

            if (box.equals(floater)) {
                return new Point(boxOffset.getX(), boxOffset.getY());
            }
        }
        
        return null;
    }
    
    private void performFloatOperation(FloatOperation op, List floats) {
        for (Iterator i = floats.iterator(); i.hasNext();) {
            BoxOffset boxOffset = (BoxOffset) i.next();
            BlockBox box = boxOffset.getBox();

            box.setAbsX(box.getX() + getMaster().getAbsX() - boxOffset.getX());
            box.setAbsY(box.getY() + getMaster().getAbsY() - boxOffset.getY());
            
            op.operate(box);
        }
    }

    public void performFloatOperation(FloatOperation op) {
        performFloatOperation(op, getFloats(LEFT));
        performFloatOperation(op, getFloats(RIGHT));
    }    

    private static class BoxOffset {
        private BlockBox _box;
        private int _x;
        private int _y;

        public BoxOffset(BlockBox box, int x, int y) {
            _box = box;
            _x = x;
            _y = y;
        }

        public BlockBox getBox() {
            return _box;
        }

        public int getX() {
            return _x;
        }

        public int getY() {
            return _y;
        }
    }
    
    public interface FloatOperation {
        public void operate(Box floater);
    }
}

/*
 * $Id$
 */