package org.testng.eclipse.util.param;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.search.internal.ui.text.FileSearchQuery;
import org.eclipse.search.internal.ui.text.FileSearchResult;
import org.eclipse.search.ui.ISearchQuery;
import org.eclipse.search.ui.text.FileTextSearchScope;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.testng.eclipse.TestNGPlugin;
import org.testng.eclipse.ui.util.SuiteListSelectionDialog;
import org.testng.xml.Parser;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.google.common.collect.Maps;


/**
 * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
 */
public class ParameterSolver {
  /**
   * TODO: this method searches only for parameters of the current <code>javaElement</code>
   * and not all the dependencies.
   */
  public static Map<String, String> solveParameters(IJavaElement[] javaElements) {
    Map<String, String> paramNames = Maps.newHashMap();

    if (null == javaElements || javaElements.length == 0) {
      return paramNames;
    }

    try {
      for(IJavaElement javaElement : javaElements) {
        Map<String, String> params = getParameterNames(javaElement);
        paramNames.putAll(params);
      }

      if(paramNames.isEmpty()) {
        return paramNames;
      }

      return findParameterValues(javaElements[0].getAncestor(IJavaElement.JAVA_PROJECT).getCorrespondingResource(), paramNames);
    }
    catch(JavaModelException jmex) {
      TestNGPlugin.log(jmex);
    }

    return paramNames;
  }

  private static Map<String, String> getParameterNames(IJavaElement javaElement) throws JavaModelException {
    switch(javaElement.getElementType()) {
      case IJavaElement.PACKAGE_FRAGMENT:
      {
        return solveParameters((IPackageFragment) javaElement);
      }

      case IJavaElement.COMPILATION_UNIT:
      {
        return solveParameters((ICompilationUnit) javaElement);
      }

      case IJavaElement.TYPE:
      {
        return solveParameters((IType) javaElement);
      }

      case IJavaElement.METHOD:
      {
        return solveParameters((IMethod) javaElement);
      }

      default:
        return Maps.newHashMap();
    }
  }

  private static Map<String, String> solveParameters(IPackageFragment packageFragment) throws JavaModelException {
    return parseParameterNames(packageFragment.getCompilationUnits(), new TestNGMethodParameterVisitor());
  }

  private static Map<String, String> solveParameters(ICompilationUnit compilationUnit) throws JavaModelException {
    return parseParameterNames(new ICompilationUnit[] {compilationUnit}, new TestNGMethodParameterVisitor());
  }

  private static Map<String, String> solveParameters(IType type) {
    return parseParameterNames(new ICompilationUnit[] {type.getCompilationUnit()}, new TestNGMethodParameterVisitor(type));
  }

  private static Map<String, String> solveParameters(IMethod method) throws JavaModelException {
    if(method.getNumberOfParameters() > 0) {
      return parseParameterNames(new ICompilationUnit[] {method.getCompilationUnit()}, new TestNGMethodParameterVisitor(method));
    }

    return Maps.newHashMap();
  }

  protected static Map<String, String> parseParameterNames(ICompilationUnit[] units, TestNGMethodParameterVisitor visitor) {
    for(ICompilationUnit unit : units) {
      ASTNode node= getParserNode(unit);
      node.accept(visitor);
    }

    return visitor.getParametersMap();
  }

  protected static ASTNode getParserNode(ICompilationUnit unit) {
    ASTParser parser = ASTParser.newParser(AST.JLS3);
    parser.setSource(unit);
    return parser.createAST(null);
  }

  private static Map<String, String> findParameterValues(IResource projectRes, Map<String, String> parameters) {
    IResource[] suiteFiles= searchSuites(new IResource[] {projectRes});
    IFile selectedSuite= null;

    if (suiteFiles.length == 0) {
    	// No parameters.  If they're all @Optional, this will work anyway.
    	// Otherwise, this will ultimately cause the test to fail with a clear error.
    	return Maps.newHashMap();
    }

    if(suiteFiles.length > 1) {
      selectedSuite= showSelectionDialog(suiteFiles);
    }
    else {
      selectedSuite= (IFile) suiteFiles[0];
    }

    if (selectedSuite == null) {
      return Maps.newHashMap();
    }
    return extractParameterValues(selectedSuite, parameters);
  }

