/*
 * @(#)DTD.java	1.20 04/05/05
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package javax.swing.text.html.parser;

import java.io.PrintStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.util.Hashtable;
import java.util.Vector;
import java.util.BitSet;
import java.util.StringTokenizer;
import java.util.Enumeration;
import java.util.Properties;
import java.net.URL;

/**
 * The representation of an SGML DTD.  DTD describes a document
 * syntax and is used in parsing of HTML documents.  It contains
 * a list of elements and their attributes as well as a list of
 * entities defined in the DTD.
 *
 * @see Element
 * @see AttributeList
 * @see ContentModel
 * @see Parser
 * @author Arthur van Hoff
 * @version 1.20 05/05/04
 */
public
class DTD implements DTDConstants {
    public String name;
    public Vector<Element> elements = new Vector<Element>();
    public Hashtable<String,Element> elementHash
	= new Hashtable<String,Element>();
    public Hashtable<Object,Entity> entityHash
	= new Hashtable<Object,Entity>();
    public final Element pcdata = getElement("#pcdata");
    public final Element html = getElement("html");
    public final Element meta = getElement("meta");
    public final Element base = getElement("base");
    public final Element isindex = getElement("isindex");
    public final Element head = getElement("head");
    public final Element body = getElement("body");
    public final Element applet = getElement("applet");
    public final Element param = getElement("param");
    public final Element p = getElement("p");
    public final Element title = getElement("title");
    final Element style = getElement("style");
    final Element link = getElement("link");
    final Element script = getElement("script");

    public static final int FILE_VERSION = 1;

    /**
     * Creates a new DTD with the specified name.
     * @param name the name, as a <code>String</code> of the new DTD
     */
    protected DTD(String name) {
	this.name = name;
	defEntity("#RE", GENERAL, '\r');
	defEntity("#RS", GENERAL, '\n');
	defEntity("#SPACE", GENERAL, ' ');
	defineElement("unknown", EMPTY, false, true, null, null, null, null);
    }

    /**
     * Gets the name of the DTD.
     * @return the name of the DTD
     */
    public String getName() {
	return name;
    }

    /**
     * Gets an entity by name.
     * @return the <code>Entity</code> corresponding to the 
     *   <code>name</code> <code>String</code>
     */
    public Entity getEntity(String name) {
	return (Entity)entityHash.get(name);
    }

    /**
     * Gets a character entity.
     * @return the <code>Entity</code> corresponding to the
     *    <code>ch</code> character
     */
    public Entity getEntity(int ch) {
	return (Entity)entityHash.get(new Integer(ch));
    }

    /**
     * Returns <code>true</code> if the element is part of the DTD,
     * otherwise returns <code>false</code>.
     *
     * @param  name the requested <code>String</code>
     * @return <code>true</code> if <code>name</code> exists as
     *   part of the DTD, otherwise returns <code>false</code>
     */
    boolean elementExists(String name) {
	Element e = (Element)elementHash.get(name);
	return ((e == null) ? false : true);
    }

    /**
     * Gets an element by name. A new element is
     * created if the element doesn't exist.
     *
     * @param name the requested <code>String</code>
     * @return the <code>Element</code> corresponding to
     *   <code>name</code>, which may be newly created
     */
    public Element getElement(String name) {
	Element e = (Element)elementHash.get(name);
	if (e == null) {
	    e = new Element(name, elements.size());
	    elements.addElement(e);
	    elementHash.put(name, e);
	}
	return e;
    }

    /**
     * Gets an element by index.
     *
     * @param index the requested index
     * @return the <code>Element</code> corresponding to
     *   <code>index</code>
     */
    public Element getElement(int index) {
	return (Element)elements.elementAt(index);
    }

