/*******************************************************************************
 * Copyright (c) 2002, 2018 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.contentmodel.modelqueryimpl;
                          
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.CRC32;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.XMLCorePlugin;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.CMDocumentManager;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.IExternalSchemaLocationProvider;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.contentmodel.util.CMDocumentCache;
import org.eclipse.wst.xml.core.internal.contentmodel.util.NamespaceInfo;
import org.eclipse.wst.xml.core.internal.contentmodel.util.NamespaceTable;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 *
 */
public class CMDocumentLoader
{                                           
  private final static boolean _trace = Boolean.valueOf(Platform.getDebugOption("org.eclipse.wst.xml.core/externalSchemaLocation")).booleanValue(); //$NON-NLS-1$
  protected Document document;
  protected ModelQuery modelQuery;
  protected CMDocumentManager cmDocumentManager;
  protected boolean isInferredGrammarEnabled = true;  
  protected CMDocumentLoadingNamespaceTable namespaceTable;
  protected int count = 0;
    
  public CMDocumentLoader(Document document, ModelQuery modelQuery)
  {                             
    this(document, modelQuery.getCMDocumentManager());
  }
  
  public CMDocumentLoader(Document document, CMDocumentManager cmDocumentManager)
  {   
    this.document = document;                     
    this.cmDocumentManager = cmDocumentManager;  	
  }
  
  public void loadCMDocuments()
  {          
    //System.out.println("----------loadCMDocuments ------------");          
    //long time = System.currentTimeMillis();
       
    boolean walkDocument = false;
            
    cmDocumentManager.removeAllReferences();
      
    String[] doctypeInfo = XMLAssociationProvider.getDoctypeInfo(document);
    if (doctypeInfo != null)
    {
      // load the doctype if required
      walkDocument = handleGrammar(doctypeInfo[0], doctypeInfo[1], "DTD"); //$NON-NLS-1$
    }                                   
    else
    {      
    	if (document.getDoctype() != null) {
    		final String internalSubset = document.getDoctype().getInternalSubset();
    		if (internalSubset != null) {
    			IPath path = getInternalSubsetPath(document);
    			if (!Path.EMPTY.equals(path)) {
    				File subsets = path.removeLastSegments(1).toFile();
        			if (!subsets.exists()) {
        				subsets.mkdir();
        			}
        			FileOutputStream stream = null;
        			try {
        				stream = new FileOutputStream(path.toFile());
        				final String charset = ((IFile) ((IAdaptable) document).getAdapter(IResource.class)).getCharset();
        				stream.write(internalSubset.getBytes(charset != null ? charset : "UTF-8")); //$NON-NLS-1$
        				stream.flush();
        			} catch (FileNotFoundException e) {
        			} catch (CoreException e) {
        			} catch (UnsupportedEncodingException e) {
        			} catch (IOException e) {
        			}
        			finally {
        				if (stream != null) {
        					try {
        						stream.close();
        					}
        					catch (IOException e) { }
        				}
        			}
        			walkDocument = handleGrammar(path.toPortableString(), path.toFile().toURI().toString(), "DTD"); //$NON-NLS-1$
    			}
    		}
    	}
    	else {
	      Element element = getRootElement(document);
	      if (element != null)
	      {
	        namespaceTable = new CMDocumentLoadingNamespaceTable(document);   
	        namespaceTable.addElement(element);
	        if (namespaceTable.isNamespaceEncountered())
	        {   
	          walkDocument = true;
	          //System.out.println("isNamespaceAware");
	        }
	        else
	        {
	          namespaceTable = null;
	          walkDocument = isInferredGrammarEnabled;
	          //System.out.println("is NOT namespaceAware");
	        }        
	      }
    	}
    } 

    if (walkDocument)
    {
    	if (!checkExternalSchema())
    		visitNode(document);   
    } 

    //System.out.println("--- elapsed time (" + count + ") = " + (System.currentTimeMillis() - time));
  }

