/*******************************************************************************
 * Copyright (c) 2002, 2006 Object Factory Inc.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *		Object Factory Inc. - Initial implementation
 *      IBM Corporation - Fix for bug 40951
 *******************************************************************************/
package org.eclipse.ant.internal.ui.dtd.schema;

import com.ibm.icu.text.MessageFormat;
import java.util.HashSet;
import java.util.LinkedList;

import org.eclipse.ant.internal.ui.dtd.IModel;
import org.eclipse.ant.internal.ui.dtd.ISchema;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DeclHandler;

/**
 * SchemaFactory is a SAX DeclHandler that converts DTD ELEMENT and ATTLIST
 * declarations to schema form on the fly. The only two methods available to
 * external users of SchemaFactory are its constructor and
 * <code>getSchema()</code>. The latter returns the schema built by this process
 * and should not be called until the XML parser to which this handler is
 * attached has finished parsing.
 * @author Bob Foster
 */
public class SchemaFactory implements DeclHandler {
	// used for parsing models
	private char[] fBuf;
	private int fLen;
	private int fPos;
	private Element fElement;
	
	private Schema fSchema;
	private static HashSet fTypes = new HashSet();
	private Exception fErrorException;
	static {
		fTypes.add("CDATA"); //$NON-NLS-1$
		fTypes.add("ID"); //$NON-NLS-1$
		fTypes.add("IDREF"); //$NON-NLS-1$
		fTypes.add("IDREFS"); //$NON-NLS-1$
		fTypes.add("NMTOKEN"); //$NON-NLS-1$
		fTypes.add("NMTOKENS"); //$NON-NLS-1$
		fTypes.add("ENTITY"); //$NON-NLS-1$
		fTypes.add("ENTITIES"); //$NON-NLS-1$
	}
	
	/**
	 * Constructor.
	 */
	public SchemaFactory() {
		fSchema = new Schema();
	}
	
	/**
	 * @return ISchema produced from the DeclHandler. The schema is always
	 * correct, though it may be incomplete if the parse was interrupted due to
	 * validation or well-formed errors.
	 */
	public ISchema getSchema() {
		fSchema.setErrorException(fErrorException);
		return fSchema;
	}
	
	/**
	 * @see org.xml.sax.ext.DeclHandler#attributeDecl(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
	 */
	public void attributeDecl(String eName, String aName, String type, String valueDefault, String value) {
		Element element = getElement(eName);
		Attribute attr = (Attribute) element.getAttributes().get(aName);
		if (attr == null) {
			attr = new Attribute(aName, element);
			element.addAttribute(attr);
			
			String[] enumeration = null;
			if (fTypes.contains(type))
				attr.setType(type);
			else if (type.startsWith("NOTATION")) //$NON-NLS-1$
				enumeration = parseValues(type.substring("NOTATION".length()+1), ','); //$NON-NLS-1$
			else {
			    type = stripSurroundingParentheses(type);
				enumeration = parseValues(type, '|');
			}
			attr.setEnum(enumeration);
			
			attr.setRequired(valueDefault == null || !valueDefault.equals("#IMPLIED")); //$NON-NLS-1$
			attr.setFixed(valueDefault != null && valueDefault.equals("#FIXED")); //$NON-NLS-1$
			attr.setDefault(value);
		}
	}
	

    /**
     * Strips the surrounding parentheses from <code>aString</code>.
     * <P>
     * i.e.: (true|false) -> true|false
     */
    private String stripSurroundingParentheses(String aString) {
        if(aString.startsWith("(")) { //$NON-NLS-1$
            aString = aString.substring(1);
        }
        if(aString.endsWith(")")) { //$NON-NLS-1$
            aString = aString.substring(0, aString.length()-1);
        }
        return aString;
    }
	
	
	/**
	 * @param eName Element name
	 * @return Element from schema or new element. Either way
	 * the element is in the schema upon return.
	 */
	private Element getElement(String eName) {
		Element element = (Element) fSchema.getElement(eName);
		if (element == null) {
			element = new Element(eName);
			fSchema.addElement(element); 
		}
		return element;
	}

	private String[] parseValues(String type, char separator) {
		int start = 0, pos, len = type.length();
		LinkedList values = new LinkedList();
		while (start < len) {
			pos = type.indexOf(separator, start);
			if (pos < 0) pos = len;
			String term = type.substring(start, pos);
			start = pos + 1;
			values.add(term);
		}
		return (String[]) values.toArray(new String[values.size()]);
	}

