/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.common.meta.impl;

import org.xml.sax.ContentHandler;
import java.io.*;
import java.util.*;
import java.net.*;

import org.w3c.dom.*;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import org.apache.xerces.xni.XMLResourceIdentifier;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.jboss.tools.common.CommonPlugin;
import org.jboss.tools.common.model.plugin.ModelPlugin;
import org.jboss.tools.common.model.util.*;
import org.jboss.tools.common.xml.SAXValidator;
import org.jboss.tools.common.xml.XMLEntityResolver;
import org.jboss.tools.common.xml.XMLEntityResolverImpl;
import org.jboss.tools.common.xml.XMLUtilities;

public class MetaLibLoader {
	public static String DOC_PUBLICID = "-//Red Hat, Inc.//DTD Meta 1.0//EN"; //$NON-NLS-1$
	
	public static boolean validateMetaXML = false;

    static {
        try {
            Class<?> c = MetaLibLoader.class;
            XMLEntityResolver.registerPublicEntity(DOC_PUBLICID, c, "/meta/meta.dtd"); //$NON-NLS-1$
        } catch (IOException e) {
        	ModelPlugin.getPluginLog().logError(e);
        }
    }
    
	
	private XModelMetaDataImpl meta = null;
    private HashSet<String> metas = new HashSet<String>();
    private ArrayList<ModuleRef> metarefs = new ArrayList<ModuleRef>();
    
    private Element root = XMLUtilities.createDocumentElement("meta"); //$NON-NLS-1$

    public MetaLibLoader() {}

    public void load(XModelMetaDataImpl meta) {
        this.meta = meta;
		Map<String,URL> resources = MetaResourceLoader.getMetaResources();
		Iterator<String> it = resources.keySet().iterator();
		while(it.hasNext()) {
			String path = it.next();
			URL url = resources.get(path);
			load(path, url);
		}

        for (int i = 0; i < metarefs.size(); i++) {
            ModuleRef r = metarefs.get(i);
            load(r.element, r.name, r.info);
        }
    }

    void sift(Set modules) {
        boolean b = true;
        while(b) {
            b = false;
            for (int i = metarefs.size() - 1; i >= 0; i--) {
                ModuleRef r = metarefs.get(i);
                if(r.acceptable(modules)) continue;
                b = true;
                metarefs.remove(i);
            }
        }
    }
    
