package blanco.xml.bind;

import java.util.Stack;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;

import blanco.xml.bind.valueobject.BlancoXmlAttribute;
import blanco.xml.bind.valueobject.BlancoXmlCharacters;
import blanco.xml.bind.valueobject.BlancoXmlComment;
import blanco.xml.bind.valueobject.BlancoXmlDocument;
import blanco.xml.bind.valueobject.BlancoXmlDtd;
import blanco.xml.bind.valueobject.BlancoXmlElement;
import blanco.xml.bind.valueobject.BlancoXmlIgnorableWhitespace;
import blanco.xml.bind.valueobject.BlancoXmlLocator;

/**
 * コンテンツハンドラ実装です。
 * 
 * このクラスは XML/バリューオブジェクトマッピング (X/Oマッピング) blancoXmlBinding の一部です。<br>
 * ※このクラスはパッケージ内でのみ利用可能です。
 * 
 * @author IGA Tosiki
 */
class BlancoXmlUnmarshallerContentHandler implements ContentHandler,
        LexicalHandler {
    /**
     * メモリ効率のためなどに文字列のキャッシュなどを実験しましたが、むしろ悪化しました。<br>
     * 現時点では小細工はせずに、処理の改善に注力することとします。
     */

    /**
     * ルートドキュメントを記憶します。
     */
    final BlancoXmlDocument fDocument = new BlancoXmlDocument();

    /**
     * 現在処理中のポイントを記憶します。
     */
    private Stack fDocumentElementStack = new Stack();

    /**
     * CDATAの中にいるのかどうかを記憶します。
     */
    private boolean fIsInCdata = false;

    /**
     * コンテントハンドラオブジェクトを新規作成します。
     */
    public BlancoXmlUnmarshallerContentHandler() {
        fDocumentElementStack.push(fDocument);
    }

    /**
     * ルートドキュメントを取得します。
     * 
     * @return
     */
    public BlancoXmlDocument getDocument() {
        return fDocument;
    }

    // ------------------------------------------------
    // ここからは ContentHandler のためのメソッド。
    // ------------------------------------------------

    public void startDocument() throws SAXException {
        // 既にpush済みです。
    }

    public void endDocument() throws SAXException {
        fDocumentElementStack.pop();
    }

    public void setDocumentLocator(final Locator argLocator) {
        final BlancoXmlLocator locator = new BlancoXmlLocator();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlDocument == false) {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: Documentではないものに対して Locatorをセットしようとしました。");
        }

        ((BlancoXmlDocument) objCurrent).setLocator(locator);

        locator.setPublicId(argLocator.getPublicId());
        locator.setSystemId(argLocator.getSystemId());
        locator.setLineNumber(argLocator.getLineNumber());
        locator.setColumnNumber(argLocator.getColumnNumber());
    }

    public void startPrefixMapping(final String prefix, final String uri)
            throws SAXException {
        // 現在の仕様では、prefixMappingはサポート外です。
        // SAX の XML リーダは、http://xml.org/sax/features/namespaces
        // がtrue（default）の場合には自動置換されます。このため、prefixMappingをサポートしなくとも、問題が無いものと判断しています。
        throw new IllegalArgumentException(
                "BlancoXmlUnmarshallerContentHandler: 発生が想定されないstartPrefixMapping("
                        + prefix + ", " + uri + ")イベントが発火しました。");
    }

    public void endPrefixMapping(final String prefix) throws SAXException {
        // 現在の仕様では、prefixMappingはサポート外です。
        // SAX の XML リーダは、http://xml.org/sax/features/namespaces
        // がtrue（default）の場合には自動置換されます。このため、prefixMappingをサポートしなくとも、問題が無いものと判断しています。
        throw new IllegalArgumentException(
                "BlancoXmlUnmarshallerContentHandler: 発生が想定されないendPrefixMapping("
                        + prefix + ")イベントが発火しました。");
    }

    public void startElement(final String uri, final String localName,
            final String qName, final Attributes atts) throws SAXException {
        final BlancoXmlElement element = new BlancoXmlElement();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlElement) {
            final BlancoXmlElement elementParent = ((BlancoXmlElement) objCurrent);
            elementParent.getChildNodes().add(element);
        } else if (objCurrent instanceof BlancoXmlDocument) {
            ((BlancoXmlDocument) objCurrent).getChildNodes().add(element);
        } else {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: 想定されない型["
                            + objCurrent.getClass().getName()
                            + "]に対して Elementを追加しようとしました。");
        }

        // エレメントをコピーします。
        element.setUri(uri);
        element.setLocalName(localName);
        element.setQName(qName);

        // アトリビュートのコピーを行います。
        copyAttributes(atts, element);

        fDocumentElementStack.push(element);
    }

    /**
     * アトリビュートのコピーを行います。
     * 
     * @param atts
     *            入力となるアトリビュート。
     * @param element
     *            複写先のエレメント。
     */
    private void copyAttributes(final Attributes atts,
            final BlancoXmlElement element) {
        final int attrLength = atts.getLength();
        for (int index = 0; index < attrLength; index++) {
            final BlancoXmlAttribute attribute = new BlancoXmlAttribute();
            element.getAtts().add(attribute);

            attribute.setUri(atts.getURI(index));
            attribute.setLocalName(atts.getLocalName(index));
            attribute.setQName(atts.getQName(index));
            attribute.setType(atts.getType(index));
            attribute.setValue(atts.getValue(index));
        }
    }

    public void endElement(final String uri, final String localName,
            final String qName) throws SAXException {
        fDocumentElementStack.pop();
    }

    public void characters(final char[] ch, final int start, final int length)
            throws SAXException {
        final BlancoXmlCharacters characters = new BlancoXmlCharacters();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlElement) {
            ((BlancoXmlElement) objCurrent).getChildNodes().add(characters);
        } else if (objCurrent instanceof BlancoXmlDocument) {
            ((BlancoXmlDocument) objCurrent).getChildNodes().add(characters);
        } else {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: 想定されない型["
                            + objCurrent.getClass().getName()
                            + "]に対して Charactersを追加しようとしました。");
        }

        characters.setValue(new String(ch, start, length));
        characters.setCdata(fIsInCdata);
    }

    public void ignorableWhitespace(final char[] ch, final int start,
            final int length) throws SAXException {
        final BlancoXmlIgnorableWhitespace ignorableWhitespace = new BlancoXmlIgnorableWhitespace();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlElement) {
            ((BlancoXmlElement) objCurrent).getChildNodes().add(
                    ignorableWhitespace);
        } else if (objCurrent instanceof BlancoXmlDocument) {
            ((BlancoXmlDocument) objCurrent).getChildNodes().add(
                    ignorableWhitespace);
        } else {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: 想定されない型["
                            + objCurrent.getClass().getName()
                            + "]に対して BlancoXmlIgnorableWhitespaceを追加しようとしました。");
        }

        ignorableWhitespace.setValue(new String(ch, start, length));
        ignorableWhitespace.setCdata(fIsInCdata);
    }

    public void processingInstruction(final String target, final String data)
            throws SAXException {
        // このイベントは現時点での仕様では無視します。
    }

    public void skippedEntity(final String name) throws SAXException {
        // このイベントは現時点での仕様では無視します。
    }

    // ------------------------------------------------
    // ここからは LexicalHandler のためのメソッド。
    // ------------------------------------------------

    public void startDTD(final String name, final String publicId,
            final String systemId) throws SAXException {
        final BlancoXmlDtd dtd = new BlancoXmlDtd();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlDocument) {
            ((BlancoXmlDocument) objCurrent).getChildNodes().add(dtd);
        } else {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: 想定されない型["
                            + objCurrent.getClass().getName()
                            + "]に対して BlancoXmlDtdを追加しようとしました。");
        }
    }

    public void endDTD() throws SAXException {
    }

    public void startEntity(final String name) throws SAXException {
        // 現在の仕様では、サポート外です。
    }

    public void endEntity(final String name) throws SAXException {
        // 現在の仕様では、サポート外です。
    }

    public void startCDATA() throws SAXException {
        // 現在の仕様では、CDATAの入れ子はサポートしません。
        fIsInCdata = true;
    }

    public void endCDATA() throws SAXException {
        // 現在の仕様では、CDATAの入れ子はサポートしません。
        fIsInCdata = false;
    }

    public void comment(final char[] ch, final int start, final int length)
            throws SAXException {
        final BlancoXmlComment comment = new BlancoXmlComment();

        // 現在処理中のオブジェクトを取得します。
        final Object objCurrent = fDocumentElementStack.peek();
        if (objCurrent instanceof BlancoXmlElement) {
            ((BlancoXmlElement) objCurrent).getChildNodes().add(comment);
        } else if (objCurrent instanceof BlancoXmlDocument) {
            ((BlancoXmlDocument) objCurrent).getChildNodes().add(comment);
        } else {
            throw new IllegalArgumentException(
                    "BlancoXmlUnmarshallerContentHandler: 想定されない型["
                            + objCurrent.getClass().getName()
                            + "]に対して BlancoXmlCommentを追加しようとしました。");
        }

        comment.setValue(new String(ch, start, length));
    }
}
