/*
 * $Revision: 1.9 $ $Date: 2006/02/06 16:49:02 $
 * (C) 2004 SUGIMOTO Ken-ichi
 * 作成日： 2004/08/27
 */

package feat.v1.impl.template;

import java.util.List;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import feat.v1.StringUtil;
import feat.v1.template.HTMLAttribute;
import feat.v1.template.HTMLDocument;
import feat.v1.template.HTMLElement;
import feat.v1.template.HTMLNode;
import feat.v1.template.HTMLNodeList;
import feat.v1.template.NodeIterator;
import feat.v1.template.NodeListIterator;
import feat.v1.template.NodeNotFoundException;

/**
 * HTMLのタグを表すクラス。
 * 開始・終了タグのペア、開始タグのみ、終了タグのみの場合がある。
 */
public class HTMLElementImpl extends HTMLCompositeNodeImpl implements HTMLElement, HTMLDocument {

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

    private HTMLAttribute firstAttribute;
    private String tagName;
    private String id;
    private String[] classList;
    private String encoding;
    private boolean startTag;
    private boolean endTag;

    HTMLElementImpl(boolean isDocumentNode) {
        this(null, true, true, isDocumentNode);
    }

    HTMLElementImpl(String tagName, boolean startTag, boolean endTag, boolean isDocumentNode) {
        this.tagName = tagName;
        this.startTag = startTag;
        this.endTag = endTag;

        if ( isDocumentNode )
            setDomNode(new DOMDocumentImpl(this));
        else
            setDomNode(new DOMElementImpl(this));
    }

