/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.util;

import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;

/**
 * This is a quick and dirty implementation of XmlSerializer that isn't horribly
 * painfully slow like the normal one.  It only does what is needed for the
 * specific XML files being written with it.
 */
public class FastXmlSerializer implements XmlSerializer {
    private static final String ESCAPE_TABLE[] = new String[] {
        "&#0;",   "&#1;",   "&#2;",   "&#3;",  "&#4;",    "&#5;",   "&#6;",  "&#7;",  // 0-7
        "&#8;",   "&#9;",   "&#10;",  "&#11;", "&#12;",   "&#13;",  "&#14;", "&#15;", // 8-15
        "&#16;",  "&#17;",  "&#18;",  "&#19;", "&#20;",   "&#21;",  "&#22;", "&#23;", // 16-23
        "&#24;",  "&#25;",  "&#26;",  "&#27;", "&#28;",   "&#29;",  "&#30;", "&#31;", // 24-31
        null,     null,     "&quot;", null,     null,     null,     "&amp;",  null,   // 32-39
        null,     null,     null,     null,     null,     null,     null,     null,   // 40-47
        null,     null,     null,     null,     null,     null,     null,     null,   // 48-55
        null,     null,     null,     null,     "&lt;",   null,     "&gt;",   null,   // 56-63
    };

    private static final int DEFAULT_BUFFER_LEN = 32*1024;

    private static String sSpace = "                                                              ";

    private final int mBufferLen;
    private final char[] mText;
    private int mPos;

    private Writer mWriter;

    private OutputStream mOutputStream;
    private CharsetEncoder mCharset;
    private ByteBuffer mBytes;

    private boolean mIndent = false;
    private boolean mInTag;

    private int mNesting = 0;
    private boolean mLineStart = true;

    public FastXmlSerializer() {
        this(DEFAULT_BUFFER_LEN);
    }

    /**
     * Allocate a FastXmlSerializer with the given internal output buffer size.  If the
     * size is zero or negative, then the default buffer size will be used.
     *
     * @param bufferSize Size in bytes of the in-memory output buffer that the writer will use.
     */
    public FastXmlSerializer(int bufferSize) {
        mBufferLen = (bufferSize > 0) ? bufferSize : DEFAULT_BUFFER_LEN;
        mText = new char[mBufferLen];
        mBytes = ByteBuffer.allocate(mBufferLen);
    }

    private void append(char c) throws IOException {
        int pos = mPos;
        if (pos >= (mBufferLen-1)) {
            flush();
            pos = mPos;
        }
        mText[pos] = c;
        mPos = pos+1;
    }

    private void append(String str, int i, final int length) throws IOException {
        if (length > mBufferLen) {
            final int end = i + length;
            while (i < end) {
                int next = i + mBufferLen;
                append(str, i, next<end ? mBufferLen : (end-i));
                i = next;
            }
            return;
        }
        int pos = mPos;
        if ((pos+length) > mBufferLen) {
            flush();
            pos = mPos;
        }
        str.getChars(i, i+length, mText, pos);
        mPos = pos + length;
    }

    private void append(char[] buf, int i, final int length) throws IOException {
        if (length > mBufferLen) {
            final int end = i + length;
            while (i < end) {
                int next = i + mBufferLen;
                append(buf, i, next<end ? mBufferLen : (end-i));
                i = next;
            }
            return;
        }
        int pos = mPos;
        if ((pos+length) > mBufferLen) {
            flush();
            pos = mPos;
        }
        System.arraycopy(buf, i, mText, pos, length);
        mPos = pos + length;
    }

    private void append(String str) throws IOException {
        append(str, 0, str.length());
    }

    private void appendIndent(int indent) throws IOException {
        indent *= 4;
        if (indent > sSpace.length()) {
            indent = sSpace.length();
        }
        append(sSpace, 0, indent);
    }

    private void escapeAndAppendString(final String string) throws IOException {
        final int N = string.length();
        final char NE = (char)ESCAPE_TABLE.length;
        final String[] escapes = ESCAPE_TABLE;
        int lastPos = 0;
        int pos;
        for (pos=0; pos<N; pos++) {
            char c = string.charAt(pos);
            if (c >= NE) continue;
            String escape = escapes[c];
            if (escape == null) continue;
            if (lastPos < pos) append(string, lastPos, pos-lastPos);
            lastPos = pos + 1;
            append(escape);
        }
        if (lastPos < pos) append(string, lastPos, pos-lastPos);
    }

    private void escapeAndAppendString(char[] buf, int start, int len) throws IOException {
        final char NE = (char)ESCAPE_TABLE.length;
        final String[] escapes = ESCAPE_TABLE;
        int end = start+len;
        int lastPos = start;
        int pos;
        for (pos=start; pos<end; pos++) {
            char c = buf[pos];
            if (c >= NE) continue;
            String escape = escapes[c];
            if (escape == null) continue;
            if (lastPos < pos) append(buf, lastPos, pos-lastPos);
            lastPos = pos + 1;
            append(escape);
        }
        if (lastPos < pos) append(buf, lastPos, pos-lastPos);
    }