    /**
     * Defines an entity.  If the <code>Entity</code> specified
     * by <code>name</code>, <code>type</code>, and <code>data</code>
     * exists, it is returned; otherwise a new <code>Entity</code>
     * is created and is returned.
     *
     * @param name the name of the <code>Entity</code> as a <code>String</code>
     * @param type the type of the <code>Entity</code>
     * @param data the <code>Entity</code>'s data
     * @return the <code>Entity</code> requested or a new <code>Entity</code>
     *   if not found
     */
    public Entity defineEntity(String name, int type, char data[]) {
	Entity ent = (Entity)entityHash.get(name);
	if (ent == null) {
	    ent = new Entity(name, type, data);
	    entityHash.put(name, ent);
	    if (((type & GENERAL) != 0) && (data.length == 1)) {
		switch (type & ~GENERAL) {
		  case CDATA:
		  case SDATA:
		    entityHash.put(new Integer(data[0]), ent);
		    break;
		}
	    }
	}
	return ent;
    }

    /**
     * Returns the <code>Element</code> which matches the
     * specified parameters.  If one doesn't exist, a new
     * one is created and returned.
     *
     * @param name the name of the <code>Element</code>
     * @param type the type of the <code>Element</code>
     * @param omitStart <code>true</code if start should be omitted
     * @param omitEnd  <code>true</code> if end should be omitted
     * @param content  the <code>ContentModel</code>
     * @param atts the <code>AttributeList</code> specifying the 
     *    <code>Element</code>
     * @return the <code>Element</code> specified
     */
    public Element defineElement(String name, int type,
		       boolean omitStart, boolean omitEnd, ContentModel content,
		       BitSet exclusions, BitSet inclusions, AttributeList atts) {
	Element e = getElement(name);
	e.type = type;
	e.oStart = omitStart;
	e.oEnd = omitEnd;
	e.content = content;
	e.exclusions = exclusions;
	e.inclusions = inclusions;
	e.atts = atts;
	return e;
    }

    /**
     * Returns the <code>Element</code> which matches the
     * specified <code>AttributeList</code>. 
     * If one doesn't exist, a new one is created and returned.
     *
     * @param name the name of the <code>Element</code>
     * @param atts the <code>AttributeList</code> specifying the 
     *    <code>Element</code>
     * @return the <code>Element</code> specified
     */
    public void defineAttributes(String name, AttributeList atts) {
	Element e = getElement(name);
	e.atts = atts;
    }

    /**
     * Creates and returns a character <code>Entity</code>.
     * @param name the entity's name
     * @return the new character <code>Entity</code>
     */
    public Entity defEntity(String name, int type, int ch) {
	char data[] = {(char)ch};
	return defineEntity(name, type, data);
    }

    /**
     * Creates and returns an <code>Entity</code>.
     * @param name the entity's name
     * @return the new <code>Entity</code>
     */
    protected Entity defEntity(String name, int type, String str) {
	int len = str.length();
	char data[] = new char[len];
	str.getChars(0, len, data, 0);
	return defineEntity(name, type, data);
    }

    /**
     * Creates and returns an <code>Element</code>.
     * @param name the element's name
     * @return the new <code>Element</code>
     */
    protected Element defElement(String name, int type,
		       boolean omitStart, boolean omitEnd, ContentModel content,
		       String[] exclusions, String[] inclusions, AttributeList atts) {
	BitSet excl = null;
	if (exclusions != null && exclusions.length > 0) {
	    excl = new BitSet();
	    for (int i = 0; i < exclusions.length; i++) {
		String str = exclusions[i];
		if (str.length() > 0) {
		    excl.set(getElement(str).getIndex());
		}
	    }
	}
	BitSet incl = null;
	if (inclusions != null && inclusions.length > 0) {
	    incl = new BitSet();
	    for (int i = 0; i < inclusions.length; i++) {
		String str = inclusions[i];
		if (str.length() > 0) {
		    incl.set(getElement(str).getIndex());
		}
	    }
	}
	return defineElement(name, type, omitStart, omitEnd, content, excl, incl, atts);
    }

    /**
     * Creates and returns an <code>AttributeList</code>.
     * @param name the attribute list's name
     * @return the new <code>AttributeList</code>
     */
    protected AttributeList defAttributeList(String name, int type, int modifier, String value, String values, AttributeList atts) {
	Vector vals = null;
	if (values != null) {
	    vals = new Vector();
	    for (StringTokenizer s = new StringTokenizer(values, "|") ; s.hasMoreTokens() ;) {
		String str = s.nextToken();
		if (str.length() > 0) {
		    vals.addElement(str);
		}
	    }
	}
	return new AttributeList(name, type, modifier, value, vals, atts);
    }

