/*
 GNU LESSER GENERAL PUBLIC LICENSE
 Copyright (C) 2006 The Lobo Project

 This library 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 library 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 library; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

 Contact info: lobochief@users.sourceforge.net
 */
/*
 * Created on Apr 16, 2005
 */
package org.lobobrowser.html.renderer;

import java.util.*;
import java.util.logging.*;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import java.awt.image.ImageObserver;

import org.lobobrowser.html.*;
import org.lobobrowser.html.domimpl.*;
import org.lobobrowser.html.style.*;
import org.lobobrowser.util.*;
import org.w3c.dom.Node;

/**
 * Represents a HTML block in a rendered document, typically
 * a DIV. The root renderer node is of this type as well.
 * <p>
 * Immediately below an <code>RBlock</code> you will find a node of
 * type {@link RBlockViewport}.
 */
public class RBlock extends BaseElementRenderable implements
		RenderableContainer, ImageObserver {
	static final Logger logger = Logger.getLogger(RBlock.class.getName());

	private static final boolean loggableInfo = logger.isLoggable(Level.INFO);

	private static final FloatingBounds INVALID_FLOAT_BOUNDS = new FloatingViewportBounds(
			null, false, 0, 0, 0);

	protected final FrameContext frameContext;

	protected final int listNesting;

	protected final HtmlRendererContext rendererContext;

	protected final int defaultOverflow;

	protected final RBlockViewport bodyLayout;

	protected RenderableSpot startSelection;

	protected RenderableSpot endSelection;

	protected JScrollBar vScrollBar;

	protected JScrollBar hScrollBar;

	protected boolean hasHScrollBar = false;

	protected boolean hasVScrollBar = false;

	// Validation-dependent variables...
	// private Dimension layoutSize = null;
	private int lastTentativeHeight = -1;

	private int lastTentativeWidth = -1;

	private int lastWhiteSpace = -1;

	private FloatingBounds lastFloatBounds = INVALID_FLOAT_BOUNDS;

	private Font lastFont = null;

	public RBlock(NodeImpl modelNode, int listNesting,
			UserAgentContext pcontext, HtmlRendererContext rcontext,
			FrameContext frameContext, RenderableContainer parentContainer) {
		this(modelNode, listNesting, pcontext, rcontext, frameContext,
				parentContainer, RBlock.OVERFLOW_NONE);
	}

	public RBlock(NodeImpl modelNode, int listNesting,
			UserAgentContext pcontext, HtmlRendererContext rcontext,
			FrameContext frameContext, RenderableContainer parentContainer,
			int defaultOverflow) {
		super(parentContainer, modelNode, pcontext);
		this.listNesting = listNesting;
		this.frameContext = frameContext;
		this.rendererContext = rcontext;
		this.defaultOverflow = defaultOverflow;
		RBlockViewport bl = new RBlockViewport(modelNode, this, this
				.getViewportListNesting(listNesting), pcontext, rcontext,
				frameContext, this);
		this.bodyLayout = bl;
		bl.setOriginalParent(this);
		// Initialize origin of RBlockViewport to be as far top-left as
		// possible.
		// This will be corrected on first layout.
		bl.setX(Short.MAX_VALUE);
		bl.setY(Short.MAX_VALUE);
	}

	/**
	 * Gets the width the vertical scrollbar has when shown.
	 */
	public int getVScrollBarWidth() {
		return SCROLL_BAR_THICKNESS;
	}

	public void finalize() throws Throwable {
		super.finalize();
	}

	public int getVAlign() {
		// Not used
		return VALIGN_BASELINE;
	}

	public void ensureVisible(Point point) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			boolean hscroll = this.hasHScrollBar;
			boolean vscroll = this.hasVScrollBar;
			int origX = bodyLayout.x;
			int origY = bodyLayout.y;
			Insets insets = this.getInsets(hscroll, vscroll);
			if (hscroll) {
				if (point.x < insets.left) {
					bodyLayout.x += (insets.left - point.x);
				} else if (point.x > this.width - insets.right) {
					bodyLayout.x -= (point.x - this.width + insets.right);
				}
			}
			if (vscroll) {
				if (point.y < insets.top) {
					bodyLayout.y += (insets.top - point.y);
				} else if (point.y > this.height - insets.bottom) {
					bodyLayout.y -= (point.y - this.height + insets.bottom);
				}
			}
			if (hscroll || vscroll) {
				this.correctViewportOrigin(insets, this.width, this.height);
				if (origX != bodyLayout.x || origY != bodyLayout.y) {
					this.resetScrollBars(null);
					// TODO: This could be paintImmediately.
					this.repaint();
				}
			}
		}
	}

	private JScrollBar getHScrollBar() {
		JScrollBar sb = this.hScrollBar;
		if (sb == null) {
			// Should never go back to null
			sb = new JScrollBar(JScrollBar.HORIZONTAL);
			sb.addAdjustmentListener(new LocalAdjustmentListener(
					JScrollBar.HORIZONTAL));
			this.hScrollBar = sb;
		}
		return sb;
	}

	private JScrollBar getVScrollBar() {
		JScrollBar sb = this.vScrollBar;
		if (sb == null) {
			// Should never go back to null
			sb = new JScrollBar(JScrollBar.VERTICAL);
			sb.addAdjustmentListener(new LocalAdjustmentListener(
					JScrollBar.VERTICAL));
			this.vScrollBar = sb;
		}
		return sb;
	}

	public final boolean couldBeScrollable() {
		int overflow = this.getOverflow();
		return overflow != OVERFLOW_NONE
				&& (overflow == OVERFLOW_SCROLL
						|| overflow == OVERFLOW_VERTICAL || overflow == OVERFLOW_AUTO);
	}

	public final boolean isOverflowVisible() {
		int overflow = this.getOverflow();
		return overflow == OVERFLOW_NONE || overflow == OVERFLOW_VISIBLE;
	}

	private Insets defaultPaddingInsets = null;

	public void setDefaultPaddingInsets(Insets insets) {
		this.defaultPaddingInsets = insets;
	}

	public void setDefaultMarginInsets(Insets insets) {
		this.defaultMarginInsets = insets;
	}

	public int getFirstLineHeight() {
		return this.bodyLayout.getFirstLineHeight();
	}

	public int getFirstBaselineOffset() {
		return this.bodyLayout.getFirstBaselineOffset();
	}

	public void setSelectionEnd(RenderableSpot rpoint) {
		this.endSelection = rpoint;
	}

	public void setSelectionStart(RenderableSpot rpoint) {
		this.startSelection = rpoint;
	}

	protected final Insets getPaddingInsets(RenderState rs) {
		Insets mi = rs.getPaddingInsets();
		if (mi == null) {
			return this.defaultPaddingInsets;
		}
		return mi;
	}

	public int getViewportListNesting(int blockNesting) {
		return blockNesting;
	}

	public void paint(Graphics g) {
		RenderState rs = this.modelNode.getRenderState();
		if (rs != null && rs.getVisibility() != RenderState.VISIBILITY_VISIBLE) {
			// Just don't paint it.
			return;
		}
		boolean linfo = loggableInfo;
		long time1 = linfo ? System.currentTimeMillis() : 0;
		this.prePaint(g);
		long time2 = linfo ? System.currentTimeMillis() : 0;
		long time3 = 0;
		try {
			Insets insets = this.getInsets(this.hasHScrollBar,
					this.hasVScrollBar);
			RBlockViewport bodyLayout = this.bodyLayout;
			if (bodyLayout != null) {
				int overflow = this.getOverflow();
				if (overflow == OVERFLOW_NONE || overflow == OVERFLOW_VISIBLE) {
					// Simply translate.
					int bx = bodyLayout.x;
					int by = bodyLayout.y;
					g.translate(bx, by);
					try {
						bodyLayout.paint(g);
					} finally {
						g.translate(-bx, -by);
					}
				} else {
					// Clip when there potential scrolling or hidden overflow
					// was requested.
					Graphics newG = g.create(insets.left, insets.top,
							this.width - insets.left - insets.right,
							this.height - insets.top - insets.bottom);
					try {
						// Second, translate
						newG.translate(bodyLayout.x - insets.left, bodyLayout.y
								- insets.top);
						// Third, paint in clipped + translated region.
						bodyLayout.paint(newG);
					} finally {
						newG.dispose();
					}
				}

				if (linfo) {
					time3 = System.currentTimeMillis();
				}
			} else {
				// nop
			}

			// Paint FrameContext selection.
			// This is only done by root RBlock.

			RenderableSpot start = this.startSelection;
			RenderableSpot end = this.endSelection;
			boolean inSelection = false;
			if (start != null && end != null && !start.equals(end)) {
				this.paintSelection(g, inSelection, start, end);
			}
			// Must paint scrollbars too.
			JScrollBar hsb = this.hScrollBar;
			if (hsb != null) {
				Graphics sbg = g.create(insets.left, this.height
						- insets.bottom, this.width - insets.left
						- insets.right, SCROLL_BAR_THICKNESS);
				try {
					hsb.paint(sbg);
				} finally {
					sbg.dispose();
				}
			}
			JScrollBar vsb = this.vScrollBar;
			if (vsb != null) {
				Graphics sbg = g.create(this.width - insets.right, insets.top,
						SCROLL_BAR_THICKNESS, this.height - insets.top
								- insets.bottom);
				try {
					vsb.paint(sbg);
				} finally {
					sbg.dispose();
				}
			}

		} finally {
			// Must always call super implementation
			super.paint(g);
		}
		if (linfo) {
			long time4 = System.currentTimeMillis();
			if (time4 - time1 > 100) {
				logger.info("paint(): Elapsed: " + (time4 - time1)
						+ " ms. Prepaint: " + (time2 - time1)
						+ " ms. Viewport: " + (time3 - time2) + " ms. RBlock: "
						+ this + ".");
			}
		}
	}

	public final void layout(int availWidth, int availHeight,
			int defaultOverflow) {
		this.layout(availWidth, availHeight, null, 0, defaultOverflow);
	}

	public final void layout(int availWidth, int availHeight,
			FloatingBounds floatBounds, int tentativeY) {
		this.layout(availWidth, availHeight, floatBounds, tentativeY,
				this.defaultOverflow);
	}

	public final void layout(int availWidth, int availHeight,
			FloatingBounds floatBounds, int tentativeY, int defaultOverflow) {
		try {
			this.doLayout(availWidth, availHeight, floatBounds, tentativeY,
					defaultOverflow);
		} finally {
			this.layoutUpTreeCanBeInvalidated = true;
			this.layoutDeepCanBeInvalidated = true;
			// this.renderStyleCanBeInvalidated = true;
		}
	}

	public final void doLayout(int availWidth, int availHeight) {
		this.doLayout(availWidth, availHeight, null, 0, this.defaultOverflow);
	}

	/**
	 * Lays out and sets dimensions only if RBlock is invalid (or never before
	 * layed out), if the parameters passed differ from the last layout, or if
	 * the current font differs from the font for the last layout.
	 * 
	 * @param availWidth
	 * @param availHeight
	 */
	public void doLayout(int availWidth, int availHeight,
			FloatingBounds floatBounds, int tentativeY, int defaultOverflow) {
		// Expected to be invoked in the GUI thread.
		int prevTentativeWidth = this.lastTentativeWidth;
		int prevTentativeHeight = this.lastTentativeHeight;
		RenderState renderState = this.modelNode.getRenderState();
		Integer dw = this.getDeclaredWidth(renderState, availWidth);
		Integer dh = this.getDeclaredHeight(renderState, availHeight);
		int adjDeclaredWidth = -1;
		int adjDeclaredHeight = -1;
		int tentativeWidth = availWidth;
		int tentativeHeight = availHeight;
		if (dw != null || dh != null) {
			Insets marginInsets = this.getMarginInsets(renderState);
			if (dw != null) {
				if (marginInsets != null) {
					adjDeclaredWidth = tentativeWidth = dw.intValue()
							+ marginInsets.left + marginInsets.right;
				} else {
					adjDeclaredWidth = tentativeWidth = dw.intValue();
				}
			}
			if (dh != null) {
				if (marginInsets != null) {
					adjDeclaredHeight = tentativeHeight = dh.intValue()
							+ marginInsets.top + marginInsets.bottom;
				} else {
					adjDeclaredHeight = tentativeHeight = dh.intValue();
				}
			}
		}
		boolean forced = tentativeHeight != prevTentativeHeight
				|| tentativeWidth != prevTentativeWidth;
		if (!forced) {
			if (renderState != null) {
				Font font = renderState.getFont();
				if (!font.equals(this.lastFont)) {
					forced = true;
				} else {
					int newWhiteSpace = renderState.getWhiteSpace();
					if (newWhiteSpace != this.lastWhiteSpace) {
						forced = true;
					}
				}
			}
			if (!forced) {
				if (this.lastFloatBounds == INVALID_FLOAT_BOUNDS) {
					forced = true;
				} else {
					forced = !Objects.equals(this.lastFloatBounds, floatBounds);
				}
			}
		}
		if (forced) {
			this.forceLayout(renderState, tentativeWidth, tentativeHeight,
					adjDeclaredWidth, adjDeclaredHeight, floatBounds,
					tentativeY, defaultOverflow);
		} else {
			// nothing to do
		}

		// Even if we didn't do layout, the parent is
		// expected to have removed its GUI components.
		this.sendGUIComponentsToParent();

		// Even if we didn't do layout, the parent is
		// expected to have removed its delayed pairs.
		this.sendDelayedPairsToParent();
	}

	private final boolean correctViewportOrigin(Insets insets, int blockWidth,
			int blockHeight) {
		RBlockViewport bodyLayout = this.bodyLayout;
		int viewPortX = bodyLayout.x;
		int viewPortY = bodyLayout.y;
		boolean corrected = false;
		if (viewPortX > insets.left) {
			bodyLayout.x = insets.left;
			corrected = true;
		} else if (viewPortX < blockWidth - insets.right - bodyLayout.width) {
			bodyLayout.x = Math.min(insets.left, blockWidth - insets.right
					- bodyLayout.width);
			corrected = true;
		}
		if (viewPortY > insets.top) {
			bodyLayout.y = insets.top;
			corrected = true;
		} else if (viewPortY < blockHeight - insets.bottom - bodyLayout.height) {
			bodyLayout.y = Math.min(insets.top, blockHeight - insets.bottom
					- bodyLayout.height);
			corrected = true;
		}
		return corrected;
	}

	/**
	 * Lays out the block without checking for prior dimensions.
	 * 
	 * @param availWidth
	 * @param availHeight
	 * @return
	 */
	private final void forceLayout(RenderState renderState, int tentativeWidth,
			int tentativeHeight, int adjDeclaredWidth, int adjDeclaredHeight,
			FloatingBounds floatBounds, int tentativeY, int defaultOverflow) {
		// Expected to be invoked in the GUI thread.
		// TODO: Not necessary to do full layout if only expandWidth or
		// expandHeight change (specifically in tables).
		RenderState rs = renderState;
		if (rs == null) {
			rs = new BlockRenderState(null);
		}
		// Force invalidate of adjust().
		this.lastAdjustTentativeWidth = -1;

		// See if RenderState must be invalidated.
		if (this.lastTentativeWidth == -1) {
			// invalid or first time
			rs.invalidate();
			this.applyStyle();
		}
		Font newFont = rs.getFont();
		this.lastFont = newFont;
		this.lastTentativeHeight = tentativeHeight;
		this.lastTentativeWidth = tentativeWidth;
		this.lastFloatBounds = floatBounds;
		this.lastWhiteSpace = rs.getWhiteSpace();

		RBlockViewport bodyLayout = this.bodyLayout;
		NodeImpl node = (NodeImpl) this.modelNode;
		if (node == null || bodyLayout == null) {
			Insets insets = this.getInsets(false, false);
			this.width = insets.left + insets.right;
			this.height = insets.bottom + insets.top;
			this.hasHScrollBar = false;
			this.hasVScrollBar = false;
			return;
		}

		// Remove all GUI components previously added by descendents
		// The RBlockViewport.layout() method is expected to add all of them
		// back.
		this.clearGUIComponents();

		int overflow = this.getOverflow();
		if (overflow == OVERFLOW_NONE) {
			overflow = defaultOverflow;
		}
		boolean auto = overflow == OVERFLOW_AUTO;
		boolean bothScrollBars = overflow == OVERFLOW_SCROLL;
		boolean hscroll = bothScrollBars;
		boolean vertical = overflow == OVERFLOW_VERTICAL;
		boolean hauto = vertical || auto;
		boolean vscroll = bothScrollBars || vertical;
		Insets paddingInsets = this.getPaddingInsets(rs);
		if (paddingInsets == null) {
			paddingInsets = RBlockViewport.ZERO_INSETS;
		}
		Insets insets = null;
		for (int tries = (auto ? 0 : 1); tries < 2; tries++) {
			try {
				insets = this.getInsets(hscroll, vscroll);
				int maxY = tries == 0 ? (adjDeclaredHeight == -1 ? -1
						: adjDeclaredHeight - insets.bottom - insets.top
								- paddingInsets.bottom) : -1;
				int desiredViewportWidth = tentativeWidth - insets.left
						- insets.right;
				int desiredViewportHeight = tentativeHeight - insets.top
						- insets.bottom;
				bodyLayout.layout(desiredViewportWidth, desiredViewportHeight,
						paddingInsets, maxY, floatBounds);
				break;
			} catch (SizeExceededException hee) {
				if (tries != 0) {
					throw new IllegalStateException("tries=" + tries + ",auto="
							+ auto);
				}
				vscroll = true;
			}
		}
		this.hasVScrollBar = vscroll;
		Dimension size = bodyLayout.getSize();
		Dimension rblockSize = new Dimension(size.width + insets.left
				+ insets.right, size.height + insets.top + insets.bottom);
		if (hauto
				&& !hscroll
				&& ((adjDeclaredWidth != -1 && rblockSize.width > adjDeclaredWidth) || (rblockSize.width > tentativeWidth))) {
			hscroll = true;
			insets = this.getInsets(hscroll, vscroll);
			rblockSize = new Dimension(size.width + insets.left + insets.right,
					size.height + insets.top + insets.bottom);
		}
		this.hasHScrollBar = hscroll;
		boolean visible = !auto && !bothScrollBars
				&& (overflow != OVERFLOW_HIDDEN);
		int resultingWidth;
		int resultingHeight;
		if (adjDeclaredWidth == -1) {
			resultingWidth = rblockSize.width;
			if (hscroll && resultingWidth > tentativeWidth) {
				resultingWidth = Math.max(tentativeWidth, SCROLL_BAR_THICKNESS);
			}
		} else {
			resultingWidth = visible ? Math.max(rblockSize.width,
					adjDeclaredWidth) : adjDeclaredWidth;
		}
		if (adjDeclaredHeight == -1) {
			resultingHeight = rblockSize.height;
			if (vscroll && resultingHeight > tentativeHeight) {
				resultingHeight = Math.max(tentativeHeight,
						SCROLL_BAR_THICKNESS);
			}
		} else {
			resultingHeight = visible ? Math.max(rblockSize.height,
					adjDeclaredHeight) : adjDeclaredHeight;
		}
		if (vscroll) {
			JScrollBar sb = this.getVScrollBar();
			this.addComponent(sb);
			// Bounds set by updateWidgetBounds
		}
		if (hscroll) {
			JScrollBar sb = this.getHScrollBar();
			this.addComponent(sb);
			// Bounds set by updateWidgetBounds
		}

		this.width = resultingWidth;
		this.height = resultingHeight;
		
		// Correction of scrollbars and viewport position not needed at 
		// this point. In fact, the viewport height could be very much mistaken
		// right now.
	}

	private Boolean lastAdjustExpandHeight;

	private Boolean lastAdjustExpandWidth;

	private int lastAdjustTentativeWidth;

	private int lastAdjustTentativeHeight;

	private FloatingBounds lastAdjustFloatBounds;

	/**
	 * Adjustment step which must be done after layout. This will expand blocks
	 * that need to be expanded and relayout blocks with relative sizes
	 * accordingly.
	 * 
	 * @param availWidth
	 * @param availHeight
	 * @param expandWidth
	 * @param expandHeight
	 */
	public void adjust(int availWidth, int availHeight, boolean expandWidth,
			boolean expandHeight, FloatingBoundsSource floatBoundsSource) {
		// Expected to be invoked in the GUI thread.
		boolean forced = false;
		RenderState renderState = this.modelNode.getRenderState();
		Integer dw = this.getDeclaredWidth(renderState, availWidth);
		Integer dh = this.getDeclaredHeight(renderState, availHeight);
		int adjDeclaredWidth = -1;
		int adjDeclaredHeight = -1;
		int tentativeWidth = availWidth;
		int tentativeHeight = availHeight;
		if (dw != null || dh != null) {
			Insets marginInsets = this.getMarginInsets(renderState);
			if (dw != null) {
				if (marginInsets != null) {
					adjDeclaredWidth = tentativeWidth = dw.intValue()
							+ marginInsets.left + marginInsets.right;
				} else {
					adjDeclaredWidth = tentativeWidth = dw.intValue();
				}
			}
			if (dh != null) {
				if (marginInsets != null) {
					adjDeclaredHeight = tentativeHeight = dh.intValue()
							+ marginInsets.top + marginInsets.bottom;
				} else {
					adjDeclaredHeight = tentativeHeight = dh.intValue();
				}
			}
		}
		FloatingBounds blockFloatBounds = null;
		if (floatBoundsSource != null) {
			blockFloatBounds = floatBoundsSource
					.getChildBlockFloatingBounds(tentativeWidth);
		}
		if (tentativeWidth != this.lastAdjustTentativeWidth
				|| tentativeHeight != this.lastAdjustTentativeHeight) {
			forced = true;
		} else {
			Boolean lastAdjustExpandWidth = this.lastAdjustExpandWidth;
			if (lastAdjustExpandWidth != null
					&& expandWidth != lastAdjustExpandWidth.booleanValue()) {
				forced = true;
			} else {
				Boolean lastAdjustExpandHeight = this.lastAdjustExpandHeight;
				if (lastAdjustExpandHeight != null
						&& expandHeight != lastAdjustExpandHeight
								.booleanValue()) {
					forced = true;
				} else {
					FloatingBounds lastBounds = this.lastAdjustFloatBounds;
					if (lastBounds != INVALID_FLOAT_BOUNDS
							&& !Objects.equals(lastBounds, blockFloatBounds)) {
						forced = true;
					}
				}
			}
		}
		if (forced) {
			this.forceAdjust(renderState, tentativeWidth, tentativeHeight,
					adjDeclaredWidth, adjDeclaredHeight, expandWidth,
					expandHeight, blockFloatBounds, this.defaultOverflow);
		}

		// We send GUI components up in adjust() in case new ones were added.
		this.sendGUIComponentsToParent();

		// No sending delayed pairs here.
	}

	/**
	 * This adjustment step needs to be performed after layout. In this case,
	 * the dimensions previously obtained in the layout are assumed to be the
	 * desired dimensions of the block.
	 */
	public void adjust() {
		// Expected to be invoked in the GUI thread.
		RenderState renderState = this.modelNode.getRenderState();
		int adjDeclaredWidth = this.width;
		int adjDeclaredHeight = this.height;
		int tentativeWidth = adjDeclaredWidth;
		int tentativeHeight = adjDeclaredHeight;
		boolean forced = false;
		if (tentativeWidth != this.lastAdjustTentativeWidth
				|| tentativeHeight != this.lastAdjustTentativeHeight) {
			forced = true;
		}

		if (forced) {
			this.forceAdjust(renderState, tentativeWidth, tentativeHeight,
					adjDeclaredWidth, adjDeclaredHeight, false, false, null,
					this.defaultOverflow);
		}

		// Even if we didn't do layout, the parent is
		// expected to have removed its GUI components.
		this.sendGUIComponentsToParent();

		// No sending delayed pairs here.
	}

	/**
	 * 
	 * @param renderState
	 * @param tentativeWidth
	 *            The tentative or max width that will be tried.
	 * @param tentativeHeight
	 *            The tentative or max height that will be tried.
	 * @param adjDeclaredWidth
	 *            The declared width plus margins.
	 * @param adjDeclaredHeight
	 *            The declared height plus margins.
	 * @param floatBounds
	 *            Float bounds that need to be passed to the viewport.
	 * @param defaultOverflow
	 */
	private final void forceAdjust(RenderState renderState, int tentativeWidth,
			int tentativeHeight, int adjDeclaredWidth, int adjDeclaredHeight,
			boolean expandWidth, boolean expandHeight,
			FloatingBounds blockFloatBounds, int defaultOverflow) {
		// Expected to be invoked in the GUI thread.
		this.lastAdjustTentativeWidth = tentativeWidth;
		this.lastAdjustTentativeHeight = tentativeHeight;
		this.lastAdjustExpandWidth = Boolean.valueOf(expandWidth);
		this.lastAdjustExpandHeight = Boolean.valueOf(expandHeight);
		this.lastAdjustFloatBounds = blockFloatBounds;

		RenderState rs = renderState;
		if (rs == null) {
			rs = new BlockRenderState(null);
		}
		RBlockViewport bodyLayout = this.bodyLayout;
		NodeImpl node = (NodeImpl) this.modelNode;
		if (node == null || bodyLayout == null) {
			Insets insets = this.getInsets(false, false);
			this.width = insets.left + insets.right;
			this.height = insets.bottom + insets.top;
			this.hasHScrollBar = false;
			this.hasVScrollBar = false;
			return;
		}

		// No clearing of GUI components here

		int overflow = this.getOverflow();
		if (overflow == OVERFLOW_NONE) {
			overflow = defaultOverflow;
		}
		boolean auto = overflow == OVERFLOW_AUTO;
		boolean bothScrollBars = overflow == OVERFLOW_SCROLL;
		boolean hscroll = bothScrollBars;
		boolean vertical = overflow == OVERFLOW_VERTICAL;
		boolean hauto = auto || vertical;
		boolean vscroll = bothScrollBars || vertical;
		Insets paddingInsets = this.getPaddingInsets(rs);
		if (paddingInsets == null) {
			paddingInsets = RBlockViewport.ZERO_INSETS;
		}
		Insets insets = null;
		for (int tries = (auto ? 0 : 1); tries < 2; tries++) {
			try {
				insets = this.getInsets(hscroll, vscroll);
				int desiredViewportWidth = tentativeWidth - insets.left
						- insets.right;
				int desiredViewportHeight = tentativeHeight - insets.top
						- insets.bottom;
				FloatingBounds viewportFloatBounds = null;
				if (blockFloatBounds != null) {
					viewportFloatBounds = new ShiftedFloatingBounds(
							blockFloatBounds, -insets.left, -insets.right,
							-insets.top);
				}
				bodyLayout.adjust(desiredViewportWidth, desiredViewportHeight,
						paddingInsets, viewportFloatBounds);
				break;
			} catch (SizeExceededException hee) {
				if (tries != 0) {
					throw new IllegalStateException("tries=" + tries + ",auto="
							+ auto);
				}
				vscroll = true;
			}
		}
		// Dimension size = bodyLayout.getSize();
		// Dimension rblockSize = new Dimension(size.width + insets.left +
		// insets.right, size.height + insets.top + insets.bottom);
		int rblockWidth = bodyLayout.width + insets.left + insets.right;
		if (hauto
				&& !hscroll
				&& ((adjDeclaredWidth != -1 && rblockWidth > adjDeclaredWidth) || (rblockWidth > tentativeWidth))) {
			hscroll = true;
			insets = this.getInsets(hscroll, vscroll);
			rblockWidth = bodyLayout.width + insets.left + insets.right;
		}
		this.hasHScrollBar = hscroll;

		// Calculate resulting width.
		boolean visible = !auto && !hauto && !bothScrollBars
				&& (overflow != OVERFLOW_HIDDEN);
		int resultingWidth;
		if (adjDeclaredWidth == -1) {
			resultingWidth = rblockWidth;
			if (hscroll && resultingWidth > tentativeWidth) {
				resultingWidth = Math.max(tentativeWidth, SCROLL_BAR_THICKNESS);
			} else if (expandWidth && resultingWidth < tentativeWidth) {
				resultingWidth = tentativeWidth;
			}
		} else {
			resultingWidth = visible ? Math.max(rblockWidth, adjDeclaredWidth)
					: adjDeclaredWidth;
		}
		// Align horizontally now. This may change canvas height.
		int alignmentXPercent = rs.getAlignXPercent();
		if (alignmentXPercent > 0) {
			// TODO: OPTIMIZATION: alignment should not be done in table cell
			// sizing determination.
			int canvasWidth = Math.max(bodyLayout.width, resultingWidth
					- insets.left - insets.right);
			// Alignment is done afterwards because canvas dimensions might have
			// changed.
			bodyLayout.alignX(alignmentXPercent, canvasWidth, paddingInsets);
		}

		int resultingHeight;
		int rblockHeight = bodyLayout.height + insets.top + insets.bottom;
		if (auto
				&& !vscroll
				&& ((adjDeclaredHeight != -1 && rblockHeight > adjDeclaredHeight) || (rblockHeight > tentativeHeight))) {
			vscroll = true;
			insets = this.getInsets(hscroll, vscroll);
			rblockHeight = bodyLayout.height + insets.top + insets.bottom;
		}
		this.hasVScrollBar = vscroll;

		if (adjDeclaredHeight == -1) {
			resultingHeight = rblockHeight;
			if (vscroll && resultingHeight > tentativeHeight) {
				resultingHeight = Math.max(tentativeHeight,
						SCROLL_BAR_THICKNESS);
			} else if (expandHeight && resultingHeight < tentativeHeight) {
				resultingHeight = tentativeHeight;
			}
		} else {
			resultingHeight = visible ? Math.max(rblockHeight,
					adjDeclaredHeight) : adjDeclaredHeight;
		}

		// Align vertically now
		int alignmentYPercent = rs.getAlignYPercent();
		if (alignmentYPercent > 0) {
			// TODO: OPTIMIZATION: alignment should not be done in table cell
			// sizing determination.
			int canvasHeight = Math.max(bodyLayout.height, resultingHeight
					- insets.top - insets.bottom);
			// Alignment is done afterwards because canvas dimensions might have
			// changed.
			bodyLayout.alignY(alignmentYPercent, canvasHeight, paddingInsets);
		}

		if (vscroll) {
			JScrollBar sb = this.getVScrollBar();
			this.addComponent(sb);
			// Bounds set by updateWidgetBounds
		}
		if (hscroll) {
			JScrollBar sb = this.getHScrollBar();
			this.addComponent(sb);
			// Bounds set by updateWidgetBounds
		}

		this.width = resultingWidth;
		this.height = resultingHeight;

		if (hscroll || vscroll) {
			// In this case, viewport origin should not be changed.
			// We don't want to cause the document to scroll back
			// up while rendering.
			this.correctViewportOrigin(insets, resultingWidth, resultingHeight);
			// Depends on width, height and origin
			this.resetScrollBars(rs);
		} else {
			bodyLayout.x = insets.left;
			bodyLayout.y = insets.top;
		}
	}

	private int getVUnitIncrement(RenderState renderState) {
		if (renderState != null) {
			return renderState.getFontMetrics().getHeight();
		} else {
			return new BlockRenderState(null).getFontMetrics().getHeight();
		}
	}

	private boolean resettingScrollBars = false;

	/**
	 * Changes scroll bar state to match viewport origin.
	 */
	private void resetScrollBars(RenderState renderState) {
		// Expected to be called only in the GUI thread.
		this.resettingScrollBars = true;
		try {
			RBlockViewport bodyLayout = this.bodyLayout;
			if (bodyLayout != null) {
				Insets insets = this.getInsets(this.hasHScrollBar,
						this.hasVScrollBar);
				JScrollBar vsb = this.vScrollBar;
				if (vsb != null) {
					int newValue = insets.top - bodyLayout.y;
					int newExtent = this.height - insets.top - insets.bottom;
					int newMin = 0;
					int newMax = bodyLayout.height;
					vsb.setValues(newValue, newExtent, newMin, newMax);
					vsb.setUnitIncrement(this.getVUnitIncrement(renderState));
					vsb.setBlockIncrement(newExtent);
				}
				JScrollBar hsb = this.hScrollBar;
				if (hsb != null) {
					int newValue = insets.left - bodyLayout.x;
					int newExtent = this.width - insets.left - insets.right;
					int newMin = 0;
					int newMax = bodyLayout.width;
					hsb.setValues(newValue, newExtent, newMin, newMax);
				}
			}
		} finally {
			this.resettingScrollBars = false;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.UIControl#paintSelection(java.awt.Graphics,
	 *      boolean, org.xamjwg.html.renderer.RenderablePoint,
	 *      org.xamjwg.html.renderer.RenderablePoint)
	 */
	public boolean paintSelection(Graphics g, boolean inSelection,
			RenderableSpot startPoint, RenderableSpot endPoint) {
		Graphics newG = g.create();
		try {
			Insets insets = this.getInsets(this.hasHScrollBar,
					this.hasVScrollBar);
			// Just clip, don't translate.
			newG.clipRect(insets.left, insets.top, this.width - insets.left
					- insets.right, this.height - insets.top - insets.bottom);
			return super
					.paintSelection(newG, inSelection, startPoint, endPoint);
		} finally {
			newG.dispose();
		}
		// boolean endSelectionLater = false;
		// if(inSelection) {
		// if(startPoint.renderable == this || endPoint.renderable == this) {
		// return false;
		// }
		// }
		// else {
		// if(startPoint.renderable == this || endPoint.renderable == this) {
		// // This can only occur if the selection point
		// // is on the margin or border or the block.
		// inSelection = true;
		// if(startPoint.renderable == this && endPoint.renderable == this) {
		// // Start and end selection points on margin or border.
		// endSelectionLater = true;
		// }
		// }
		// }
		// RBlockViewport bodyLayout = this.bodyLayout;
		// if(bodyLayout != null) {
		// Insets insets = this.getInsets(this.hasHScrollBar,
		// this.hasVScrollBar);
		// Graphics newG = g.create(insets.left, insets.top, this.width -
		// insets.left - insets.right, this.height - insets.top -
		// insets.bottom);
		// try {
		// newG.translate(bodyLayout.x - insets.left, bodyLayout.y -
		// insets.top);
		// boolean newInSelection = bodyLayout.paintSelection(newG, inSelection,
		// startPoint, endPoint);
		// if(endSelectionLater) {
		// return false;
		// }
		// return newInSelection;
		// } finally {
		// newG.dispose();
		// }
		// }
		// else {
		// return inSelection;
		// }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.BoundableRenderable#getRenderablePoint(int,
	 *      int)
	 */
	public RenderableSpot getLowestRenderableSpot(int x, int y) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			Insets insets = this.getInsets(this.hasHScrollBar,
					this.hasVScrollBar);
			if (x > insets.left && x < this.width - insets.right
					&& y > insets.top && y < this.height - insets.bottom) {
				return bodyLayout.getLowestRenderableSpot(x - bodyLayout.x, y
						- bodyLayout.y);
			} else {
				return new RenderableSpot(this, x, y);
			}
		} else {
			return new RenderableSpot(this, x, y);
		}
	}

	/**
	 * RBlocks should only be invalidated if one of their properties change, or
	 * if a descendent changes, or if a style property of an ancestor is such
	 * that it could produce layout changes in this RBlock.
	 */
	public void invalidateLayoutLocal() {
		super.invalidateLayoutLocal();
		this.lastTentativeHeight = -1;
		this.lastTentativeWidth = -1;
		this.lastWhiteSpace = -1;
		this.lastFloatBounds = INVALID_FLOAT_BOUNDS;
		this.lastAdjustTentativeHeight = -1;
		this.lastAdjustTentativeWidth = -1;
		this.lastAdjustExpandHeight = null;
		this.lastAdjustExpandWidth = null;
		this.lastAdjustFloatBounds = INVALID_FLOAT_BOUNDS;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.BoundableRenderable#onMouseClick(java.awt.event.MouseEvent,
	 *      int, int)
	 */
	public boolean onMouseClick(MouseEvent event, int x, int y) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			if (!bodyLayout.onMouseClick(event, x - bodyLayout.x, y
					- bodyLayout.y)) {
				return false;
			}
		}
		if (!HtmlController.getInstance().onMouseClick(this.modelNode, event,
				x, y)) {
			return false;
		}
		if (this.backgroundColor != null) {
			return false;
		}
		return true;
	}

	public boolean onDoubleClick(MouseEvent event, int x, int y) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			if (!bodyLayout.onDoubleClick(event, x - bodyLayout.x, y
					- bodyLayout.y)) {
				return false;
			}
		}
		if (this.backgroundColor != null) {
			return false;
		}
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.BoundableRenderable#onMouseDisarmed(java.awt.event.MouseEvent)
	 */
	public boolean onMouseDisarmed(MouseEvent event) {
		BoundableRenderable br = this.armedRenderable;
		if (br != null) {
			try {
				return br.onMouseDisarmed(event);
			} finally {
				this.armedRenderable = null;
			}
		} else {
			return true;
		}
	}

	private BoundableRenderable armedRenderable;

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.BoundableRenderable#onMousePressed(java.awt.event.MouseEvent,
	 *      int, int)
	 */
	public boolean onMousePressed(MouseEvent event, int x, int y) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			int newX = x - bodyLayout.x;
			int newY = y - bodyLayout.y;
			if (bodyLayout.contains(newX, newY)) {
				this.armedRenderable = bodyLayout;
				if (!bodyLayout.onMousePressed(event, newX, newY)) {
					return false;
				}
			} else {
				this.armedRenderable = null;
			}
		} else {
			this.armedRenderable = null;
		}
		if (!HtmlController.getInstance().onMouseDown(this.modelNode, event, x,
				y)) {
			return false;
		}
		if (this.backgroundColor != null) {
			return false;
		}
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.BoundableRenderable#onMouseReleased(java.awt.event.MouseEvent,
	 *      int, int)
	 */
	public boolean onMouseReleased(MouseEvent event, int x, int y) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			int newX = x - bodyLayout.x;
			int newY = y - bodyLayout.y;
			if (bodyLayout.contains(newX, newY)) {
				this.armedRenderable = null;
				if (!bodyLayout.onMouseReleased(event, newX, newY)) {
					return false;
				}
			} else {
				BoundableRenderable br = this.armedRenderable;
				if (br != null) {
					br.onMouseDisarmed(event);
				}
			}
		}
		if (!HtmlController.getInstance()
				.onMouseUp(this.modelNode, event, x, y)) {
			return false;
		}
		if (this.backgroundColor != null) {
			return false;
		}
		return true;
	}

	public Color getPaintedBackgroundColor() {
		return this.backgroundColor;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.renderer.RCollection#getRenderables()
	 */
	public Iterator getRenderables() {
		final RBlockViewport bodyLayout = this.bodyLayout;
		return new Iterator() {
			private RBlockViewport bl = bodyLayout;

			public boolean hasNext() {
				return bl != null;
			}

			public Object next() {
				if (bl == null) {
					throw new NoSuchElementException();
				}
				try {
					return bl;
				} finally {
					bl = null;
				}
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.xamjwg.html.domimpl.ContainingBlockContext#repaint(org.xamjwg.html.domimpl.RenderableContext)
	 */
	public void repaint(ModelNode modelNode) {
		// this.invalidateRenderStyle();
		this.repaint();
	}

	// public boolean extractSelectionText(StringBuffer buffer, boolean
	// inSelection, RenderableSpot startPoint, RenderableSpot endPoint) {
	// RBlockViewport bodyLayout = this.bodyLayout;
	// if(bodyLayout != null) {
	// inSelection = inSelection ? endPoint.renderable != this :
	// startPoint.renderable == this;
	// return bodyLayout.extractSelectionText(buffer, inSelection, startPoint,
	// endPoint);
	// }
	// else {
	// return inSelection;
	// }
	// }

	public void updateWidgetBounds(int guiX, int guiY) {
		super.updateWidgetBounds(guiX, guiY);
		boolean hscroll = this.hasHScrollBar;
		boolean vscroll = this.hasVScrollBar;
		if (hscroll || vscroll) {
			Insets insets = this.getInsets(hscroll, vscroll);
			if (hscroll) {
				JScrollBar hsb = this.hScrollBar;
				if (hsb != null) {
					hsb.setBounds(guiX + insets.left, guiY + this.height
							- insets.bottom, this.width - insets.left
							- insets.right, SCROLL_BAR_THICKNESS);
				}
			}
			if (vscroll) {
				JScrollBar vsb = this.vScrollBar;
				if (vsb != null) {
					vsb.setBounds(guiX + this.width - insets.right, guiY
							+ insets.top, SCROLL_BAR_THICKNESS, this.height
							- insets.top - insets.bottom);
				}
			}
		}
	}

	public void scrollHorizontalTo(int newX) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			Insets insets = this.getInsets(this.hasHScrollBar,
					this.hasVScrollBar);
			int viewPortX = newX;
			if (viewPortX > insets.left) {
				bodyLayout.x = insets.left;
			} else if (viewPortX < this.width - insets.right - bodyLayout.width) {
				bodyLayout.x = Math.min(insets.left, this.width - insets.right
						- bodyLayout.width);
			} else {
				bodyLayout.x = viewPortX;
			}
			this.resetScrollBars(null);
			this.updateWidgetBounds();
			this.repaint();
		}
	}

	public void scrollVerticalTo(int newY) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			Insets insets = this.getInsets(this.hasHScrollBar,
					this.hasVScrollBar);
			int viewPortY = newY;
			if (viewPortY > insets.top) {
				bodyLayout.y = insets.top;
			} else if (viewPortY < this.height - insets.bottom
					- bodyLayout.height) {
				bodyLayout.y = Math.min(insets.top, this.height - insets.bottom
						- bodyLayout.height);
			} else {
				bodyLayout.y = viewPortY;
			}
			this.resetScrollBars(null);
			this.updateWidgetBounds();
			this.repaint();
		}
	}

	public void scrollByUnits(int orientation, int units) {
		int offset = orientation == JScrollBar.VERTICAL ? this
				.getVUnitIncrement(null)
				* units : units;
		this.scrollBy(orientation, offset);
	}

	public void scrollBy(int orientation, int offset) {
		RBlockViewport bodyLayout = this.bodyLayout;
		if (bodyLayout != null) {
			switch (orientation) {
			case JScrollBar.HORIZONTAL:
				this.scrollHorizontalTo(bodyLayout.x - offset);
				break;
			case JScrollBar.VERTICAL:
				this.scrollVerticalTo(bodyLayout.y - offset);
				break;
			}
		}
	}

	/**
	 * Scrolls the viewport's origin to the given location, or
	 * as close to it as possible.
	 * <p>
	 * This method should be invoked in the GUI thread.
	 * @param bounds The bounds of the scrollable area that should become visible.
	 * @param xIfNeeded If this parameter is <code>true</code> the x coordinate
	 *                  is changed only if the horizontal bounds are not currently visible.
	 * @param yIfNeeded If this parameter is <code>true</code> the y coordinate
	 *                  is changed only if the vertical bounds are not currently visible.
	 */
	public void scrollTo(Rectangle bounds, boolean xIfNeeded, boolean yIfNeeded) {
		boolean hscroll = this.hasHScrollBar;
		boolean vscroll = this.hasVScrollBar;
		if(hscroll || vscroll) {
			RBlockViewport bv = this.bodyLayout;
			Insets insets = this.getInsets(hscroll, vscroll);
			int vpheight = this.height - insets.top - insets.bottom;
			int vpwidth = this.width - insets.left - insets.right;
			int tentativeX = insets.left - bounds.x;
			int tentativeY = insets.top - bounds.y;
			boolean needCorrection = false;
			if(!(xIfNeeded && tentativeX <= bv.x && -tentativeX + bv.x + bounds.width <= vpwidth)) {
				bv.setX(tentativeX);
				needCorrection = true;
			}
			if(!(yIfNeeded && tentativeY <= bv.y && -tentativeY + bv.y + bounds.height <= vpheight)) {
				bv.setY(tentativeY);
				needCorrection = true;
			}
			if(needCorrection) {
				this.correctViewportOrigin(insets, this.width, this.height);
			}
		}		
	}
	
	private void scrollToSBValue(int orientation, int value) {
		Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar);
		switch (orientation) {
		case JScrollBar.HORIZONTAL:
			int xOrigin = insets.left - value;
			this.scrollHorizontalTo(xOrigin);
			break;
		case JScrollBar.VERTICAL:
			int yOrigin = insets.top - value;
			this.scrollVerticalTo(yOrigin);
			break;
		}
	}

	public RBlockViewport getRBlockViewport() {
		return this.bodyLayout;
	}

	public boolean extractSelectionText(StringBuffer buffer, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) {
		boolean result = super.extractSelectionText(buffer, inSelection, startPoint, endPoint);
		String br = System.getProperty("line.separator");
		if(inSelection) {
			buffer.insert(0, br);
		}
		if(result) {
			buffer.append(br);
		}
		return result;
	}

	public String toString() {
		return "RBlock[node=" + this.modelNode + "]";
	}

	public FloatingBounds getExportableFloatingBounds() {
		RBlockViewport viewport = this.bodyLayout;
		FloatingBounds viewportBounds = viewport.getExportableFloatingBounds();
		if (viewportBounds == null) {
			return null;
		}
		Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar);
		return new ShiftedFloatingBounds(viewportBounds, insets.left,
				insets.right, viewport.y);
	}

	private class LocalAdjustmentListener implements AdjustmentListener {
		private final int orientation;

		public LocalAdjustmentListener(int orientation) {
			this.orientation = orientation;
		}

		public void adjustmentValueChanged(AdjustmentEvent e) {
			if (RBlock.this.resettingScrollBars) {
				return;
			}
			switch (e.getAdjustmentType()) {
			case AdjustmentEvent.UNIT_INCREMENT:
				// fall through
			case AdjustmentEvent.UNIT_DECREMENT:
				// fall through
			case AdjustmentEvent.BLOCK_INCREMENT:
				// fall through
			case AdjustmentEvent.BLOCK_DECREMENT:
				// fall through
			case AdjustmentEvent.TRACK: {
				int value = e.getValue();
				RBlock.this.scrollToSBValue(this.orientation, value);
				break;
			}
			}
		}
	}

	private static class BodyFilter implements NodeFilter {
		public boolean accept(Node node) {
			return node instanceof org.w3c.dom.html2.HTMLBodyElement;
		}
	}
}