/*
 * 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.dom.XPathAttrValue;
import woolpack.utils.OGE;
import woolpack.utils.XPathWrapper;

/**
 * HTML のフレームをテーブルに変換する{@link DomExpression}。
 * 各フレームをマージする際にターゲットの HTML HEADタグを残す。
 * Struts の Tiles プラグインのようにレイアウトを制御するために使用する。
 * @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 OGE("\"100%\"")));
	static final DomExpression TR = new InsertElementToParent("TR", DomConstants.NULL);
	private static final DomExpression TD = new InsertElementToParent("TD", 
			new Serial(
					new UpdateAttrValue("align", new OGE("\"left\"")),
					new UpdateAttrValue("valign", new OGE("\"top\""))
			));
	
	private static final XPathWrapper XPATH_BODY = new XPathWrapper("//BODY");
	private static final XPathWrapper XPATH_HTML_BODY = new XPathWrapper("/HTML/BODY");
	private static final XPathWrapper XPATH_FRAME = new XPathWrapper("//FRAME");

	static final XPathWrapper XPATH_FRAMESET_ROWS = new XPathWrapper("FRAMESET[@rows]");
	static final XPathWrapper XPATH_FRAMESET_COLS = new XPathWrapper("FRAMESET[@cols]");
	private static final XPathWrapper XPATH_HTML = new XPathWrapper("/HTML");
	static final XPathWrapper XPATH_FRAME2 = new XPathWrapper("FRAME");

	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 ノードを作成する委譲先。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException frameId または targetName が空の場合。
	 */
	public FrameToTable(
			final String frameId, 
			final String targetName, 
			final DomExpression nodeMaker){
		frameId.charAt(0);
		targetName.charAt(0);
		nodeMaker.getClass();
		
		this.nodeMaker = nodeMaker;
		this.frameId = frameId;
		
		final DomExpression removeTargetAttr = new XPathAttrValue("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 OGE("\"left\"")),
						new UpdateAttrValue("valign", new OGE("\"top\"")),
						new Frame2TableTdWidth(((Element)context.getNode()).getAttribute("cols"))
				));
		final DomExpression e1 = new Serial(
				new XPath(FrameToTable.XPATH_FRAMESET_ROWS, new Serial(
						e0,
						FrameToTable.TABLE,
						processRow,
						DomConstants.RETAIN_CHILDREN
				)),
				new XPath(FrameToTable.XPATH_FRAMESET_COLS, new Serial(
						e0,
						FrameToTable.TABLE,
						FrameToTable.TR,
						new Frame2TableFramesetCol(processRow, processFrame),
						DomConstants.RETAIN_CHILDREN
				)),
				new XPath(FrameToTable.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++]);
	}
}
