/*******************************************************************************
 * 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.normalizer.parser.helper;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.List;

import blanco.html.parser.BlancoHtmlContentHandler;
import blanco.html.parser.valueobject.BlancoHtmlAttribute;

/**
 * TagSoup が数値文字リファレンス化しすぎるものをデコードして可読性を出すためのコンテンツ・ハンドラー
 * 
 * @author IGA Tosiki
 */
public class BlancoHtmlDecodeNumericCharacterReferenceContentHandler implements
        BlancoHtmlContentHandler {
    /**
     * 文字エンコーディング。
     */
    protected String fEncoding = "Windows-31J";

    /**
     * 文字エンコーディングとして妥当かどうかをチェックする。
     */
    protected CharsetEncoder fValidateCharsetEncode = null;

    /**
     * チェイン先のコンテンツ・ハンドラー。
     */
    protected BlancoHtmlContentHandler fHandler = null;

    /**
     * 出力ハンドラーを設定します。
     * 
     * @param handler
     *            ハンドラー。
     */
    public void setHandler(final BlancoHtmlContentHandler handler) {
        fHandler = handler;
    }

    /**
     * 出力ハンドラーを取得します。
     * 
     * @return 出力ハンドラー。
     */
    public BlancoHtmlContentHandler getHandler() {
        return fHandler;
    }

    /**
     * 文字エンコーディングを指定します。
     * 
     * @param encoding
     *            文字エンコーディング。
     */
    public void setEncoding(final String encoding) {
        fEncoding = encoding;
    }

    /**
     * 文字エンコーディングを取得します。
     * 
     * @return 文字エンコーディング。
     */
    public String getEncoding() {
        return fEncoding;
    }

    /**
     * ドキュメントの開始。
     * 
     * @throws IOException
     */
    public void startDocument() throws IOException {
        if (fHandler == null) {
            throw new IllegalArgumentException(
                    "ハンドラが null なまま startDocument が呼び出されました。");
        }

        fValidateCharsetEncode = Charset.forName(getEncoding()).newEncoder();

        fHandler.startDocument();
    }

    public void endDocument() throws IOException {
        fHandler.endDocument();
    }

    public void startElement(String name, List<BlancoHtmlAttribute> attrs)
            throws IOException {
        for (BlancoHtmlAttribute attr : attrs) {
            if (attr.getValue() != null) {
                final String decoded = decode(attr.getValue());
                if (attr.getValue().equals(decoded) == false) {
                    attr.setValue(decoded);
                }
            }
        }

        fHandler.startElement(name, attrs);
    }

    public void endElement(String name) throws IOException {
        fHandler.endElement(name);
    }

    /**
     * 文字列。文字参照などの処理がされていない文字列が「そのまま」戻ります。
     * 
     * @param argCharacters
     *            文字列。文字参照などは「そのまま」格納します。
     */
    public void characters(final String argCharacters) throws IOException {
        fHandler.characters(decode(argCharacters));
    }

    public void comments(String argComments, int argType) throws IOException {
        fHandler.comments(argComments, argType);
    }

    protected String decode(final String argCharacters) throws IOException {
        final StringReader reader = new StringReader(argCharacters);
        final StringBuffer result = new StringBuffer();

        for (;;) {
            reader.mark(10);
            final int iRead = reader.read();
            if (iRead < 0) {
                break;
            }
            char cRead = (char) iRead;
            if (cRead == '&') {
                if (isReferenceNumber(reader)) {
                    final int number = getReferenceNumber(reader);

                    if (fValidateCharsetEncode.canEncode((char) number)) {
                        result.append((char) number);
                    } else {
                        if (number == 160) {
                            result.append("&nbsp;");
                            // TODO 他の数字についてもチェック。
                        } else {
                            // 仕方がないのでもとのまま出力。
                            result.append("&#" + number + ";");
                        }
                    }
                } else {
                    result.append(cRead);
                }
            } else {
                result.append(cRead);
            }
        }
        return result.toString();
    }

    /**
     * 最初の & はチェック済みとして処理。
     * 
     * @param reader
     * @return
     * @throws IOException
     */
    private boolean isReferenceNumber(Reader reader) throws IOException {
        // 参考: http://e-words.jp/p/r-htmlentity.html
        reader.mark(12);
        final int iRead = reader.read();
        if (iRead < 0) {
            reader.reset();
            return false;
        }
        final char cRead = (char) iRead;
        if (cRead != '#') {
            reader.reset();
            return false;
        }
        reader.reset();
        return true;
    }

    /**
     * 最初の & はチェック済みとして処理。
     * 
     * @param reader
     * @return
     * @throws IOException
     */
    private int getReferenceNumber(Reader reader) throws IOException {
        // 参考: http://e-words.jp/p/r-htmlentity.html
        final StringBuffer buf = new StringBuffer();
        reader.mark(12);
        final int iRead = reader.read();
        if (iRead < 0) {
            reader.reset();
            return -1;
        }
        final char cRead = (char) iRead;
        if (cRead != '#') {
            reader.reset();
            return -1;
        }
        for (;;) {
            final int iRead2 = reader.read();
            if (iRead2 < 0) {
                reader.reset();
                return -1;
            }
            final char cRead2 = (char) iRead2;
            if (cRead2 == ';') {
                // 文字列終了。
                break;
            } else {
                buf.append(cRead2);
            }
        }

        // TODO 16進は未考慮
        return Integer.parseInt(buf.toString());
    }
}