	/**
	 * @see org.xml.sax.ext.DeclHandler#elementDecl(java.lang.String, java.lang.String)
	 */
	public void elementDecl(String name, String model) throws SAXException {
		Element element = getElement(name);
		if (!element.isUndefined()) {
			// if the element has already been defined, this is an error
			throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Doubly_defined, new String[]{name}));
		}
		
		fElement = element;
		if (model.equals("ANY")) { //$NON-NLS-1$
			element.setAny(true);
		} else if (model.equals("EMPTY")) { //$NON-NLS-1$
			element.setEmpty(true);
		} else if (model.equals("(#PCDATA)")) {//$NON-NLS-1$
			element.setText(true);
		} else {
			element.setContentModel(parseModel(model));
		}
	}

	/**
	 * Convert model string to IModel. The <code>fElement</code>
	 * variable is an implicit argument to this method, and it
	 * sets <code>fBuf</code>, <code>fPos</code> and <code>fLen</code> for use
	 * by other parser methods.
	 * @param model String from DTD, with parameter entities replaced.
	 * @return IModel
	 * @throws SAXException if syntax error detected in model. This is a
	 * validation error. Since the DTD is usually not read unless the parser is
	 * validating, we may not ever be handed a bad content model, but we need to
	 * check them, just the same.
	 */
	private IModel parseModel(String model) throws SAXException {
		fBuf = model.toCharArray();
		fLen = fBuf.length;
		if (fBuf[0] != '(') {
			throw new SAXException(
				MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Start_with_left_parenthesis, new String[]{fElement.getName()}));
		}

		boolean ortext = model.startsWith("(#PCDATA|"); //$NON-NLS-1$
		if (ortext) {
			fPos = 8; //"(#PCDATA".length()			
		} else {
			fPos = 0;
		}
		IModel emodel= scanExpr();
		return emodel;
	}

	/**
	 * Scan a parenthesized expression starting
	 * from the left parenthesis or leftmost operator.
	 * @return IModel
	 */
	private IModel scanExpr() throws SAXException {
		// skip opening ( or |
		fPos++;
		return scanExpr(scanElement());
	}
	
	/**
	 * Scan a parenthesized expression with the
	 * first term in hand.
	 * @param term The first operand in the expression, pre-scanned.
	 * @return IModel
	 * @throws SAXException if errors are detected in the model.
	 */
	private IModel scanExpr(IModel term) throws SAXException {
		checkLen();
		if (fBuf[fPos] != ')') {
			char op = fBuf[fPos];
			if (op != '|' && op != ',') {
				throw new SAXException(
					MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Expecting_operator_or_right_parenthesis,
						new String[]{fElement.getName(),String.valueOf(fBuf)}));
			}
			Model model = new Model(op == '|' ? IModel.CHOICE : IModel.SEQUENCE);
			model.addModel(term);
			term = model;
			
			while (fBuf[fPos] == op) {
				fPos++;
				IModel next = scanElement();
				model.addModel(next);
			}
			if (fBuf[fPos] != ')') {
				throw new SAXException(
						MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Expecting_operator_or_right_parenthesis,
						new String[]{fElement.getName(), String.valueOf(fBuf)}));					
			}
			fPos++;
		}
		return term;
	}

	/**
	 * Scan an element name or a parenthesized sub-expression.
	 * @return IModel
	 * @throws SAXException
	 */
	private IModel scanElement() throws SAXException {
		checkLen();
		if (fBuf[fPos] == '(')
			return scanExpr();
		StringBuffer sb = new StringBuffer();
		while (fBuf[fPos] != '|' && fBuf[fPos] != ',' && fBuf[fPos] != ')'
		&& fBuf[fPos] != '*' && fBuf[fPos] != '+' && fBuf[fPos] != '?' ) {
			sb.append(fBuf[fPos++]);
			checkLen();
		}
		String name = sb.toString();
		Element element = getElement(name);
		Model model = new Model(IModel.LEAF);
		model.setLeaf(element);
		return model;
	}

	private void checkLen() throws SAXException {
		if (fPos == fLen) {
			throw new SAXException(
				MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Unexpected_end,
					new String[]{fElement.getName(),
						String.valueOf(fBuf)}));
		}	
	}

	/**
	 * @see org.xml.sax.ext.DeclHandler#externalEntityDecl(java.lang.String, java.lang.String, java.lang.String)
	 */
	public void externalEntityDecl(String name, String publicId, String systemId) {
	}

	/**
	 * @see org.xml.sax.ext.DeclHandler#internalEntityDecl(java.lang.String, java.lang.String)
	 */
	public void internalEntityDecl(String name, String value) {
	}

	public void setErrorException(Exception e) {
		fErrorException = e;
	}
}