    /**
     * Creates and returns a new content model.
     * @param type the type of the new content model
     * @return the new <code>ContentModel</code>
     */
    protected ContentModel defContentModel(int type, Object obj, ContentModel next) {
	return new ContentModel(type, obj, next);
    }

    /**
     * Returns a string representation of this DTD.
     * @return the string representation of this DTD
     */
    public String toString() {
	return name;
    }

    /**
     * The hashtable of DTDs.
     */
    static Hashtable dtdHash = new Hashtable();

  public static void putDTDHash(String name, DTD dtd) {
    dtdHash.put(name, dtd);
  }
    /**
     * Returns a DTD with the specified <code>name</code>.  If
     * a DTD with that name doesn't exist, one is created
     * and returned.  Any uppercase characters in the name
     * are converted to lowercase.
     *
     * @param name the name of the DTD
     * @return the DTD which corresponds to <code>name</code>
     */
    public static DTD getDTD(String name) throws IOException {
	name = name.toLowerCase();
	DTD dtd = (DTD)dtdHash.get(name);
	if (dtd == null)
	  dtd = new DTD(name);

	return dtd;
    }

    /**
     * Recreates a DTD from an archived format.
     * @param in  the <code>DataInputStream</code> to read from
     */
    public void read(DataInputStream in) throws IOException {
	if (in.readInt() != FILE_VERSION) {
	}

	//
	// Read the list of names
	//
	String[] names = new String[in.readShort()];
	for (int i = 0; i < names.length; i++) {
	    names[i] = in.readUTF();
	}


	//
	// Read the entities
	//
	int num = in.readShort();
	for (int i = 0; i < num; i++) {
	    short nameId = in.readShort();
	    int type = in.readByte();
	    String name = in.readUTF();
	    defEntity(names[nameId], type | GENERAL, name);
	}

	// Read the elements
	//
	num = in.readShort();
	for (int i = 0; i < num; i++) {
	    short nameId = in.readShort();
	    int type = in.readByte();
	    byte flags = in.readByte();
	    ContentModel m = readContentModel(in, names);
	    String[] exclusions = readNameArray(in, names);
	    String[] inclusions = readNameArray(in, names);
	    AttributeList atts = readAttributeList(in, names);
	    defElement(names[nameId], type,
		       ((flags & 0x01) != 0), ((flags & 0x02) != 0),
		       m, exclusions, inclusions, atts);
	}
    }

    private ContentModel readContentModel(DataInputStream in, String[] names)
		throws IOException {
	byte flag = in.readByte();
	switch(flag) {
	    case 0:		// null
		return null;
	    case 1: {		// content_c
		int type = in.readByte();
		ContentModel m = readContentModel(in, names);
		ContentModel next = readContentModel(in, names);
		return defContentModel(type, m, next);
	    }
	    case 2: {		// content_e
		int type = in.readByte();
		Element el = getElement(names[in.readShort()]);
		ContentModel next = readContentModel(in, names);
		return defContentModel(type, el, next);
	    }
	default:
		throw new IOException("bad bdtd");
	}
    }

    private String[] readNameArray(DataInputStream in, String[] names)
		throws IOException {
	int num = in.readShort();
	if (num == 0) {
	    return null;
	}
	String[] result = new String[num];
	for (int i = 0; i < num; i++) {
	    result[i] = names[in.readShort()];
	}
	return result;
    }


    private AttributeList readAttributeList(DataInputStream in, String[] names)
		throws IOException  {
	AttributeList result = null;
	for (int num = in.readByte(); num > 0; --num) {
	    short nameId = in.readShort();
	    int type = in.readByte();
	    int modifier = in.readByte();
	    short valueId = in.readShort();
	    String value = (valueId == -1) ? null : names[valueId];
	    Vector values = null;
	    short numValues = in.readShort();
	    if (numValues > 0) {
		values = new Vector(numValues);
		for (int i = 0; i < numValues; i++) {
		    values.addElement(names[in.readShort()]);
		}
	    }
result = new AttributeList(names[nameId], type, modifier, value,
				       values, result);
	    // We reverse the order of the linked list by doing this, but
	    // that order isn't important.
	}
	return result;
    }

}
