content: /*
  * Copyright (C) 2012 by Xavier GOUCHET (http://xgouchet.fr, android@xgouchet.fr) MIT Licence / Expat
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  * associated documentation files (the "Software"), to deal in the Software without restriction,
  * including without limitation the rights to use, copy, modify, merge, publish, distribute,
  * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
  *
  * The above copyright notice and this permission notice shall be included in all copies or
  * substantial portions of the Software.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
  * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS ( XAVIER GOUCHET ) BE LIABLE FOR ANY CLAIM, DAMAGES
  * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
  
  package fr.xgouchet.axml;
  
  import android.util.Log;
  
  import org.w3c.dom.Document;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.text.DecimalFormat;
  import java.util.HashMap;
  import java.util.Map;
  
  import javax.xml.parsers.ParserConfigurationException;
  
  public class CompressedXmlParser {
  
  	public static final String TAG = "CXP";
  
  	public static final int WORD_START_DOCUMENT = 0x00080003;
  
  	public static final int WORD_STRING_TABLE = 0x001C0001;
  	public static final int WORD_RES_TABLE = 0x00080180;
  
  	public static final int WORD_START_NS = 0x00100100;
  	public static final int WORD_END_NS = 0x00100101;
  	public static final int WORD_START_TAG = 0x00100102;
  	public static final int WORD_END_TAG = 0x00100103;
  	public static final int WORD_TEXT = 0x00100104;
  	public static final int WORD_EOS = 0xFFFFFFFF;
  	public static final int WORD_SIZE = 4;
  
  	private static final int TYPE_ID_REF = 0x01000008;
  	private static final int TYPE_ATTR_REF = 0x02000008;
  	private static final int TYPE_STRING = 0x03000008;
  	private static final int TYPE_DIMEN = 0x05000008;
  	private static final int TYPE_FRACTION = 0x06000008;
  	private static final int TYPE_INT = 0x10000008;
  	private static final int TYPE_FLOAT = 0x04000008;
  
  	private static final int TYPE_FLAGS = 0x11000008;
  	private static final int TYPE_BOOL = 0x12000008;
  	private static final int TYPE_COLOR = 0x1C000008;
  	private static final int TYPE_COLOR2 = 0x1D000008;
  
  	private static final String[] DIMEN = new String[] { "px", "dp", "sp",
  			"pt", "in", "mm" };
  
  	public CompressedXmlParser() {
  		mNamespaces = new HashMap<>();
  	}
  
  	/**
  	 * Parses the xml data in the given file,
  	 * 
  	 * @param source
  	 *            the source file to parse
  	 * @param listener
  	 *            the listener for XML events (must not be null)
  	 * @throws IOException
  	 *             if the input can't be read
  	 */
  	public void parse(final InputStream input,
  			final CompressedXmlParserListener listener) throws IOException {
  
  		if (listener == null) {
  			throw new IllegalArgumentException(
  					"CompressedXmlParser Listener can' be null");
  		}
  		mListener = listener;
  
  		// TODO is.available may not be accurate !!!
  		mData = new byte[input.available()];
  		input.read(mData);
  		input.close();
  
  		// parseCompressedHeader();
  		parseCompressedXml();
  
  	}
  
  	/**
  	 * Parses the xml data in the given file,
  	 * 
  	 * @param source
  	 *            the source file to parse
  	 * @return the DOM document object
  	 * 
  	 * @throws IOException
  	 *             if the input can't be read
  	 * @throws ParserConfigurationException
  	 *             if a DocumentBuilder can't be created
  	 */
  	public Document parseDOM(final InputStream input) throws IOException,
  			ParserConfigurationException {
  		CompressedXmlDomListener dom = new CompressedXmlDomListener();
  
  		parse(input, dom);
  
  		return dom.getDocument();
  	}
  
  	/**
  	 * Each tag starts with a 32 bits word (different for start tag, end tag and
  	 * end doc)
  	 * 
  	 */
  	private void parseCompressedXml() {
  		int word0;
  
  		while (mParserOffset < mData.length) {
  			word0 = getLEWord(mParserOffset);
  			switch (word0) {
  			case WORD_START_DOCUMENT:
  				parseStartDocument();
  				break;
  			case WORD_STRING_TABLE:
  				parseStringTable();
  				break;
  			case WORD_RES_TABLE:
  				parseResourceTable();
  				break;
  			case WORD_START_NS:
  				parseNamespace(true);
  				break;
  			case WORD_END_NS:
  				parseNamespace(false);
  				break;
  			case WORD_START_TAG:
  				parseStartTag();
  				break;
  			case WORD_END_TAG:
  				parseEndTag();
  				break;
  			case WORD_TEXT:
  				parseText();
  				break;
  			case WORD_EOS:
  				mListener.endDocument();
  				break;
  			default:
  				mParserOffset += WORD_SIZE;
  				Log.w(TAG, "Unknown word 0x" + Integer.toHexString(word0)
  						+ " @" + mParserOffset);
  				break;
  			}
  		}
  
  		mListener.endDocument();
  	}
  
  	/**
  	 * A doc starts with the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x00080003</li>
  	 * <li>1st word : chunk size</li>
  	 * </ul>
  	 */
  	private void parseStartDocument() {
  		mListener.startDocument();
  		mParserOffset += (2 * WORD_SIZE);
  	}
  
  	/**
  	 * the string table starts with the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x1c0001</li>
  	 * <li>1st word : chunk size</li>
  	 * <li>2nd word : number of string in the string table</li>
  	 * <li>3rd word : number of styles in the string table</li>
  	 * <li>4th word : ???? (0)</li>
  	 * <li>5th word : Offset to String data</li>
  	 * <li>6th word : Offset to style data</li>
  	 * </ul>
  	 */
  	private void parseStringTable() {
  
  		int chunk = getLEWord(mParserOffset + (1 * WORD_SIZE));
  		mStringsCount = getLEWord(mParserOffset + (2 * WORD_SIZE));
  		mStylesCount = getLEWord(mParserOffset + (3 * WORD_SIZE));
  		int strOffset = mParserOffset
  				+ getLEWord(mParserOffset + (5 * WORD_SIZE));
  		int styleOffset = getLEWord(mParserOffset + (6 * WORD_SIZE));
  
  		mStringsTable = new String[mStringsCount];
  		int offset;
  		for (int i = 0; i < mStringsCount; ++i) {
  			offset = strOffset
  					+ getLEWord(mParserOffset + ((i + 7) * WORD_SIZE));
  			mStringsTable[i] = getStringFromStringTable(offset);
  		}
  
  		if (styleOffset > 0) {
  			Log.w(TAG, "Unread styles");
  			for (int i = 0; i < mStylesCount; ++i) {
  				// TODO read the styles ???
  			}
  		}
  
  		mParserOffset += chunk;
  	}
  
  	/**
  	 * the resource ids table starts with the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x00080180</li>
  	 * <li>1st word : chunk size</li>
  	 * </ul>
  	 */
  	private void parseResourceTable() {
  		int chunk = getLEWord(mParserOffset + (1 * WORD_SIZE));
  		mResCount = (chunk / 4) - 2;
  
  		mResourcesIds = new int[mResCount];
  		for (int i = 0; i < mResCount; ++i) {
  			mResourcesIds[i] = getLEWord(mParserOffset + ((i + 2) * WORD_SIZE));
  		}
  
  		mParserOffset += chunk;
  	}
  
  	/**
  	 * A namespace tag contains the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x00100100 = Start NS / 0x00100101 = end NS</li>
  	 * <li>1st word : chunk size</li>
  	 * <li>2nd word : line this tag appeared</li>
  	 * <li>3rd word : ??? (always 0xFFFFFF)</li>
  	 * <li>4th word : index of namespace prefix in StringIndexTable</li>
  	 * <li>5th word : index of namespace uri in StringIndexTable</li>
  	 * </ul>
  	 */
  	private void parseNamespace(boolean start) {
  		final int prefixIdx = getLEWord(mParserOffset + (4 * WORD_SIZE));
  		final int uriIdx = getLEWord(mParserOffset + (5 * WORD_SIZE));
  
  		final String uri = getString(uriIdx);
  		final String prefix = getString(prefixIdx);
  
  		if (start) {
  			mListener.startPrefixMapping(prefix, uri);
  			mNamespaces.put(uri, prefix);
  		} else {
  			mListener.endPrefixMapping(prefix, uri);
  			mNamespaces.remove(uri);
  		}
  
  		// Offset to first tag
  		mParserOffset += (6 * WORD_SIZE);
  	}
  
  	/**
  	 * A start tag will start with the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x00100102 = Start_Tag</li>
  	 * <li>1st word : chunk size</li>
  	 * <li>2nd word : line this tag appeared in the original file</li>
  	 * <li>3rd word : ??? (always 0xFFFFFF)</li>
  	 * <li>4th word : index of namespace uri in StringIndexTable, or 0xFFFFFFFF
  	 * for default NS</li>
  	 * <li>5th word : index of element name in StringIndexTable</li>
  	 * <li>6th word : ???</li>
  	 * <li>7th word : number of attributes following the start tag</li>
  	 * <li>8th word : ??? (0)</li>
  	 * </ul>
  	 * 
  	 */
  	private void parseStartTag() {
  		// get tag info
  		final int uriIdx = getLEWord(mParserOffset + (4 * WORD_SIZE));
  		final int nameIdx = getLEWord(mParserOffset + (5 * WORD_SIZE));
  		final int attrCount = getLEWord(mParserOffset + (7 * WORD_SIZE));
  
  		final String name = getString(nameIdx);
  		String uri, qname;
  		if (uriIdx == 0xFFFFFFFF) {
  			uri = "";
  			qname = name;
  		} else {
  			uri = getString(uriIdx);
  			if (mNamespaces.containsKey(uri)) {
  				qname = mNamespaces.get(uri) + ':' + name;
  			} else {
  				qname = name;
  			}
  		}
  
  		// offset to start of attributes
  		mParserOffset += (9 * WORD_SIZE);
  
  		final Attribute[] attrs = new Attribute[attrCount]; // NOPMD
  		for (int a = 0; a < attrCount; a++) {
  			attrs[a] = parseAttribute(); // NOPMD
  
  			// offset to next attribute or tag
  			mParserOffset += (5 * 4);
  		}
  
  		mListener.startElement(uri, name, qname, attrs);
  	}
  
  	/**
  	 * An attribute will have the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : index of namespace uri in StringIndexTable, or 0xFFFFFFFF
  	 * for default NS</li>
  	 * <li>1st word : index of attribute name in StringIndexTable</li>
  	 * <li>2nd word : index of attribute value, or 0xFFFFFFFF if value is a
  	 * typed value</li>
  	 * <li>3rd word : value type</li>
  	 * <li>4th word : resource id value</li>
  	 * </ul>
  	 */
  	private Attribute parseAttribute() {
  		final int attrNSIdx = getLEWord(mParserOffset);
  		final int attrNameIdx = getLEWord(mParserOffset + (1 * WORD_SIZE));
  		final int attrValueIdx = getLEWord(mParserOffset + (2 * WORD_SIZE));
  		final int attrType = getLEWord(mParserOffset + (3 * WORD_SIZE));
  		final int attrData = getLEWord(mParserOffset + (4 * WORD_SIZE));
  
  		final Attribute attr = new Attribute();
  		attr.setName(getString(attrNameIdx));
  
  		if (attrNSIdx == 0xFFFFFFFF) {
  			attr.setNamespace(null);
  			attr.setPrefix(null);
  		} else {
  			String uri = getString(attrNSIdx);
  			if (mNamespaces.containsKey(uri)) {
  				attr.setNamespace(uri);
  				attr.setPrefix(mNamespaces.get(uri));
  			}
  		}
  
  		if (attrValueIdx == 0xFFFFFFFF) {
  			attr.setValue(getAttributeValue(attrType, attrData));
  		} else {
  			attr.setValue(getString(attrValueIdx));
  		}
  
  		return attr;
  
  	}
  
  	/**
  	 * A text will start with the following 4bytes word :
  	 * <ul>
  	 * <li>0th word : 0x00100104 = Text</li>
  	 * <li>1st word : chunk size</li>
  	 * <li>2nd word : line this element appeared in the original document</li>
  	 * <li>3rd word : ??? (always 0xFFFFFFFF)</li>
  	 * <li>4rd word : string index in string table</li>
  	 * <li>5rd word : ??? (always 8)</li>
  	 * <li>6rd word : ??? (always 0)</li>
  	 * </ul>
  	 * 
  	 */
  	private void parseText() {
  		// get tag infos
  		final int strIndex = getLEWord(mParserOffset + (4 * WORD_SIZE));
  
  		String data = getString(strIndex);
  		mListener.characterData(data);
  
  		// offset to next node
  		mParserOffset += (7 * WORD_SIZE);
  	}
  
  	/**
  	 * EndTag contains the following 4bytes words :
  	 * <ul>
  	 * <li>0th word : 0x00100103 = End_Tag</li>
  	 * <li>1st word : chunk size</li>
  	 * <li>2nd word : line this tag appeared in the original file</li>
  	 * <li>3rd word : ??? (always 0xFFFFFFFF)</li>
  	 * <li>4th word : index of namespace name in StringIndexTable, or 0xFFFFFFFF
  	 * for default NS</li>
  	 * <li>5th word : index of element name in StringIndexTable</li>
  	 * </ul>
  	 */
  	private void parseEndTag() {
  		// get tag info
  		final int uriIdx = getLEWord(mParserOffset + (4 * WORD_SIZE));
  		final int nameIdx = getLEWord(mParserOffset + (5 * WORD_SIZE));
  
  		final String name = getString(nameIdx);
  		String uri;
  		if (uriIdx == 0xFFFFFFFF) {
  			uri = "";
  		} else {
  			uri = getString(uriIdx);
  		}
  
  		mListener.endElement(uri, name, null);
  
  		// offset to start of next tag
  		mParserOffset += (6 * WORD_SIZE);
  	}
  
  	/**
  	 * @param index
  	 *            the index of the string in the StringIndexTable
  	 * @return the string
  	 */
  	private String getString(final int index) {
  		String res;
  		if ((index >= 0) && (index < mStringsCount)) {
  			res = mStringsTable[index];
  		} else {
  			res = null; // NOPMD
  		}
  
  		return res;
  	}
  
  	/**
  	 * @param offset
  	 *            offset of the beginning of the string inside the StringTable
  	 *            (and not the whole data array)
  	 * @return the String
  	 */
  	private String getStringFromStringTable(final int offset) {
  		int strLength;
  		byte chars[];
  		if (mData[offset + 1] == mData[offset]) {
  			strLength = mData[offset];
  			chars = new byte[strLength];// NOPMD
  			for (int i = 0; i < strLength; i++) {
  				chars[i] = mData[offset + 2 + i]; // NOPMD
  			}
  		} else {
  
  			strLength = ((mData[offset + 1] << 8) & 0xFF00)
  					| (mData[offset] & 0xFF);
  			chars = new byte[strLength]; // NOPMD
  			for (int i = 0; i < strLength; i++) {
  				chars[i] = mData[offset + 2 + (i * 2)]; // NOPMD
  			}
  
  		}
  		return new String(chars);
  	}
  
  	/**
  	 * @param arr
  	 *            the byte array to read
  	 * @param off
  	 *            the offset of the word to read
  	 * @return value of a Little Endian 32 bit word from the byte arrayat offset
  	 *         off.
  	 */
  	private int getLEWord(final int off) {
  		return ((mData[off + 3] << 24) & 0xff000000)
  				| ((mData[off + 2] << 16) & 0x00ff0000)
  				| ((mData[off + 1] << 8) & 0x0000ff00)
  				| ((mData[off + 0] << 0) & 0x000000ff);
  	}
  
  	/**
  	 * @param word
  	 *            a word read in an attribute data
  	 * @return the typed value
  	 */
  	private String getAttributeValue(final int type, final int data) {
  		String res;
  
  		switch (type) {
  		case TYPE_STRING:
  			res = getString(data);
  			break;
  		case TYPE_DIMEN:
  			res = Integer.toString(data >> 8) + DIMEN[data & 0xFF];
  			break;
  		case TYPE_FRACTION:
  			double fracValue = (((double) data) / ((double) 0x7FFFFFFF));
  			// res = String.format("%.2f%%", fracValue);
  			res = new DecimalFormat("#.##%").format(fracValue);
  			break;
  		case TYPE_FLOAT:
  			res = Float.toString(Float.intBitsToFloat(data));
  			break;
  		case TYPE_INT:
  		case TYPE_FLAGS:
  			res = Integer.toString(data);
  			break;
  		case TYPE_BOOL:
  			res = Boolean.toString(data != 0);
  			break;
  		case TYPE_COLOR:
  		case TYPE_COLOR2:
  			res = String.format("#%08X", data);
  			break;
  		case TYPE_ID_REF:
  			res = String.format("@id/0x%08X", data);
  			break;
  		case TYPE_ATTR_REF:
  			res = String.format("?id/0x%08X", data);
  			break;
  		default:
  			Log.w(TAG, "(type=" + Integer.toHexString(type) + ") : " + data
  					+ " (0x" + Integer.toHexString(data) + ") @"
  					+ mParserOffset);
  			res = String.format("%08X/0x%08X", type, data);
  			break;
  		}
  
  		return res;
  	}
  
  	// Data
  	private CompressedXmlParserListener mListener;
  
  	// Internal
  	private Map<String, String> mNamespaces;
  	private byte[] mData;
  
  	private String[] mStringsTable;
  	private int[] mResourcesIds;
  	private int mStringsCount, mStylesCount, mResCount;
  	private int mParserOffset;
  
  }
  