/*******************************************************************************
 * Copyright (c) 2004, 2008 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
 *******************************************************************************/

package org.eclipse.wst.xml.core.internal.search;

import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.wst.common.core.search.SearchMatch;
import org.eclipse.wst.common.core.search.SearchParticipant;
import org.eclipse.wst.common.core.search.SearchRequestor;
import org.eclipse.wst.common.core.search.document.ComponentDeclarationEntry;
import org.eclipse.wst.common.core.search.document.Entry;
import org.eclipse.wst.common.core.search.document.FileReferenceEntry;
import org.eclipse.wst.common.core.search.document.SearchDocument;
import org.eclipse.wst.common.core.search.document.SearchDocumentSet;
import org.eclipse.wst.common.core.search.pattern.ComponentSearchPattern;
import org.eclipse.wst.common.core.search.pattern.FileReferencePattern;
import org.eclipse.wst.common.core.search.pattern.SearchPattern;
import org.eclipse.wst.common.core.search.scope.ContentTypeSearchScope;
import org.eclipse.wst.common.core.search.scope.SearchScope;
import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolver;
import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.search.impl.IXMLSearchConstants;
import org.eclipse.wst.xml.core.internal.search.impl.XMLSearchDocument;
import org.eclipse.wst.xml.core.internal.search.matching.PatternMatcher;
import org.eclipse.wst.xml.core.internal.search.matching.XMLSearchPatternMatcher;
import org.eclipse.wst.xml.core.internal.search.quickscan.XMLQuickScan;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 *
 */
public abstract class XMLSearchParticipant extends SearchParticipant {
	
