/*
 * 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.ee;

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

import woolpack.fn.Fn;
import woolpack.fn.FnUtils;
import woolpack.utils.BuildableArrayList;
import woolpack.utils.Utils;
import woolpack.xml.NodeContext;
import woolpack.xml.NodeFindable;
import woolpack.xml.XmlUtils;

/**
 * HTML のフレームをテーブルに変換する{@link Fn}です。
 * 各フレームをマージする際にターゲットの HTML HEAD タグを残します。
 * Struts の Tiles プラグインのようにレイアウトを制御するために使用します。
 * 
 * @author nakamura
 * 
 */
public class FrameToTable implements Fn<EEContext, Void> {
	private static final Fn<NodeContext, Void> BODY = XmlUtils.insertElementToParent("BODY");

	private static final Fn<NodeContext, Void> TABLE = XmlUtils.insertElementToParent("TABLE",
			XmlUtils.updateAttrValue("width", FnUtils.fix("100%")));

	private static final Fn<NodeContext, Void> TR = XmlUtils.insertElementToParent("TR");

	private static final Fn<NodeContext, Void> TD = XmlUtils.insertElementToParent("TD",
			FnUtils.seq(Utils
					.list(XmlUtils.updateAttrValue("align", FnUtils.fix("left")))
					.list(XmlUtils.updateAttrValue("valign", FnUtils.fix("top")))));

	private final NodeFindable xpathBody;
	private final NodeFindable xpathHtmlBody;
	private final NodeFindable xpathFrame;
	private final NodeFindable xpathFramesetRows;
	private final NodeFindable xpathFramesetCols;
	private final NodeFindable xpathFrame2;
	private final String frameId;

	private final Fn<EEContext, Void> nodeMaker;
	private final Fn<EEContext, Void> framesetRow;
	private final Fn<EEContext, Void> root;

	/**
	 * @param frameId フレームが定義された HTML の id。
	 * @param targetName {@link EEContext#getId()}で生成された DOM ノードを流し込む"//frame[\@name]"の値。
	 * @param nodeMaker ノードを作成する委譲先。
	 * @param factory {@link NodeFindable}のファクトリ。
	 */
	public FrameToTable(
			final String frameId,
			final String targetName,
			final Fn<EEContext, Void> nodeMaker,
			final Fn<String, NodeFindable> factory) {

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

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

		final Fn<NodeContext, Void> removeTargetAttr = XmlUtils.findNode(
				factory.exec("//*[@target=\"" + targetName + "\"]"),
				XmlUtils.removeAttr("target"));

		final Fn<EEContext, Void> processFrame = new Fn<EEContext, Void>() {
			public Void exec(final EEContext context) {
				final Element element = (Element) context.getNode();
				if (targetName.equals(element.getAttribute("name"))) {
					return null;
				}
				
				final String id = context.getId();
				final Node node = context.getNode();
				try {
					context.setId(element.getAttribute("src"));
					nodeMaker.exec(context);
					removeTargetAttr.exec(context);
					final Node target = xpathBody.evaluateOne(context.getNode());
					Node child = target.getFirstChild();
					while (child != null) {
						node.getParentNode().insertBefore(
								XmlUtils.getDocumentNode(node).importNode(child, true),
								node);
						child = child.getNextSibling();
					}
				} finally {
					context.setId(id);
					context.setNode(node);
				}
				XmlUtils.REMOVE_THIS.exec(context);
				return null;
			}
		};

		final Fn<EEContext, Void> processRow = new Fn<EEContext, Void>() {
			public Void exec(final EEContext context) {
				framesetRow.exec(context);
				return null;
			}
		};

		framesetRow = FnUtils.seq(Utils
				.list(XmlUtils.findNode(xpathFramesetRows, FnUtils.seq(
						new BuildableArrayList<Fn<? super EEContext, Void>>()
						.list(TR)
						.list(TD)
						.list(TABLE)
						.list(processRow)
						.list(XmlUtils.RETAIN_CHILDREN))))
				.list(XmlUtils.findNode(xpathFramesetCols, FnUtils.seq(
						new BuildableArrayList<Fn<? super EEContext, Void>>()
						.list(TR)
						.list(TD)
						.list(TABLE)
						.list(TR)
						.list(new Frame2TableFramesetCol(processRow, processFrame))
						.list(XmlUtils.RETAIN_CHILDREN))))
				.list(XmlUtils.findNode(xpathFrame2, FnUtils.seq(
						new BuildableArrayList<Fn<? super EEContext, Void>>()
						.list(TR)
						.list(TD)
						.list(processFrame)))));

		root = XmlUtils.findNode(xpathHtml, FnUtils.seq(Utils
				.list(XmlUtils.findNode(xpathFramesetRows, FnUtils.seq(
						new BuildableArrayList<Fn<? super EEContext, Void>>()
						.list(BODY)
						.list(TABLE)
						.list(processRow)
						.list(XmlUtils.RETAIN_CHILDREN))))
				.list(XmlUtils.findNode(xpathFramesetCols, FnUtils.seq(
						new BuildableArrayList<Fn<? super EEContext, Void>>()
						.list(BODY)
						.list(TABLE)
						.list(TR)
						.list(new Frame2TableFramesetCol(processRow, processFrame))
						.list(XmlUtils.RETAIN_CHILDREN))))));
	}