  protected boolean checkExternalSchema() {
	  boolean externalSchemaLoaded = false;
	  if (document instanceof IDOMDocument) {
		  final String baseLocation = ((IDOMDocument) document).getModel().getBaseLocation();
		  if (baseLocation == null)
			  return false;
		  final IPath basePath = new Path(baseLocation);
		  IFile file = null;
		  if (basePath.segmentCount() > 1) {
			  file = ResourcesPlugin.getWorkspace().getRoot().getFile(basePath);
		  }
		  final URI uri = (file == null || !file.isAccessible()) ? new File(baseLocation).toURI() : file.getLocationURI();
		  if (uri != null) {
			  IExternalSchemaLocationProvider[] providers = ExternalSchemaLocationProviderRegistry.getInstance().getProviders();
			  for (int i = 0; i < providers.length; i++) {
				  long time = _trace ? System.currentTimeMillis(): 0;
				  final Map locations = providers[i].getExternalSchemaLocation(uri);
				  if (_trace) {
					  long diff = System.currentTimeMillis() - time;
					  if (diff > 250)
						  Logger.log(Logger.INFO, "Schema location provider took [" + diff + "ms] for URI [" + uri + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				  }
				  if (locations != null && !locations.isEmpty()) {
					  // Use the externalSchemaLocation
					  if (namespaceTable != null && namespaceTable.isNamespaceEncountered()) {
						Object schemaLocation = locations.get(IExternalSchemaLocationProvider.SCHEMA_LOCATION);
						if (schemaLocation != null) {
						  final String location = schemaLocation.toString();
						  if (location != null) {
							  final String[] ids = StringUtils.asArray(location);
							  // namespace : location pairs
							  if (ids.length >= 2 && ids.length % 2 == 0) {
								  if (!externalSchemaLoaded)
									  cmDocumentManager.removeAllReferences();
								  for (int j = 0; j < ids.length; j+=2) {
									  handleGrammar(ids[j], ids[j + 1], "XSD"); //$NON-NLS-1$
									  externalSchemaLoaded = true;
								  }
							  }
						  }
						  else {
							  Logger.log(Logger.ERROR_DEBUG, "External schema location provider did not return an external schema location for IExternalSchemaLocationProvider.SCHEMA_LOCATION: " + providers[i].getClass().getName()); //$NON-NLS-1$
						  }
						}
					  }
					  else { // noNamespace
						  Object schemaLocation = locations.get(IExternalSchemaLocationProvider.NO_NAMESPACE_SCHEMA_LOCATION);
						  if (schemaLocation != null) {
						    handleGrammar(uri.toString(), schemaLocation.toString(), "XSD"); //$NON-NLS-1$
						    externalSchemaLoaded = true;
						    break;
						  }
						  else {
						    Logger.log(Logger.ERROR_DEBUG, "External schema location provider did not return an external schema location for IExternalSchemaLocationProvider.NO_NAMESPACE_SCHEMA_LOCATION: " + providers[i].getClass().getName()); //$NON-NLS-1$
						  }
					  }
				  }
			  }
		  }
	  }
	  return externalSchemaLoaded;
  }

  public boolean handleGrammar(String publicId, String systemId, String type)
  {           
    boolean result = false;
    
    int status = cmDocumentManager.getCMDocumentStatus(publicId);
    if (status == CMDocumentCache.STATUS_NOT_LOADED)
    {
      cmDocumentManager.addCMDocumentReference(publicId, systemId, type);
    }                 
    else if (status == CMDocumentCache.STATUS_ERROR)
    {
      result = true;
    }
    return result;
  } 
    

  public void handleElement(Element element)
  {  
    visitChildNodes(element);
  }

                             
  public void handleElementNS(Element element)
  {
    namespaceTable.addElement(element);
    visitChildNodes(element);
  }
                                                    

  public void visitNode(Node node)
  {                    
    int nodeType = node.getNodeType();
    if (nodeType == Node.ELEMENT_NODE)
    {
      count++;       

      Element element = (Element)node;    
      if (namespaceTable == null)
      {
        handleElement(element); 
      }
      else
      {
        handleElementNS(element);
      }            
    }
    else if (nodeType == Node.DOCUMENT_NODE)
    {
      visitChildNodes(node);
    }
  }


  protected void visitChildNodes(Node node)
  {   
	  for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) 
    {
	    visitNode(child);
    }
	}             


  protected class CMDocumentLoadingNamespaceTable extends NamespaceTable
  {                                        
    protected List newNamespaceList;

    public CMDocumentLoadingNamespaceTable(Document document)
    {                                                          
      super(document);     
    }  
                                           

    public void addElement(Element element)
    {                               
      newNamespaceList = null;
      super.addElement(element);  
      if (newNamespaceList != null)
      {
        for (Iterator i = newNamespaceList.iterator(); i.hasNext(); )
        {
          NamespaceInfo info = (NamespaceInfo)i.next();
          handleGrammar(info.uri, info.locationHint, "XSD"); //$NON-NLS-1$
        }
      }
    }                 
     
                               
    protected void internalAddNamespaceInfo(String key, NamespaceInfo info)
    {
      super.internalAddNamespaceInfo(key, info);           
      if (newNamespaceList == null)
      {
        newNamespaceList = new ArrayList();
      }
      newNamespaceList.add(info);    
    }                     
  }

  
  protected Element getRootElement(Document document)
  {
    Element result = null;
    NodeList nodeList = document.getChildNodes();
    int nodeListLength = nodeList.getLength();
    for (int i = 0 ; i < nodeListLength; i++)
    {
      Node node = nodeList.item(i);
      if (node.getNodeType() == Node.ELEMENT_NODE)
      {
        result = (Element)node;
        break;
      }
    }
    return result;
  }

  static IPath getInternalSubsetPath(Document document) {
	  IPath path = Path.EMPTY;
	  if (document instanceof IAdaptable) {
			final IResource resource = ((IAdaptable) document).getAdapter(IResource.class);
			if (resource instanceof IFile) {
				CRC32 calc = new CRC32();
				calc.update(resource.getFullPath().toString().getBytes());
	  			path = XMLCorePlugin.getDefault().getStateLocation().append("internalsubsets").append(calc.getValue() + ".dtd"); //$NON-NLS-1$ //$NON-NLS-2$
			}
	  }
	  return path;
  }
}
