/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * 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.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * $Id$
 */

package net.noderunner.exml;

import java.io.IOException;
import java.io.Reader;
import java.util.StringTokenizer;

/**
 * Contains an XML Entity and its attributes.
 * Also, contains logic for resolving and evaluating an entity.
 */
public class Entity {
	private String value;
	private boolean resolved;
	private boolean resolving;
	private String publicID;
	private String systemID;

	/**
	 * Constructs an Entity with a string value.
	 */
	public Entity(String value) {
		this.value = value;
	}

	/**
	 * Constructs an unresolved entity.
	 */
	public Entity(String publicID, String systemID) {
		this.publicID = publicID;
		this.systemID = systemID;
	}

	private StringBuffer evaluate(Reader r) 
		throws IOException
	{
		StringBuffer sb = new StringBuffer(64);
		int len = 0;
		char cbuf[] = new char[256];
		do {
			len = r.read(cbuf, 0, cbuf.length);
			if (len > 0)
				sb.append(cbuf, 0, len);
		} while (len > 0);
		return sb;
	}

	/**
	 * Returns a character for the given built-in entity,
	 * or -1 if it isn't built-in.
	 */
	private static int builtin(String entityName) {
		if (entityName.equals("lt"))
			return '<';
		if (entityName.equals("gt"))
			return '>';
		if (entityName.equals("quot"))
			return '"';
		if (entityName.equals("apos"))
			return '\'';
		if (entityName.equals("amp"))
			return '&';
		return -1;
	}

	/**
	 * Resolves all entities in the given string.
	 * Returns the result.
	 */
	private static String doResolve(String s, XmlReader reader, boolean all)
		throws XmlException, IOException
	{
		StringTokenizer st = new StringTokenizer(s, "&;", true);
		if (st.countTokens() == 1) // no &'s here
			return s;
		StringBuffer sb = new StringBuffer(s.length());
		while (st.hasMoreTokens()) {
			String token = st.nextToken();
			if (token.equals("&")) {
				if (!st.hasMoreTokens())
					throw new XmlException("Expected name in entity " + s);
				String entityName = st.nextToken();
				if (!st.hasMoreTokens())
					throw new XmlException("Expected ; in entity " + s);
				st.nextToken();
				int c = builtin(entityName);
				if (c > 0) {
					if (all)
						sb.append(c);
					else
						sb.append('&').append(entityName).append(';');
				} else {
					Entity entity = reader.getDtd().getEntity(entityName);
					if (entity == null)
						throw new XmlException("Entity not found " + entityName);
					sb.append(entity.resolve(reader));
				}
			} else {
				sb.append(token);
			}
		}
		return sb.toString();
	}

	/**
	 * Returns the resolved entity value with escaped characters.
	 * Returns immediately if the entity has already been resolved.
	 * If it hasn't been yet resolved, uses the resolver from the
	 * given {@link XmlReader}.
	 *
	 * @see XmlReader#getResolver
	 */
	private String resolve(XmlReader reader)
		throws XmlException, IOException
	{
		if (resolved)
			return value;
		if (value == null) {
			if (systemID == null)
				throw new XmlException("SystemID not set, could not resolve entity");
			Reader r = reader.getResolver().resolve(systemID);
			value = evaluate(r).toString();
			return value;
		}
		if (resolving)
			throw new XmlException("Circular entity evaluation for " + this);
		resolving = true;
		value = doResolve(value, reader, false);
		resolving = false;
		resolved = true;
		return value;
	}

	/**
	 * Returns true if this Entity is external.
	 * An entity is external if it has a system ID.
	 */
	public boolean isExternal() {
		return systemID != null;
	}

	/**
	 * Returns the resolved entity value.
	 * Returns immediately if the entity has already been resolved.
	 * If it hasn't been yet resolved, uses the resolver from the
	 * given {@link XmlReader}.
	 *
	 * @see XmlReader#getResolver
	 */
	public String resolveAll(XmlReader reader)
		throws XmlException, IOException
	{
		if (!resolved)
			resolve(reader);
		return value;
	}

	/**
	 * Returns the resolved value, if it exists.
	 * Returns null if it has not yet been resolved.
	 * @see #resolveAll
	 */
	public String getValue() {
		return value;
	}

	/** 
	 * Sets the resolved value.
	 */
	public void setValue(String value) { 
		this.value = value; 
	}

	/**
	 * Returns the system ID.
	 */
	public String getSystemID() {
		return systemID;
	}

	/**
	 * Returns the optional public ID.
	 */
	public String getPublicID() {
		return publicID;
	}

	/**
	 * Returns true if this Entity is being resolved, internally or
	 * externally.
	 */
	boolean isResolving() {
		return resolving; 
	}

	/**
	 * Sets if this Entity is being resolved, internally or externally.
	 */
	void setResolving(boolean resolving) { 
		this.resolving = resolving;
	}

	/**
	 * Returns a string representation of this object.
	 */
	@Override
	public String toString() {
		return "Entity value=" + value + 
			" systemID=" + systemID + 
			" publicID=" + publicID;
	}
}