    public boolean isDocumentNode() {
        return getDomNode() instanceof Document;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /*void addAttribute(HTMLAttribute attr) {
        addAttribute(attr, true);
    }*/

    /**
     * 属性を追加する。
     * @param attr 単体の属性ノード。リスト構造のノードは正しく受け入れられない。
     * @param withWhitespace attrの前にスペースを入れるならtrue
     */
    void addAttribute(HTMLAttribute attr, boolean withWhitespace) {
        if ( attr == null )
            return;

        // 最後の属性ノードを取得
        HTMLAttribute last = firstAttribute != null ? (HTMLAttribute)firstAttribute.getLast() : null;

        // 追加する属性が値付き属性のとき
        if ( attr instanceof HTMLValuedAttribute ) {
            // この要素に属性がなかったらスペースを追加
            if ( last == null ) {
                HTMLWhitespaceAttribute firstSpace = new HTMLWhitespaceAttribute(" ");
                firstAttribute = firstSpace;
                ((HTMLNodeImpl)firstSpace).setParent(this);
                last = firstSpace;
            }
            // 最後が空白ではなかったらスペースを追加
            else if ( withWhitespace && !(last instanceof HTMLWhitespaceAttribute) ) {
                HTMLWhitespaceAttribute spaceNode = new HTMLWhitespaceAttribute(" ");
                last.insertAfter(spaceNode);
                last = spaceNode;
            }
            last.insertAfter(attr);

            HTMLValuedAttribute vattr = (HTMLValuedAttribute)attr;
            String name = vattr.getName();
            if ( name != null ) {
                name = name.toLowerCase();
                if ( name.equals("id") ) {
                    this.id = vattr.getValue();
                }
                else if ( name.equals("class") ) {
                    setClassList(vattr.getValue());
                }
            }
        }
        // 追加する属性がスペースのとき
        else if ( attr instanceof HTMLWhitespaceAttribute ) {
            // 属性ノードがなかったらattrを属性ノードに設定
            if ( last == null ) {
                firstAttribute = attr;
                ((HTMLNodeImpl)attr).setParent(this);
            }
            // 最後がスペースだったら文字列を追加
            else if ( last instanceof HTMLWhitespaceAttribute ) {
                ((HTMLWhitespaceAttribute)last).addText(attr.getValue());
            }
            else {
                last.insertAfter(attr);
            }
        }
        else
            last.insertAfter(attr);
    }

    /*void addWhitespace(String str) {
        if ( str == null || str.length() == 0 ) {
            str = " ";
        }

        HTMLAttribute attr = (HTMLAttribute)NodeSelectUtil.getLastNode(firstAttribute);
        if ( attr != null && attr instanceof HTMLWhitespaceAttribute ) {
            ((HTMLWhitespaceAttribute)attr).addText(str);
        }
        else {
            NodeControlUtil.insertAfter(attr, new HTMLWhitespaceAttribute(str));
        }
    }*/

    public boolean hasStartTag() {
        return startTag;
    }

    /**
     * リストの最初のスペースを除くすべての要素をひつとの属性にまとめる。
     */
    void assemble() {
        if ( firstAttribute != null ) {
            // 属性をすべて削除して独立した属性のリストを得る
            HTMLAttribute attrList = (HTMLAttribute)detachAll(firstAttribute);
            firstAttribute = null;

            // リストの最初のノードがスペースなら要素の属性に追加
            if ( attrList instanceof HTMLWhitespaceAttribute ) {
                HTMLNode wsNode = attrList;
                attrList = (HTMLAttribute)((HTMLNodeImpl)attrList).next;
                wsNode.detach();
                if ( attrList == null )
                    return;
            }

            // リストにノードが残っていたらすべてを1つの属性にまとめる
            StringBuffer buf = TemplateUtil.nodeListToHTML(attrList, new StringBuffer());
            HTMLValuedAttribute a = new HTMLValuedAttribute(null, buf.toString());
            a.setQuote(null);
            addAttribute(a, false);
        }
    }

    HTMLNodeMapImpl getAttributes() {
        HTMLNodeMapImpl ret = new HTMLNodeMapImpl();
        for(NodeIterator it=new NodeListIterator(firstAttribute); it.hasNext(); ) {
            HTMLNode node = it.nextNode();
            if ( node instanceof HTMLValuedAttribute ) {
                HTMLValuedAttribute attr = (HTMLValuedAttribute)node;
                if ( attr.getName() != null )
                    ret.put(attr.getName().toLowerCase(), attr);
            }
        }
        return ret;
    }

    public String toString() {
        return startTagToString(new StringBuffer()).toString();
    }

    // HTMLDocumentの実装 -------------------------------------------------------

    public String getEncoding() {
        return encoding;
    }

    public HTMLNode find(String xpath) throws NodeNotFoundException {
        Node domNode = getDomNode();

        JXPathContext context = JXPathContext.newContext(domNode);
        context.setLenient(true);
        Node found = (Node)context.selectSingleNode(xpath);
        HTMLNode ret = null;
        if ( found instanceof HTMLConvertibleNode ) {
            ret = ((HTMLConvertibleNode)found).convertHTML();
        }

        if ( ret == null )
            throw new NodeNotFoundException("xpath:"+xpath);

        return ret;
    }

    public HTMLNodeList select(String xpath) {
        Node domNode = getDomNode();
        if ( domNode == null )
            return null;

        JXPathContext context = JXPathContext.newContext(domNode);
        context.setLenient(true);
        List list = context.selectNodes(xpath);

        HTMLNodeListImpl ret = new HTMLNodeListImpl();
        for(int i=0; i<list.size(); i++) {
            ret.add(((HTMLConvertibleNode)list.get(i)).convertHTML());
        }
        return ret;
    }

    public String toHTML() {
        StringBuffer buf = new StringBuffer();
        toHTML(buf);
        return buf.toString();
    }


    // HTMLElementの実装 --------------------------------------------------------

    public String getAttribute(String name) {
        String nameL = name.toLowerCase();
        if ( nameL.equals("id") ) {
            return this.id;
        }
        else {
            HTMLValuedAttribute attr = getValuedAttribute(name);
            if ( attr == null )
                return null;
            else
                return attr.getValue();
        }
    }

    public boolean containsClassValue(String classValue) {
        if ( classList != null )
            for(int i=0; i<classList.length; i++) {
                if ( classList[i].equals(classValue) )
                    return true;
            }
        return false;
    }

    /**
     * 値付きの属性を返す。
     * @param name
     * @return 見つからなかったらnull
     */
    HTMLValuedAttribute getValuedAttribute(String name) {
        NodeIterator it = new NodeListIterator(firstAttribute);
        while(it.hasNext()) {
            Object a = it.next();
            if ( a instanceof HTMLValuedAttribute ) {
                HTMLValuedAttribute attr = (HTMLValuedAttribute)a;
                if ( attr.getName().equalsIgnoreCase(name) ) {
                    return attr;
                }
            }
        }
        return null;
    }

    /**
     * 要素の属性を設定する。属性名は大文字小文字を区別しない。
     * 文字列はエスケープされる。
     * @param name String
     * @param value String
     */
    public void setAttribute(String name, String value) {
        String nameL = name.toLowerCase();
        if ( nameL.equals("id") ) {
            this.id = value;
        }
        else if ( nameL.equals("class") ) {
            setClassList(value);
        }

        HTMLValuedAttribute attr = getValuedAttribute(name);
        if ( attr == null ) {
            attr = new HTMLValuedAttribute(name, null);
            addAttribute(attr, true);
        }
        attr.setValue(value);
    }

    private void setClassList(String classValue) {
        classList = StringUtil.split(classValue);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        setAttribute("id", id);
    }

    public String[] getClassList() {
        return classList;
    }

    public void setClassValue(String classValue) {
        setAttribute("class", classValue);
    }

    public boolean hasAttribute() {
        NodeIterator it = new NodeListIterator(firstAttribute);
        while(it.hasNext()) {
            Object a = it.next();
            if ( a instanceof HTMLValuedAttribute ) {
                return true;
            }
        }
        return false;
    }

    public boolean hasAttribute(String name) {
        HTMLValuedAttribute attr = getValuedAttribute(name);
        return attr != null;
    }

    public void removeAttribute(String name) {
        String nameL = name.toLowerCase();
        if ( nameL.equals("id") ) {
            this.id = null;
        }
        else if ( nameL.equals("class") ) {
            this.classList = null;
        }

        HTMLValuedAttribute attr = getValuedAttribute(name);
        if ( attr != null ) {
            attr.setParent(null);
            if ( attr == firstAttribute ) {
                firstAttribute = (HTMLAttribute)attr.next;
            }
            attr.detach();
        }
    }

    public String getTagName() {
        return tagName;
    }

    /**
     * タグ名を設定する。
     * nullを指定するとタグ文字列が出力されなくなる。
     * @param name String
     */
    public void setTagName(String name) {
        tagName = name;
    }

    public boolean hasEndTag() {
        return endTag;
    }

    /**
     * 終了タグが必要ならtrueをセットする。
     * @param v boolean
     */
    public void setEndTag(boolean v) {
        endTag = v;
    }


    // HTMLNodeの実装 -----------------------------------------------------------

    public String getValue() {
        return null;
    }

    public HTMLNode copy(boolean deep) {
        HTMLElementImpl ret = new HTMLElementImpl(isDocumentNode());

        // プロパティのコピー
        ret.tagName = tagName;
        ret.id = id;
        if ( classList != null )
            ret.classList = (String[])classList.clone();
        ret.startTag = startTag;
        ret.endTag = endTag;
        ret.encoding = encoding;

        // 属性のコピー
        if ( firstAttribute != null ) {
            ret.firstAttribute = (HTMLAttribute)TemplateUtil.copyNodeList((HTMLNodeImpl)firstAttribute, ret);
        }

        if ( deep ) {
            ret.setFirstChild(TemplateUtil.copyNodeList(firstChild, ret));
        }

        return ret;
    }

    public StringBuffer toHTML(StringBuffer buf) {
        // 開始タグ
        startTagToString(buf);

        // 子ノード
        TemplateUtil.nodeListToHTML(firstChild, buf);

        // 終了タグ
        endTagToString(buf);

        return buf;
    }

    private StringBuffer startTagToString(StringBuffer buf) {
        if ( tagName != null && startTag ) {
            buf.append("<").append(tagName);
            TemplateUtil.nodeListToHTML(firstAttribute, buf);
            buf.append(">");
        }
        return buf;
    }

    private StringBuffer endTagToString(StringBuffer buf) {
        if ( (endTag || firstChild != null) && tagName != null ) {
            buf.append("</").append(tagName).append(">");
        }
        return buf;
    }

    public int getNodeType() {
        return isDocumentNode() ? NODETYPE_DOCUMENT : NODETYPE_ELEMENT;
    }
}
