/*
 * 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.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 XPATH_BODY;
	private final NodeFindable XPATH_HTML_BODY;
	private final NodeFindable XPATH_FRAME;

	final NodeFindable XPATH_FRAMESET_ROWS;
	final NodeFindable XPATH_FRAMESET_COLS;
	private final NodeFindable XPATH_HTML;
	final NodeFindable XPATH_FRAME2;

	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){
		frameId.charAt(0);
		targetName.charAt(0);
		nodeMaker.getClass();
		
		this.nodeMaker = nodeMaker;
		this.frameId = frameId;	
		XPATH_BODY = factory.newInstance("//BODY");
		XPATH_HTML_BODY = factory.newInstance("/HTML/BODY");
		XPATH_FRAME = factory.newInstance("//FRAME");

		XPATH_FRAMESET_ROWS = factory.newInstance("FRAMESET[@rows]");
		XPATH_FRAMESET_COLS = factory.newInstance("FRAMESET[@cols]");
		XPATH_HTML = factory.newInstance("/HTML");
		XPATH_FRAME2 = 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 = XPATH_BODY.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(XPATH_FRAMESET_ROWS, new Serial(
						TR,
						TD,
						TABLE,
						processRow,
						DomConstants.RETAIN_CHILDREN
				)),
				new XPath(XPATH_FRAMESET_COLS, new Serial(
						TR,
						TD,
						TABLE,
						TR,
						new Frame2TableFramesetCol(processRow, processFrame),
						DomConstants.RETAIN_CHILDREN
				)),
				new XPath(XPATH_FRAME2, new Serial(
						TR,
						TD,
						processFrame
				)));
		
		root = new XPath(XPATH_HTML, new Serial(
				new XPath(XPATH_FRAMESET_ROWS, new Serial(
						BODY,
						TABLE,
						processRow,
						DomConstants.RETAIN_CHILDREN
				)),
				new XPath(XPATH_FRAMESET_COLS, new Serial(
						BODY,
						TABLE,
						TR,
						new Frame2TableFramesetCol(processRow, processFrame),
						DomConstants.RETAIN_CHILDREN
				))
		));
	}
	
	/**
	 * @throws NullPointerException 引数が null の場合。
	 */
	public void interpret(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(XPATH_HTML_BODY.evaluateOne(base), true);
		final Node baseFrame = XPATH_FRAME.evaluateOne(baseBody);
		final Node targetBody = XPATH_HTML_BODY.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.XPATH_FRAMESET_ROWS, new Serial(
							e0,
							FrameToTable.TABLE,
							processRow,
							DomConstants.RETAIN_CHILDREN
					)),
					new XPath(FrameToTable.this.XPATH_FRAMESET_COLS, new Serial(
							e0,
							FrameToTable.TABLE,
							FrameToTable.TR,
							new Frame2TableFramesetCol(processRow, processFrame),
							DomConstants.RETAIN_CHILDREN
					)),
					new XPath(FrameToTable.this.XPATH_FRAME2, new Serial(
							e0,
							processFrame
					)));
			e1.interpret(context);
		}
	}
}

class Frame2TableTdWidth implements DomExpression{
	private final String[] array;
	private int index = 0;
	Frame2TableTdWidth(final String divide){
		array = divide.split(",");
		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 >= 100){
				throw new IllegalArgumentException("over 100%: " + divide);
			}
			array[astarIndex] = String.valueOf(100 - total) + "%";
		}else{
			if(total != 100){
				throw new IllegalArgumentException("total not 100%: " + divide);
			}
		}
	}
	
	public void interpret(final DomContext context) {
		((Element)context.getNode()).setAttribute("width", array[index++]);
	}
}
