/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package woolpack.html;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import woolpack.dom.DomConstants;
import woolpack.dom.DomContext;
import woolpack.dom.DomExpression;
import woolpack.dom.InsertElementToParent;
import woolpack.dom.RemoveAttr;
import woolpack.dom.Serial;
import woolpack.dom.UpdateAttrValue;
import woolpack.dom.XPath;
import woolpack.el.FixEL;
import woolpack.utils.CheckUtils;
import woolpack.utils.NodeFindable;
import woolpack.utils.NodeFindableFactory;

/**
 * HTML のフレームをテーブルに変換する{@link DomExpression}。 各フレームをマージする際にターゲットの HTML HEAD
 * タグを残す。 Struts の Tiles プラグインのようにレイアウトを制御するために使用する。 適用しているパターン：Proxy。
 * 
 * @author nakamura
 * 
 */
public class FrameToTable implements DomExpression {
	private static final DomExpression BODY = new InsertElementToParent("BODY",
			DomConstants.NULL);

	static final DomExpression TABLE = new InsertElementToParent("TABLE",
			new UpdateAttrValue("width", new FixEL("100%")));

	static final DomExpression TR = new InsertElementToParent("TR",
			DomConstants.NULL);

	private static final DomExpression TD = new InsertElementToParent("TD",
			new Serial(new UpdateAttrValue("align", new FixEL("left")),
					new UpdateAttrValue("valign", new FixEL("top"))));

	private final NodeFindable xpathBody;

	private final NodeFindable xpathHtmlBody;

	private final NodeFindable xpathFrame;

	private final NodeFindable xpathFramesetRows;

	private final NodeFindable xpathFramesetCols;

	private final NodeFindable xpathHtml;

	private final NodeFindable xpathFrame2;

	private final String frameId;

	private final DomExpression nodeMaker;

	private final DomExpression framesetRow;

	private final DomExpression root;

	/**
	 * コンストラクタ。
	 * 
	 * @param frameId
	 *            フレームが定義された HTML の id。
	 * @param targetName
	 *            {@link DomContext#getId()}で生成された DOM
	 *            ノードを流し込む"//frame[\@name]"の値。
	 * @param nodeMaker
	 *            ノードを作成する委譲先。
	 * @param factory
	 *            {@link NodeFindable}のファクトリ。
	 * @throws NullPointerException
	 *             引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException
	 *             frameId または targetName が空の場合。
	 */
	public FrameToTable(final String frameId, final String targetName,
			final DomExpression nodeMaker, final NodeFindableFactory factory) {
		CheckUtils.checkNotEmpty(frameId);
		CheckUtils.checkNotEmpty(targetName);
		CheckUtils.checkNotNull(nodeMaker);

		this.nodeMaker = nodeMaker;
		this.frameId = frameId;
		xpathBody = factory.newInstance("//BODY");
		xpathHtmlBody = factory.newInstance("/HTML/BODY");
		xpathFrame = factory.newInstance("//FRAME");

		xpathFramesetRows = factory.newInstance("FRAMESET[@rows]");
		xpathFramesetCols = factory.newInstance("FRAMESET[@cols]");
		xpathHtml = factory.newInstance("/HTML");
		xpathFrame2 = factory.newInstance("FRAME");

		final DomExpression removeTargetAttr = new XPath(factory
				.newInstance("//*[@target=\"" + targetName + "\"]"),
				new RemoveAttr("target"));

		final DomExpression processFrame = new DomExpression() {
			public void interpret(final DomContext context) {
				final Element element = (Element) context.getNode();
				if (targetName.equals(element.getAttribute("name"))) {
					return;
				}
				final DomContext newContext = context.copy();
				newContext.setId(element.getAttribute("src"));
				nodeMaker.interpret(newContext);
				removeTargetAttr.interpret(newContext);

				final Node target = xpathBody
						.evaluateOne(newContext.getNode());
				Node child = target.getFirstChild();
				while (child != null) {
					context.getNode().getParentNode()
							.insertBefore(
									DomConstants.getDocumentNode(
											context.getNode()).importNode(
											child, true), context.getNode());
					child = child.getNextSibling();

				}
				DomConstants.REMOVE_THIS.interpret(context);
			}
		};

		final DomExpression processRow = new DomExpression() {
			public void interpret(final DomContext context) {
				framesetRow.interpret(context);
			}
		};

		framesetRow = new Serial(new XPath(xpathFramesetRows, new Serial(TR,
				TD, TABLE, processRow, DomConstants.RETAIN_CHILDREN)),
				new XPath(xpathFramesetCols, new Serial(TR, TD, TABLE, TR,
						new Frame2TableFramesetCol(processRow, processFrame),
						DomConstants.RETAIN_CHILDREN)), new XPath(xpathFrame2,
						new Serial(TR, TD, processFrame)));

		root = new XPath(xpathHtml, new Serial(new XPath(xpathFramesetRows,
				new Serial(BODY, TABLE, processRow,
						DomConstants.RETAIN_CHILDREN)), new XPath(
				xpathFramesetCols, new Serial(BODY, TABLE, TR,
						new Frame2TableFramesetCol(processRow, processFrame),
						DomConstants.RETAIN_CHILDREN))));
	}