    public XmlSerializer attribute(String namespace, String name, String value) throws IOException,
            IllegalArgumentException, IllegalStateException {
        append(' ');
        if (namespace != null) {
            append(namespace);
            append(':');
        }
        append(name);
        append("=\"");

        escapeAndAppendString(value);
        append('"');
        mLineStart = false;
        return this;
    }

    public void cdsect(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void comment(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void docdecl(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException {
        flush();
    }

    public XmlSerializer endTag(String namespace, String name) throws IOException,
            IllegalArgumentException, IllegalStateException {
        mNesting--;
        if (mInTag) {
            append(" />\n");
        } else {
            if (mIndent && mLineStart) {
                appendIndent(mNesting);
            }
            append("</");
            if (namespace != null) {
                append(namespace);
                append(':');
            }
            append(name);
            append(">\n");
        }
        mLineStart = true;
        mInTag = false;
        return this;
    }

    public void entityRef(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    private void flushBytes() throws IOException {
        int position;
        if ((position = mBytes.position()) > 0) {
            mBytes.flip();
            mOutputStream.write(mBytes.array(), 0, position);
            mBytes.clear();
        }
    }

    public void flush() throws IOException {
        //Log.i("PackageManager", "flush mPos=" + mPos);
        if (mPos > 0) {
            if (mOutputStream != null) {
                CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos);
                CoderResult result = mCharset.encode(charBuffer, mBytes, true);
                while (true) {
                    if (result.isError()) {
                        throw new IOException(result.toString());
                    } else if (result.isOverflow()) {
                        flushBytes();
                        result = mCharset.encode(charBuffer, mBytes, true);
                        continue;
                    }
                    break;
                }
                flushBytes();
                mOutputStream.flush();
            } else {
                mWriter.write(mText, 0, mPos);
                mWriter.flush();
            }
            mPos = 0;
        }
    }

    public int getDepth() {
        throw new UnsupportedOperationException();
    }

    public boolean getFeature(String name) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getNamespace() {
        throw new UnsupportedOperationException();
    }

    public String getPrefix(String namespace, boolean generatePrefix)
            throws IllegalArgumentException {
        throw new UnsupportedOperationException();
    }

    public Object getProperty(String name) {
        throw new UnsupportedOperationException();
    }

    public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void processingInstruction(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void setFeature(String name, boolean state) throws IllegalArgumentException,
            IllegalStateException {
        if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) {
            mIndent = true;
            return;
        }
        throw new UnsupportedOperationException();
    }

    public void setOutput(OutputStream os, String encoding) throws IOException,
            IllegalArgumentException, IllegalStateException {
        if (os == null)
            throw new IllegalArgumentException();
        if (true) {
            try {
                mCharset = Charset.forName(encoding).newEncoder()
                        .onMalformedInput(CodingErrorAction.REPLACE)
                        .onUnmappableCharacter(CodingErrorAction.REPLACE);
            } catch (IllegalCharsetNameException e) {
                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
                        encoding).initCause(e));
            } catch (UnsupportedCharsetException e) {
                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
                        encoding).initCause(e));
            }
            mOutputStream = os;
        } else {
            setOutput(
                encoding == null
                    ? new OutputStreamWriter(os)
                    : new OutputStreamWriter(os, encoding));
        }
    }

    public void setOutput(Writer writer) throws IOException, IllegalArgumentException,
            IllegalStateException {
        mWriter = writer;
    }

    public void setPrefix(String prefix, String namespace) throws IOException,
            IllegalArgumentException, IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void setProperty(String name, Object value) throws IllegalArgumentException,
            IllegalStateException {
        throw new UnsupportedOperationException();
    }

    public void startDocument(String encoding, Boolean standalone) throws IOException,
            IllegalArgumentException, IllegalStateException {
        append("<?xml version='1.0' encoding='utf-8' standalone='"
                + (standalone ? "yes" : "no") + "' ?>\n");
        mLineStart = true;
    }

    public XmlSerializer startTag(String namespace, String name) throws IOException,
            IllegalArgumentException, IllegalStateException {
        if (mInTag) {
            append(">\n");
        }
        if (mIndent) {
            appendIndent(mNesting);
        }
        mNesting++;
        append('<');
        if (namespace != null) {
            append(namespace);
            append(':');
        }
        append(name);
        mInTag = true;
        mLineStart = false;
        return this;
    }

    public XmlSerializer text(char[] buf, int start, int len) throws IOException,
            IllegalArgumentException, IllegalStateException {
        if (mInTag) {
            append(">");
            mInTag = false;
        }
        escapeAndAppendString(buf, start, len);
        if (mIndent) {
            mLineStart = buf[start+len-1] == '\n';
        }
        return this;
    }

    public XmlSerializer text(String text) throws IOException, IllegalArgumentException,
            IllegalStateException {
        if (mInTag) {
            append(">");
            mInTag = false;
        }
        escapeAndAppendString(text);
        if (mIndent) {
            mLineStart = text.length() > 0 && (text.charAt(text.length()-1) == '\n');
        }
        return this;
    }

}
