/*******************************************************************************
 * Copyright (c) 2008 IGA Tosiki, NTT DATA BUSINESS BRAINS Corp.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    IGA Tosiki (NTT DATA BUSINESS BRAINS Corp.) - initial API and implementation
 *******************************************************************************/
/*
 * blanco Framework
 * Copyright (C) 2008 NTT DATA BUSINESS BRAINS CORPORATION
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */
package blanco.html.parser;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import blanco.commons.util.BlancoStringUtil;
import blanco.html.parser.helper.BlancoHtmlNullContentHandler;
import blanco.html.parser.util.BlancoHtmlParserUtil;
import blanco.html.parser.valueobject.BlancoHtmlAttribute;

class BlancoHtmlParserImpl implements BlancoHtmlParser {
    protected BufferedReader fReader;

    protected BlancoHtmlContentHandler fHandler;

    public void setHandler(final BlancoHtmlContentHandler handler) {
        fHandler = handler;
    }

    public BlancoHtmlContentHandler getHandler() {
        return fHandler;
    }

    public BufferedReader getReader() {
        return fReader;
    }

    /**
     * パースの際に利用する文字エンコーディングを設定します。
     */
    protected String fEncoding;

    /**
     * パースの際に利用する文字エンコーディングを設定します。
     * 
     * @param encoding
     *            文字エンコーディング。
     */
    public void setEncoding(String encoding) {
        fEncoding = encoding;
    }

    /**
     * パースの際に利用する文字エンコーディングを取得します。
     * 
     * @return 文字エンコーディング。
     */
    public String getEncoding() {
        return fEncoding;
    }

    public void parse(final byte[] argInputHtml) throws IOException {
        // 文字コード判定処理
        if (BlancoStringUtil.null2Blank(getEncoding()).length() == 0) {
            // 文字コード指定が外部から与えられていない場合には自力で判定します。
            setEncoding(BlancoHtmlParserUtil.decideEncoding(argInputHtml));
        }

        // 確定した文字コードでパースを実施。
        final BufferedReader reader = new BufferedReader(new InputStreamReader(
                new ByteArrayInputStream(argInputHtml), getEncoding()));
        try {
            parse(reader);
        } finally {
            reader.close();
        }
    }

    public void parse(final BufferedReader reader) throws IOException {
        fReader = reader;

        if (fHandler == null) {
            // ハンドラーがセットされていないと例外が発生してしまうので nullハンドラーをセットします。
            fHandler = new BlancoHtmlNullContentHandler();
        }

        processDocument();
    }

    protected void processDocument() throws IOException {
        fHandler.startDocument();

        StringBuffer characters = new StringBuffer();
        for (;;) {
            final int iRead = fReader.read();
            if (iRead < 0) {
                break;
            }

            final char cRead = (char) iRead;
            if (cRead == '<') {
                if (characters.length() > 0) {
                    fHandler.characters(characters.toString());
                    characters = new StringBuffer();
                }

                processElementOrComment();
            } else {
                characters.append(cRead);
            }
        }

        if (characters.length() > 0) {
            fHandler.characters(characters.toString());
            characters = new StringBuffer();
        }

        fHandler.endDocument();
    }

    /**
     * 要素またはコメントを処理します。
     * 
     * @throws IOException
     */
    protected void processElementOrComment() throws IOException {
        boolean isStartElement = true;
        final StringBuffer bufElement = new StringBuffer();
        final List<BlancoHtmlAttribute> attributeList = new ArrayList<BlancoHtmlAttribute>();
        for (;;) {
            final int iRead = fReader.read();
            if (iRead < 0) {
                // ???
                break;
            }

            final char cRead = (char) iRead;
            if (BlancoHtmlParserUtil.isWhiteSpace(cRead)) {
                // 空白です。ここから属性
                final BlancoHtmlAttribute attr = processAttribute();
                if (attr != null) {
                    attributeList.add(attr);
                }

                // この空白は属性の区切りなので無視します。
                continue;
            } else if (cRead == '!') {
                if (bufElement.length() == 0) {
                    {
                        // しょっぱなに ! があるので、要素ではなく、むしろコメントの可能性が高い。
                        fReader.mark(100);
                        if (fReader.read() == '-' && fReader.read() == '-') {
                            // コメントが開始されました。
                            processComment(0);
                            break;
                        }
                        fReader.reset();
                    }
                    {
                        // しょっぱなの ! には DOCTYPE の場合もある。
                        fReader.mark(100);
                        char[] buf = new char[8];
                        fReader.read(buf);
                        if (String.valueOf(buf).equals("DOCTYPE ")) {
                            // TODO DOCTYPEの処理実装。
                            // DOCTYPE が開始されました。
                            // processDoctype();
                            // break;
                        }
                        fReader.reset();
                    }
                } else {
                }
            } else if (cRead == '%') {
                if (bufElement.length() == 0) {
                    // しょっぱなに % についてもコメントの場合があるそうな
                    fReader.mark(100);
                    if (fReader.read() == '-' && fReader.read() == '-') {
                        // コメントが開始されました。
                        processComment(1);
                        break;
                    }

                    fReader.reset();
                } else {
                }
            } else if (cRead == '/') {
                if (bufElement.length() == 0) {
                    // しょっぱなに / があるので [</]の形。
                    // これは終了タグと判断
                    isStartElement = false;
                    // 終了フラグをセット!
                    // ここでコンティニュー！
                    continue;
                } else {
                    // 次を読まないと分からない。
                    fReader.mark(100);
                    final char nextChar = (char) fReader.read();
                    if (nextChar == '>') {
                        // 自分で開始してすぐに終了する要素
                        fHandler.startElement(bufElement.toString(),
                                attributeList);
                        fHandler.endElement(bufElement.toString());
                        break;
                    }

                    // 自己完結型ではない。腐ったHTMLか?
                    fReader.reset();
                }
            } else if (cRead == '>') {
                if (isStartElement) {
                    fHandler.startElement(bufElement.toString(), attributeList);

                    // CDATA 特別処理が必要かどうかの判定。
                    final String elementNameUpper = bufElement.toString()
                            .toUpperCase();
                    if (elementNameUpper.equals("SCRIPT")
                            || elementNameUpper.equals("STYLE")) {
                        processCData(elementNameUpper);
                    }
                } else {
                    fHandler.characters("");
                    fHandler.endElement(bufElement.toString());
                }
                break;
            }

            bufElement.append(cRead);
        }
    }

