/*
 * $Id: HTMLParser15TemplateParser.java,v 1.3 2005/11/03 13:59:00 sugimotokenichi Exp $
 * Copyright (C) 2005 SUGIMOTO Ken-ichi
 * 作成日: 2005/10/23
 */
package feat.v1.impl.template;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.Enumeration;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.htmlparser.Attribute;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.lexer.Lexer;
import org.htmlparser.lexer.Page;
import org.htmlparser.nodes.RemarkNode;
import org.htmlparser.nodes.TagNode;
import org.htmlparser.nodes.TextNode;
import org.htmlparser.tags.CompositeTag;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

import feat.v1.StringUtil;
import feat.v1.TemplateParsingException;
import feat.v1.config.FileLocator;
import feat.v1.template.HTMLAttribute;
import feat.v1.template.HTMLDocument;
import feat.v1.template.HTMLElement;
import feat.v1.template.HTMLNode;
import feat.v1.template.HTMLTemplate;
import feat.v1.template.NodeNotFoundException;
import feat.v1.template.RemarkTextException;

/**
 * HTMLParser1.5を使ってテンプレートをパースするクラス。
 * @author SUGIMOTO Ken-ichi
 */
public class HTMLParser15TemplateParser implements feat.v1.template.HTMLTemplateParser {

    private static Log log = LogFactory.getLog(HTMLElementImpl.class);

    public HTMLParser15TemplateParser() {
    }

    /* (非 Javadoc)
     * @see feat.v1.template.HTMLTemplateParser#parseTemplate(feat.v1.config.FileLocator, java.lang.String)
     */
    public HTMLTemplate parseTemplate(FileLocator loc, String encoding)
            throws TemplateParsingException, IOException {

        return parse(loc, encoding);
    }

    /**
     * HTMLをパースする。
     * @param loc
     * @param encoding HTMLのエンコーディング。nullを指定するとMETAタグから判断する。
     * @return
     * @throws TemplateParsingException パース中に例外がスローされたとき
     * @throws IOException HTMLデータ読み込み中のIO例外
     */
   private HTMLTemplateImpl parse(FileLocator loc, String encoding) throws TemplateParsingException, IOException {
       HTMLTemplateImpl template = null;
       try {
           Parser parser;
           String location = loc.getURIString();
           if ( encoding == null ) {
               parser = new Parser(location);
           }
           else {
               Page page = null;
               page = new Page(new BufferedInputStream(loc.openInputStream(), 1024), encoding);
               Lexer lexer = new Lexer(page);
               parser = new Parser(lexer);
           }

           // パースしたツリーを変換
           HTMLDocument doc = null;
           doc = new HTMLElementImpl(true);
           for(NodeIterator it = parser.elements(); it.hasMoreNodes(); ) {
               Node node = it.nextNode();
               doc.addChild(convert(node));
           }
           if ( encoding == null )
               encoding = parser.getLexer().getPage().getEncoding();
           template = new HTMLTemplateImpl(loc, encoding, doc);
       }
       catch(ParserException ex) {
           throw new TemplateParsingException("パース中の例外", ex);
       }

       return template;
   }

    private HTMLNode convert(Node node) {
        if ( node instanceof TagNode ) {
            HTMLElementImpl tag = createHTMLElement((TagNode)node);

            // 子ノードの変換
            NodeList children = node.getChildren();
            if ( children != null ) {
                for(int i=0; i<children.size(); i++) {
                    HTMLNode converted = convert(children.elementAt(i));
                    if ( converted != null ) {
                        // 単体で存在する終了タグは直前の開始タグとくっつける
                        if ( !setEndTag(tag, converted) )
                            tag.addChild(converted);
                    }
                }
            }
            return tag;
        }
        else if ( node instanceof RemarkNode ) {
            String text = ((RemarkNode)node).getText();
            try {
                return new HTMLRemarkImpl(text);
            }
            catch (RemarkTextException ex) {
                try {
                    return new HTMLRemarkImpl(StringUtil.replace(text, "--", ""));
                }
                catch (RemarkTextException ex1) {
                    log.warn("", ex1);
                    return new HTMLTextImpl("");
                }
            }
        }
        else if ( node instanceof TextNode ) {
            return new HTMLTextImpl(((TextNode)node).getText());
        }
        else
            return null;
    }