    void load(String name, URL url) {
    	if(url == null) return;
//		long t = System.currentTimeMillis();
		InputStream stream1 = null;
		InputStream stream2 = null;
		try {
			stream1 = url.openStream();
			stream2 = new BufferedInputStream(stream1, 16384);
		
			Element g = parse(stream2);
			if(g == null) {
				ModelPlugin.getPluginLog().logError("SAX parser failed to load meta resource " + name);
				stream1 = url.openStream();
				stream2 = new BufferedInputStream(stream1, 16384);
				g = XMLUtilities.getElement(stream2, new EmptyResolver());
			}
			if(g == null) {
				ModelPlugin.getPluginLog().logInfo("Corrupted meta resource " + name); //$NON-NLS-1$
			} else {
				load0(g, name, url.toString());
			}
		
		} catch (IOException e) {
			ModelPlugin.getPluginLog().logError("MetaLoader: Cannot read resource " + url.toString()); //$NON-NLS-1$
			return;
		} finally {
			if(stream2!=null) {
				try {
					stream2.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
		
		if(validateMetaXML) {
			InputStream stream3 = null;
			try {
				stream3 = url.openStream();
				InputSource is = new InputSource(stream3);
				String[] errors = XMLUtil.getXMLErrors(is, true);
				if(errors != null && errors.length > 0) {
					ModelPlugin.getPluginLog().logInfo("Errors in " + name); //$NON-NLS-1$
					for (int i = 0; i < errors.length && i < 5; i++) {
						ModelPlugin.getPluginLog().logInfo(errors[i]);
					}
				}
			} catch (IOException e) {
				ModelPlugin.getPluginLog().logError(e);
			}finally {
				if(stream3!=null) {
					try {
						stream3.close();
					} catch (IOException e) {
						// ignore
					}
				}
			}
		}
		
    }
    
    Element parse(InputStream stream) {
		Parser p = new Parser();
		p.documentElement = root;
		p.current = root;
		p.parse(stream);
		Element g = p.documentElement;
		g = XMLUtilities.getUniqueChild(g, "XModelEntityGroup"); //$NON-NLS-1$
		if(g != null) {
			p.documentElement.removeChild(g);
		}
		return g;
    }

    void load0(Element g, String name, String source) {
        metarefs.add(new ModuleRef(g, name, source));
    }

    void load(Element g, String name, String source) {
        source = source.substring(0, source.length() - name.length());
        if(metas.contains(name)) {
//            ModelPlugin.log("Can't load module " + name + " second time from " + source);
        } else if(g == null) {
            //ModelPlugin.log("Can't load module " + name + " from " + source);
        } else {
            XMetaDataLoader.loadEntityGroup(meta, g);
            metas.add(name);
            //ModelPlugin.log("Module " + name + " loaded from " + source);
            meta.getLoadedModules().put(name, source);
        }
    }

}

class ModuleRef {
    Element element;
    String name;
    String info;
    String key = ""; //$NON-NLS-1$
    ArrayList<String> depends = new ArrayList<String>();

    public ModuleRef(Element e, String name, String info) {
        this.element = e;
        Element v = XMLUtil.getUniqueChild(e, "VERSION"); //$NON-NLS-1$
        this.name = name;
        this.info = info;
        if(v == null) return;
        if(v.hasAttribute("MODULE") && v.hasAttribute("VERSION")) { //$NON-NLS-1$ //$NON-NLS-2$
            key =  v.getAttribute("MODULE") + ":" + v.getAttribute("VERSION"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }
        if(v.hasAttribute("DEPENDS")) { //$NON-NLS-1$
            String s = v.getAttribute("DEPENDS"); //$NON-NLS-1$
            StringTokenizer st = new StringTokenizer(s, ","); //$NON-NLS-1$
            while(st.hasMoreTokens()) depends.add(st.nextToken());
        }
    }

    public boolean acceptable(Set modules) {
        return (true || key.endsWith("1.0")); //$NON-NLS-1$
    }

}

class Parser implements ContentHandler {
	protected static final String FATAL_ERROR_PROCESSING_FEATURE_ID 	= "http://apache.org/xml/features/continue-after-fatal-error"; //$NON-NLS-1$
	protected static final String ENTITY_RESOLVER_PROPERTY_ID 			= "http://apache.org/xml/properties/internal/entity-resolver"; //$NON-NLS-1$
	protected static final String NAMESPACES_FEATURE_ID 				= "http://xml.org/sax/features/namespaces"; //$NON-NLS-1$
    protected static final String NAMESPACE_PREFIXES_FEATURE_ID 		= "http://xml.org/sax/features/namespace-prefixes"; //$NON-NLS-1$
    protected static final String VALIDATION_FEATURE_ID 				= "http://xml.org/sax/features/validation"; //$NON-NLS-1$
    protected static final String VALIDATION_SCHEMA_FEATURE_ID 			= "http://apache.org/xml/features/validation/schema"; //$NON-NLS-1$
    protected static final String VALIDATION_SCHEMA_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking"; //$NON-NLS-1$
    protected static final String VALIDATION_DYNAMIC_FEATURE_ID 		= "http://apache.org/xml/features/validation/dynamic"; //$NON-NLS-1$
    
    protected static final String DEFAULT_SAX_PARSER_CLASS_NAME 		= "org.apache.xerces.parsers.SAXParser"; //$NON-NLS-1$

    Element documentElement = null;
    Element current = null;

    public void parse(InputStream is) {
    	parse(new InputSource(is));
    }

    public void parse(org.xml.sax.InputSource is) {
    	XMLReader parser = createParser();
    	if(parser == null) return;
    	try {
    		parser.parse(is);
    	} catch (SAXException e) {
			ModelPlugin.getPluginLog().logError(e);
    	} catch (IOException e) {
			ModelPlugin.getPluginLog().logError(e);
    	}
	}
   
    XMLReader newInstance() throws SAXException {
    	try {
    	    return (XMLReader) ModelPlugin.getDefault().getBundle().loadClass(DEFAULT_SAX_PARSER_CLASS_NAME).newInstance();
    	} catch (ClassNotFoundException e1) {
    	    throw new SAXException("SAX2 driver class " + DEFAULT_SAX_PARSER_CLASS_NAME +
    				   " not found", e1);
    	} catch (IllegalAccessException e2) {
    	    throw new SAXException("SAX2 driver class " + DEFAULT_SAX_PARSER_CLASS_NAME +
    				   " found but cannot be loaded", e2);
    	} catch (InstantiationException e3) {
    	    throw new SAXException("SAX2 driver class " + DEFAULT_SAX_PARSER_CLASS_NAME +
    	   " loaded but cannot be instantiated (no empty public constructor?)",
    				   e3);
    	} catch (ClassCastException e4) {
    	    throw new SAXException("SAX2 driver class " + DEFAULT_SAX_PARSER_CLASS_NAME +
    				   " does not implement XMLReader", e4);
    	}
    }
	
    XMLReader createParser() {
        DefaultHandler handler = new DefaultHandler();
        XMLReader parserInstance = null;

        try {
            parserInstance = newInstance();
            	//XMLReaderFactory.createXMLReader(DEFAULT_SAX_PARSER_CLASS_NAME);
        } catch (SAXException e) {
        	ModelPlugin.getPluginLog().logError("default parser failed: " + e.getMessage());        	
        	return null;
        }

        SAXValidator.setFeature(parserInstance, NAMESPACES_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, NAMESPACE_PREFIXES_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, VALIDATION_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, VALIDATION_SCHEMA_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, VALIDATION_SCHEMA_CHECKING_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, VALIDATION_DYNAMIC_FEATURE_ID, false);
        SAXValidator.setFeature(parserInstance, FATAL_ERROR_PROCESSING_FEATURE_ID, false);

        try {
        	if(MetaLibLoader.validateMetaXML) {
        		parserInstance.setProperty(ENTITY_RESOLVER_PROPERTY_ID, new XMLEntityResolverImpl());
        	} else {
        		parserInstance.setProperty(ENTITY_RESOLVER_PROPERTY_ID, new EmptyHandler());
        	}
        } catch (SAXException e1) {
        	CommonPlugin.getPluginLog().logError( e1.getMessage()+"", e1); //$NON-NLS-1$
        }
        
        parserInstance.setContentHandler(handler);
        parserInstance.setErrorHandler(handler);
        parserInstance.setContentHandler(this);
        return parserInstance;
    }
    
    //SAXParser

	public void characters(char[] ch, int start, int length) throws SAXException {
	}

	public void endDocument() throws SAXException {
	}

	public void endElement(String uri, String localName, String name) throws SAXException {
		Node p = current.getParentNode();
		if(p instanceof Element) {
			current = (Element)p;
		}
	}

	public void endPrefixMapping(String prefix) throws SAXException {}

	public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {}

	public void processingInstruction(String target, String data) throws SAXException {
	}

	public void setDocumentLocator(Locator locator) {}

	public void skippedEntity(String name) throws SAXException {}

	public void startDocument() throws SAXException {
	}

	public void startElement(String uri, String localName, String name,
			Attributes atts) throws SAXException {
		if(current == null) {
			current = XMLUtilities.createDocumentElement(name);
			documentElement = current;
		} else {
			current = XMLUtilities.createElement(current, name);
		}
		int l = atts.getLength();
		for (int i = 0; i < l; i++) {
			String n = atts.getQName(i);
			String v = atts.getValue(i);
			current.setAttribute(n, v);
		}
	}

	public void startPrefixMapping(String prefix, String uri) throws SAXException {}

}

class EmptyHandler implements org.apache.xerces.xni.parser.XMLEntityResolver {

	public XMLInputSource resolveEntity(XMLResourceIdentifier id)
			throws XNIException, IOException {
		return new XMLInputSource(id.getPublicId(), id.getBaseSystemId(), id.getBaseSystemId(), new StringReader(""), null);
	}
	
}

class EmptyResolver implements EntityResolver {
	public InputSource resolveEntity(String publicID, String systemID) throws SAXException, IOException {
		return new InputSource(new StringReader("")); //$NON-NLS-1$
	}
}