	protected static final boolean debugPerf = "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.wst.xml.core.internal.search/perf")); //$NON-NLS-1$ //$NON-NLS-2$

	public XMLSearchParticipant() {
		super();
	}

	
	/*
 public  boolean initialize(SearchPattern pattern, String[] contentTypes){
		
	    super.initialize(pattern, contentTypes);
		this.supportedContentTypes = contentTypes;
		
		if(pattern instanceof XMLComponentSearchPattern){
			return true;
		}
		return false;
	}*/

	

	public SearchDocument createSearchDocument(String documentPath) {

		return new XMLSearchDocument(documentPath, this);

	}

	public String getDescription() {
		return "XML search participant"; //$NON-NLS-1$
	}



	private void locateMatches(SearchPattern pattern, SearchDocument document,
			SearchRequestor requestor, Map searchOptions, IProgressMonitor monitor) {

        // TODO... utilize search options (that should get passed down via the SearchEngine)
        // to specify if accurate source coordinates are req'd if not, simply use the SAX results
        //
        if (pattern.getMatchRule() == SearchPattern.R_PATTERN_MATCH)
        {          
          IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(new Path(document.getPath()));
          // TODO.. don't assume the category is COMPONENT_DECL... handle any arbitarty category
          Entry[] entries = document.getEntries(IXMLSearchConstants.COMPONENT_DECL, null, 0);
          for (int i = 0; i < entries.length; i++)
          {
            // TODO.. don't assume this is just a component declaration entry            
            ComponentDeclarationEntry entry = (ComponentDeclarationEntry)entries[i];
            SearchMatch searchMatch = new SearchMatch(null, 0, 0, file);
            searchMatch.map.put("name", entry.getName()); //$NON-NLS-1$
            searchMatch.map.put("metaName", entry.getMetaName()); //$NON-NLS-1$
            try
            {
              requestor.acceptSearchMatch(searchMatch);
            }
            catch (Exception e)
            {              
            }
          }  
        }
        else 
        {  if (document.getModel() instanceof IDOMModel) {
			IDOMModel domModel = (IDOMModel) document.getModel();
			IDOMElement contextNode = (IDOMElement) domModel.getDocument()
					.getDocumentElement();
			DOMVisitor visitor = new DOMVisitor(document.getPath(), pattern,
					requestor);
			visitor.visit(contextNode);
		}
        }
	}
	
	private PatternMatcher getAdapter(Object adaptableObject, Class adapterType) {
		if (PatternMatcher.class.equals(adapterType) &&
				(adaptableObject instanceof XMLSearchPattern ||
				adaptableObject instanceof XMLComponentSearchPattern) ) {
			return new XMLSearchPatternMatcher(this);
		} 
		return null; 
	}

	private class DOMVisitor {

		String path;
		SearchPattern pattern;
		SearchRequestor requestor;
		PatternMatcher matcher;

		
		protected DOMVisitor(String path, SearchPattern pattern,
				SearchRequestor requestor) {
			super();
			this.path = path;
			this.pattern = pattern;
			
			matcher = (PatternMatcher)pattern.getAdapter(PatternMatcher.class);
			if(matcher == null){
				matcher = getAdapter(pattern, PatternMatcher.class);
			}
			this.requestor = requestor;
		}

		private void visit(Node node) {
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				match((Element)node);
				NodeList nodeList = node.getChildNodes();
				for (int i = 0; i < nodeList.getLength(); i++) {
					Node aNode = nodeList.item(i);
					visit(aNode);
				}
			}
		}

		private void match(Element node) {
			IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(
					new Path(path));
			if(matcher != null){
				matcher.locateMatches(pattern, file, node, requestor);
			}

			
		}

	}

	public SearchScope selectDocumentLocations(SearchPattern pattern, SearchScope scope, Map searchOptions, IProgressMonitor monitor) {
		/*
		 * gate #1: reduce the scope to the files with the content type that
		 * could be searched using this participant
		 */ 
		String[] contentTypes = getSupportedContentTypes();	
		if(contentTypes != null && contentTypes.length > 0){
			scope = new ContentTypeSearchScope(scope, contentTypes);
		}
		return super.selectDocumentLocations(pattern, scope, searchOptions, monitor);
	}
	
	public abstract ComponentSearchContributor getSearchContributor();


	public void beginSearching(SearchPattern pattern, Map searchOptions) {
		
		super.beginSearching(pattern, searchOptions);
		if(pattern instanceof XMLComponentDeclarationPattern){
			XMLComponentDeclarationPattern componentPattern = (XMLComponentDeclarationPattern)pattern;
			XMLSearchPattern childPattern = getSearchContributor().getDeclarationPattern(componentPattern.getMetaName());
			if(childPattern != null){
					childPattern.setSearchName(componentPattern.getName().getLocalName());
					childPattern.setSearchNamespace(componentPattern.getName().getNamespace());
	     			componentPattern.addChildren(this, new XMLSearchPattern[]{childPattern});
			}
			
		}
		else if(pattern instanceof XMLComponentReferencePattern){
			XMLComponentReferencePattern componentPattern = (XMLComponentReferencePattern)pattern;
			XMLSearchPattern[] childPatterns = getSearchContributor().getReferencesPatterns(componentPattern.getMetaName());
			for (int i = 0; i < childPatterns.length; i++) {
				XMLSearchPattern childPattern = childPatterns[i];
				childPattern.setSearchName(componentPattern.getName().getLocalName());
				childPattern.setSearchNamespace(componentPattern.getName().getNamespace());				
			}
			componentPattern.addChildren(this, childPatterns);
			
		}
	}
	
	
	/**
	 * The intend of this method is to limit the search to the files that have content 
	 * which can be searched for the given pattern. It is called from 
	 * {@link #selectDocumentLocations(SearchPattern, SearchScope, IProgressMonitor)}
	 * 
	 * @param pattern the search pattern that is searched for
	 * @return content type's unique identifiers that could be searched for the given pattern.
	 */
	public abstract String[] getSupportedContentTypes();

	public void populateSearchDocument(SearchDocument document, SearchPattern pattern)
	{
		PatternMatcher matcher = (PatternMatcher)pattern.getAdapter(PatternMatcher.class);
		if(matcher == null){
			matcher = getAdapter(pattern, PatternMatcher.class);
		}
		XMLQuickScan.populateSearchDocument(document, matcher, pattern);		
	}

	public void locateMatches(SearchDocumentSet documentSet, SearchPattern pattern, SearchScope scope, SearchRequestor requestor, Map searchOptions, IProgressMonitor monitor) throws CoreException
	{
		long time = System.currentTimeMillis();
		
		// TODO: use the file reference entries in the documents to reduce the scope to the referenced files only
		// SearchDocument[] documents = documentSet.getSearchDocuments(id);
                
        // check to see if the search pattern is qualified by a file location
        // if this is the case then we can use file scoping rules to prune the matches
        IFile targetFile = null;
        if (pattern instanceof ComponentSearchPattern)
        {
          ComponentSearchPattern componentSearchPattern = (ComponentSearchPattern)pattern;
          targetFile = componentSearchPattern.getFile();                                       
        }		
        
		// here we should have in scope only referenced files
		IFile[] files = scope.enclosingFiles();  
		for (int i = 0; i < files.length; i++)
		{
			IFile file = files[i];
			String path = file.getLocation().toString();
			SearchDocument document = documentSet.getSearchDocument(path, id); 
			if (document != null)
			{              
			Entry[] entries = document.getEntries(getSearchEntryCategory(pattern), null, 0);           
			if ((entries != null && entries.length > 0) || (searchOptions != null && searchOptions.get("searchDirtyContent") != null))
            {
              //for (int j = 0; j < entries.length; j++)
              //{
              //  Entry entry = entries[j];
                //System.out.println("entry " + entry.getCategory() + " " + entry.getKey() + " " + entry.getClass().getName());                 
              //}  
              
              boolean isInScope = true;
              if (targetFile != null)
              {
                try
                {
                  isInScope = isLinked(documentSet, "file:///" + path, "file:///" + targetFile.getLocation().toString()); //$NON-NLS-1$ //$NON-NLS-2$
                  //if (path.endsWith("CancelSelection.wsdl")  && path.indexOf("clone1") != -1)
                  //{
                  //  fileReferenceTable.debug(qualifiedPath, 0, 5);
                  //}                   
                }
                catch (Exception e)
                {
                  e.printStackTrace();
                }
              }              
              if (isInScope)
              { 
			    this.locateMatches(pattern, document, requestor, searchOptions, monitor);
              }  
            }
			}
		}
		
		
		if (debugPerf)
		{
			System.out
					.println("" //$NON-NLS-1$
							+ getDescription()
							+ ": " + (System.currentTimeMillis() - time) + "ms for locateMatches"); //$NON-NLS-1$ //$NON-NLS-2$
		}

		
	}
    
    private boolean isLinked(SearchDocumentSet set, String source, String target)
    {
      return isLinked(set, source, target, new HashMap());
    }
    
    private boolean isLinked(SearchDocumentSet set, String source, String target, HashMap visited)
    {
      if (source.equals(target))
        return true;
      
      String fileProtocol = "file:///";             //$NON-NLS-1$
      
      // Fix for bug 204174 - Begin
      if(target.charAt(fileProtocol.length()) == '/')  //$NON-NLS-1$
      {
          target = fileProtocol + target.substring(fileProtocol.length() + 1);
      }
      // Fix for bug 204174 - End
            
      if (source.startsWith(fileProtocol))
      {    
        
      SearchDocument document = set._tempGetSearchDocumetn(source.substring(fileProtocol.length()));      
      if (document != null)
      {        
        URIResolver uriResolver = URIResolverPlugin.createResolver();        
        Entry[] entries = document.getEntries(IXMLSearchConstants.REF, null, 0);
        String[] resolveEntry = new String[entries.length];        
        for (int j = 0; j < entries.length; j++)
        {
          Entry entry = entries[j];
          if (entry instanceof FileReferenceEntry)
          {
            FileReferenceEntry fileReferenceEntry = (FileReferenceEntry)entry;
            // TODO.. record an utilize the public id from the fileReferenceEntry
            //
            if (fileReferenceEntry.getRelativeFilePath() != null)
            {  
              String resolvedURI = uriResolver.resolve(source, null, fileReferenceEntry.getRelativeFilePath());
              resolveEntry[j] = resolvedURI;
              if (resolvedURI.equals(target))
              {
                return true;
              }             
            }  
          }
        }
        // now see if there's an indirect link from the source to the target
        // we keep track of the nodes we've already visited to avoid cycles
        if (visited.get(source) == null)
        {
          visited.put(source, Boolean.TRUE);
          for (int j = 0; j < entries.length; j++)
          {                     
            String resolvedURI = resolveEntry[j];
            if (resolvedURI != null && isLinked(set, resolveEntry[j], target, visited))                
              return true;            
          }    
        }                      
      }            
      } 
      return false;
    }
    
   
    
	public static String getSearchEntryCategory(SearchPattern pattern){
		if(pattern instanceof XMLComponentDeclarationPattern){
			return IXMLSearchConstants.COMPONENT_DECL;
		}
		else if(pattern instanceof XMLComponentReferencePattern){
			return IXMLSearchConstants.COMPONENT_REF;
		}
		else if(pattern instanceof FileReferencePattern){
		   return IXMLSearchConstants.COMPONENT_REF;
		}
		return null;
	}   
}