	public Void exec(final EEContext context) {
		final Node base;
		{
			final String id = context.getId();
			final Node node = context.getNode();
			try {
				context.setId(frameId);
				nodeMaker.exec(context);
				root.exec(context);
				base = context.getNode();
			} finally {
				context.setId(id);
				context.setNode(node);
			}
		}

		nodeMaker.exec(context);

		final Node baseBody = XmlUtils.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();
			}
		}
		return null;
	}

	class Frame2TableFramesetCol implements Fn<EEContext, Void> {
		private final Fn<EEContext, Void> processRow;

		private final Fn<EEContext, Void> processFrame;

		Frame2TableFramesetCol(
				final Fn<EEContext, Void> processRow,
				final Fn<EEContext, Void> processFrame) {
			this.processRow = processRow;
			this.processFrame = processFrame;
		}

		public Void exec(final EEContext context) {
			final Fn<EEContext, Void> e0 = XmlUtils.insertElementToParent("TD", FnUtils.seq(
					new BuildableArrayList<Fn<? super EEContext, Void>>()
					.list(XmlUtils.updateAttrValue("align", FnUtils.fix("left")))
					.list(XmlUtils.updateAttrValue("valign", FnUtils.fix("top")))
					.list(new Frame2TableTdWidth(((Element) context.getNode()).getAttribute("cols")))
			));
			final Fn<EEContext, Void> e1 = FnUtils.seq(Utils
					.list(XmlUtils.findNode(FrameToTable.this.xpathFramesetRows, FnUtils.seq(
							new BuildableArrayList<Fn<? super EEContext, Void>>()
							.list(e0)
							.list(FrameToTable.TABLE)
							.list(processRow)
							.list(XmlUtils.RETAIN_CHILDREN))))
					.list(XmlUtils.findNode(FrameToTable.this.xpathFramesetCols, FnUtils.seq(
							new BuildableArrayList<Fn<? super EEContext, Void>>()
							.list(e0)
							.list(FrameToTable.TABLE)
							.list(FrameToTable.TR)
							.list(new Frame2TableFramesetCol(processRow, processFrame))
							.list(XmlUtils.RETAIN_CHILDREN))))
					.list(XmlUtils.findNode(FrameToTable.this.xpathFrame2, FnUtils.seq(Utils
							.list(e0)
							.list(processFrame)))));
			e1.exec(context);
			return null;
		}
	}
}

class Frame2TableTdWidth implements Fn<NodeContext, Void> {
	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 exec(final NodeContext context) {
		((Element) context.getNode()).setAttribute("width", array[index++]);
		return null;
	}
}