    protected void processComment(final int argType) throws IOException {
        final StringBuffer bufComment = new StringBuffer();
        for (;;) {
            final int iRead = fReader.read();
            if (iRead < 0) {
                // ???
                break;
            }

            final char cRead = (char) iRead;
            if (cRead == '-') {
                fReader.mark(100);
                if (fReader.read() == '-' && fReader.read() == '>') {
                    // コメント終了
                    fHandler.comments(bufComment.toString(), argType);
                    break;
                }

                fReader.reset();
            }

            bufComment.append(cRead);
        }
    }

    protected BlancoHtmlAttribute processAttribute() throws IOException {
        final StringBuffer bufName = new StringBuffer();
        final StringBuffer bufValue = new StringBuffer();

        final BlancoHtmlAttribute attrib = new BlancoHtmlAttribute();
        attrib.setQuote(0);

        for (;;) {
            // name
            fReader.mark(1);
            final int iRead = fReader.read();
            if (iRead < 0) {
                // ???
                break;
            }

            final char cRead = (char) iRead;
            if (cRead == '=') {
                break;
            }
            if (BlancoHtmlParserUtil.isWhiteSpace(cRead)) {
                // 想定しない終了。
                fReader.reset();

                if (bufName.length() > 0) {
                    attrib.setName(bufName.toString());
                    return attrib;
                } else {
                    return null;
                }
            }
            if (cRead == '>') {
                // 想定しない終了。これもありえる。
                fReader.reset();

                if (bufName.length() > 0) {
                    attrib.setName(bufName.toString());
                    return attrib;
                } else {
                    return null;
                }
            }

            bufName.append(cRead);
        }
        outerloop: for (;;) {
            // value
            fReader.mark(1);
            final int iRead = fReader.read();
            if (iRead < 0) {
                // 値の終了
                break;
            }

            final char cRead = (char) iRead;
            if (cRead == '>') {
                // 要素の閉じ記号ですが、ここで消費せずに元のメソッドに戻します。
                fReader.reset();
                break;
            }
            if (BlancoHtmlParserUtil.isWhiteSpace(cRead)) {
                // これはアトリビュート間の空白ですが、ここで消費せずに元のメソッドに戻します。
                fReader.reset();
                break;
            }
            if (cRead == '"') {
                // エスケープ開始。
                attrib.setQuote(2);

                for (;;) {
                    final int iReadIn = fReader.read();
                    if (iReadIn < 0) {
                        // 値の終了
                        break outerloop;
                    }
                    final char cReadIn = (char) iReadIn;
                    if (cReadIn == '"') {
                        // エスケープ終了。
                        break outerloop;
                    }
                    bufValue.append(cReadIn);
                }
            } else if (cRead == '\'') {
                // エスケープ開始。
                attrib.setQuote(1);

                for (;;) {
                    final int iReadIn = fReader.read();
                    if (iReadIn < 0) {
                        // 値の終了
                        break outerloop;
                    }
                    final char cReadIn = (char) iReadIn;
                    if (cReadIn == '\'') {
                        // エスケープ終了。
                        break outerloop;
                    }
                    bufValue.append(cReadIn);
                }
            }

            // ダブルクオートで開始されていなくても、これを取り込む。
            bufValue.append(cRead);
        }

        attrib.setName(bufName.toString());
        attrib.setValue(bufValue.toString());
        return attrib;
    }

    /**
     * SCRIPT と STYLE は HTML 4.0 では CDATA として特別扱いされる必要がある。
     * 
     * http://www.w3.org/TR/html4/types.html#type-cdata
     * 
     * SCRIPT と STYLE に対応した終了タグがあるまで読み続けます。
     * 
     * @param elementNameUpper
     *            エレメント名。
     * @throws IOException
     */
    protected void processCData(final String elementNameUpper)
            throws IOException {
        final StringBuffer characters = new StringBuffer();
        for (;;) {
            fReader.mark(elementNameUpper.length() + 4);
            final int iRead = fReader.read();
            if (iRead < 0) {
                // ???
                break;
            }

            final char cRead = (char) iRead;
            if (cRead == '<') {
                final char[] bufRead = new char[elementNameUpper.length() + 2];
                final int readLen = fReader.read(bufRead);
                // 一旦リセットします。
                fReader.reset();

                final String readString = new String(bufRead, 0, readLen)
                        .toUpperCase();
                if (readString.startsWith("/" + elementNameUpper + ">")) {
                    // 開始タグと同じ名称の終了タグにたどり着きました。
                    // CDATAセクションは終了したものと考えます。o
                    break;
                } else {
                    // 終了タグではありませんでした。
                    characters.append(cRead);

                    // 巻き戻しすぎているので 1文字を破棄します。
                    fReader.read();
                }
            } else {
                characters.append(cRead);
            }
        }

        // 蓄積した文字データをイベント発生させます。
        fHandler.characters(characters.toString());
    }
}