	/**
	 * @throws NullPointerException
	 *             引数が null の場合。
	 */
	public void interpret(final DomContext context) {
		final Node base;
		{
			final DomContext frameContext = context.copy();
			frameContext.setId(frameId);
			nodeMaker.interpret(frameContext);
			root.interpret(frameContext);
			base = frameContext.getNode();
		}

		nodeMaker.interpret(context);

		final Node baseBody = DomConstants.getDocumentNode(context.getNode())
				.importNode(xpathHtmlBody.evaluateOne(base), true);
		final Node baseFrame = xpathFrame.evaluateOne(baseBody);
		final Node targetBody = xpathHtmlBody.evaluateOne(context.getNode());
		{
			Node child = targetBody.getFirstChild();
			while (child != null) {
				baseFrame.getParentNode().insertBefore(child, baseFrame);
				child = targetBody.getFirstChild();
			}
		}
		final Node baseTd = baseFrame.getParentNode();
		baseTd.removeChild(baseFrame);
		{
			Node child = baseBody.getFirstChild();
			while (child != null) {
				targetBody.appendChild(child);
				child = baseBody.getFirstChild();
			}
		}
	}

	class Frame2TableFramesetCol implements DomExpression {
		private final DomExpression processRow;

		private final DomExpression processFrame;

		Frame2TableFramesetCol(final DomExpression processRow,
				final DomExpression processFrame) {
			this.processRow = processRow;
			this.processFrame = processFrame;
		}

		public void interpret(final DomContext context) {
			final DomExpression e0 = new InsertElementToParent("TD",
					new Serial(new UpdateAttrValue("align", new FixEL("left")),
							new UpdateAttrValue("valign", new FixEL("top")),
							new Frame2TableTdWidth(
									((Element) context.getNode())
											.getAttribute("cols"))));
			final DomExpression e1 = new Serial(new XPath(
					FrameToTable.this.xpathFramesetRows, new Serial(e0,
							FrameToTable.TABLE, processRow,
							DomConstants.RETAIN_CHILDREN)),
					new XPath(FrameToTable.this.xpathFramesetCols,
							new Serial(e0, FrameToTable.TABLE, FrameToTable.TR,
									new Frame2TableFramesetCol(processRow,
											processFrame),
									DomConstants.RETAIN_CHILDREN)), new XPath(
							FrameToTable.this.xpathFrame2, new Serial(e0,
									processFrame)));
			e1.interpret(context);
		}
	}
}

class Frame2TableTdWidth implements DomExpression {
	private static final int PERCENT_ALL = 100;

	private final String[] array;

	private int index;

	Frame2TableTdWidth(final String divide) {
		array = divide.split(",");
		index = 0;
		int total = 0;
		int astarIndex = -1;
		for (int i = 0; i < array.length; i++) {
			if (array[i].equals("*")) {
				astarIndex = i;
				continue;
			}
			if (!array[i].endsWith("%")) {
				throw new IllegalArgumentException("\"%\" not found : "
						+ divide);
			}
			total += Integer.parseInt(array[i].substring(0,
					array[i].length() - 1));
		}
		if (astarIndex >= 0) {
			if (total >= PERCENT_ALL) {
				throw new IllegalArgumentException("over 100%: " + divide);
			}
			array[astarIndex] = String.valueOf(PERCENT_ALL - total) + '%';
		} else {
			if (total != PERCENT_ALL) {
				throw new IllegalArgumentException("total not 100%: " + divide);
			}
		}
	}

	public void interpret(final DomContext context) {
		((Element) context.getNode()).setAttribute("width", array[index++]);
	}
}