  private static Map<String, String> extractParameterValues(IFile file, Map<String, String> parameters) {
    try {
      InputStream is= file.getContents();
      ParameterValuesContentHandler pvch= new ParameterValuesContentHandler(parameters);
      SAXParserFactory spf = null;
      try {
        spf = SAXParserFactory.newInstance();
      }
      catch(FactoryConfigurationError ex) {
        // If running with JDK 1.4
        try {
          Class cl = Class.forName("org.apache.crimson.jaxp.SAXParserFactoryImpl");
          spf = (SAXParserFactory) cl.newInstance();
        }
        catch(Exception ex2) {
          TestNGPlugin.log(ex2);
        }
      }

      if (null == spf) {
        return parameters;
      }

      spf.setValidating(true);
      SAXParser saxParser = spf.newSAXParser();
      saxParser.parse(is, pvch);
    }
    catch(ParserConfigurationException | SAXException | IOException | CoreException ex) {
      TestNGPlugin.log(ex);
    }

    return parameters;
  }

  protected static IFile showSelectionDialog(IResource[] choices) {
    final IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
    if (window == null) {
      return null;
    }
    final Shell parent = window.getShell();

    final SuiteListSelectionDialog dialog = new SuiteListSelectionDialog(parent, choices);
    dialog.setTitle("Select parameter definition file");
    final int resultCode = dialog.open();
    if (resultCode != IDialogConstants.OK_ID) {
      return null;
    }

    final Object[] result = dialog.getResult();
    if (result == null || result.length == 0 || result[0] instanceof IFile == false) {
      return null;
    }

    return (IFile) result[0];
  }

  protected static IResource[] searchSuites(IResource[] scopeResources) {
    ISearchQuery query= new FileSearchQuery("<!DOCTYPE suite SYSTEM \"http://testng.org/testng-1.0.dtd\" >", 
                                            false /*regexp*/ , 
                                            false /*casesensitive*/, 
                                            FileTextSearchScope.newSearchScope(scopeResources, new String[] {"*.xml"}, false));
    query.run(null);
    FileSearchResult result= (FileSearchResult) query.getSearchResult(); 
    Object[] elements= result.getElements();
    IResource[] resources= new IResource[elements.length];
    for(int i= 0; i < elements.length; i++) {
      resources[i]= (IResource) elements[i];
    }

    return resources;
  }

  static class ParameterValuesContentHandler extends DefaultHandler {
    private Map<String, String> m_params;

    public ParameterValuesContentHandler(Map<String, String> parameters) {
      m_params= parameters;
    }

    @Override
    public InputSource resolveEntity(String systemId, String publicId) throws SAXException {
      InputSource result = null;
      if (Parser.DEPRECATED_TESTNG_DTD_URL.equals(publicId) || Parser.TESTNG_DTD_URL.equals(publicId)) {
        InputStream is = getClass().getClassLoader().getResourceAsStream(Parser.TESTNG_DTD);
        if (null == is) {
          is = Thread.currentThread().getContextClassLoader().getResourceAsStream(Parser.TESTNG_DTD);
          if (null == is) {
            System.out.println("WARNING: couldn't find in classpath " + publicId + "\n" + "Fetching it from the Web site.");
            try {
              result = super.resolveEntity(systemId, publicId);
            }
            catch(Exception ex) {
              ex.printStackTrace();
            }
          }
          else {
            result = new InputSource(is);
          }
        }
        else {
          result = new InputSource(is);
        }
      }
      else {
        try {
          result = super.resolveEntity(systemId, publicId);
        }
        catch(Exception ex) {
              ex.printStackTrace();
            }
      }

      return result;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
      String name = attributes.getValue("name");

      if ("parameter".equals(qName) && m_params.containsKey(name)) {
        m_params.put(name, attributes.getValue("value")); 
      }
    }
  }
}