    /**
     * nodeからHTMLElementImplのインスタンスを作る。
     * @param node
     * @return
     */
    private HTMLElementImpl createHTMLElement(TagNode node) {
        // 属性のコピー
        Enumeration attrEnum = node.getAttributesEx().elements();
        Attribute tagNameAttr = (Attribute)attrEnum.nextElement(); // 最初の要素はタグ名

        /*if ( node.getTagName().equalsIgnoreCase("input") || node.getTagName().equalsIgnoreCase("option") )
            System.out.println("DEBUG [HTMLTagNode#<init>] node:"+node);*/

        // 要素名のコピー
        String tagName;
        if ( tagNameAttr != null )
            tagName = StringUtil.replace(tagNameAttr.getName(), "/", "");
        else
            tagName = node.getTagName();

        // 開始タグがあるかどうか
        boolean startTag = !node.isEndTag();

        // 終了タグの必要性フラグ
        boolean endTag = node instanceof CompositeTag || node.isEndTag();

        HTMLElementImpl element = new HTMLElementImpl(tagName, startTag, endTag, false);

        while( attrEnum.hasMoreElements() ) {
            Attribute a = (Attribute)attrEnum.nextElement();
            HTMLAttribute ha;

            String attrName = a.getName();
            String attrValue = a.getValue();

            /*if ( attrName != null && (attrName.equals("checked") || attrName.equals("selected")) ) {
                System.out.println("DEBUG [HTMLTagNode#<init>] attr:"+a);
            }

            if ( attrValue != null && (attrValue.equals("checked") || attrValue.equals("selected")) ) {
                System.out.println("DEBUG [HTMLTagNode#<init>] attr:"+a);
            }*/

            // 空白
            if ( a.isWhitespace() ) {
                ha = new HTMLWhitespaceAttribute(a.getRawValue());
            }
            // 宣言 ( declare )
            else if ( a.isStandAlone() ) {
                HTMLValuedAttribute hva = new HTMLValuedAttribute(a.getName(), null);
                ha = hva;
                hva.setQuote(Character.toString(a.getQuote()));
            }
            // 定義 ( name="value" )
            else {
                HTMLValuedAttribute hva = new HTMLValuedAttribute(a.getName(), a.getValue());
                hva.setQuote(Character.toString(a.getQuote()));

                // 空の値
                if ( a.isEmpty() )
                    hva.setValue(null);
                ha = hva;
            }
            element.addAttribute(ha, false);
        }
        // <!DOCTYPE>
        if ( tagName.equalsIgnoreCase("!DOCTYPE") )
            element.assemble();
        return element;
    }

    /**
     * parentの最後のノードがtagの開始タグならendTagフラグを立てる。
     * @param parent
     * @param tag
     * @return boolean タグのペアが見つかったときはtrue。
     */
    private boolean setEndTag(HTMLElementImpl parent, HTMLNode tag) {
        // tagが終了タグで
        if ( tag instanceof HTMLElement ) {
            if ( !((HTMLElementImpl)tag).hasStartTag() ) {
                HTMLElement endTag = (HTMLElement)tag;
                String tagName = endTag.getTagName();

                // 直前のタグが開始タグ、かつ、まだ終了タグがない
                try {
                    HTMLElementImpl last = (HTMLElementImpl)parent.getLastTag(tagName);
                    if ( last.hasStartTag() && !last.hasEndTag() ) {
                        // last以降にノードがあったらlastの子ノードに移動
                        HTMLNode next = last.next;
                        if ( next != null ) {
                            TemplateUtil.moveAll(next, last);
                        }

                        // 終了タグのフラグ
                        last.setEndTag(true);
                        return true;
                    }
                }
                catch (NodeNotFoundException ex) {
                }
                // 終了タグに対応する開始タグがない(HTMLのエラーっぽい)
            }
        }
        return false;
    }
}
