/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jtstest.testrunner;


import java.io.*;
import java.util.*;

import org.jdom2.*;
import org.jdom2.input.*;
import org.jdom2.located.LocatedElement;
import org.jdom2.located.LocatedJDOMFactory;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.*;
import org.locationtech.jtstest.*;
import org.locationtech.jtstest.geomop.*;
import org.locationtech.jtstest.util.*;
import org.locationtech.jtstest.util.io.WKTOrWKBReader;


/**
 * @version 1.7
 */
public class TestReader
{
	private static final String TAG_geometryOperation = "geometryOperation"; 
	private static final String TAG_resultMatcher = "resultMatcher"; 
	
    Vector parsingProblems = new Vector();
    private GeometryFactory geometryFactory;
    private WKTOrWKBReader wktorbReader;
    private double tolerance = 0.0;
    private GeometryOperation geomOp = null;
    private ResultMatcher resultMatcher = null;
    
    public TestReader() 
    {
    }

    private GeometryOperation getGeometryOperation()
    {
    	// use the main one if it was user-specified or this run does not have an op specified
    	if (JTSTestRunnerCmd.isGeometryOperationSpecified()
    			|| geomOp == null)
    		return JTSTestRunnerCmd.getGeometryOperation();
    	
    	return geomOp;
    }

    private boolean isBooleanFunction(String name) {
        return getGeometryOperation().getReturnType(name) == boolean.class;
    }

    private boolean isIntegerFunction(String name) {
        return getGeometryOperation().getReturnType(name) == int.class;
    }

    private boolean isDoubleFunction(String name) {
        return getGeometryOperation().getReturnType(name) == double.class;
    }

    private boolean isGeometryFunction(String name) 
    {
    	Class returnType = getGeometryOperation().getReturnType(name);
    	if (returnType == null)
    		return false;
    	return Geometry.class.isAssignableFrom(returnType);
	}

    public List getParsingProblems() {
        return Collections.unmodifiableList(parsingProblems);
    }

    public void clearParsingProblems() {
        parsingProblems.clear();
    }

    public TestRun createTestRun(File testFile, int runIndex) {
        try {
            SAXBuilder builder = new SAXBuilder( );
            builder.setJDOMFactory(new LocatedJDOMFactory());
            Document document = builder.build(new FileInputStream(testFile));
            Element runElement = document.getRootElement();
            if (!runElement.getName().equalsIgnoreCase("run")) {
                throw new TestParseException(
                    "Expected <run> but encountered <" + runElement.getName() + ">");
            }
            return parseTestRun(runElement, testFile, runIndex);
        } catch (Exception e) {
            parsingProblems.add(
                "An exception occurred while parsing " + testFile + ": " + e.toString());
            return null;
        }
    }
    
    /**
     *  Creates a List of Test's from the given <test> Element's.
     */
    private List<Test> parseTests(
        List testElements,
        int caseIndex,
        File testFile,
        TestCase testCase,
        double tolerance)
        throws TestParseException {
        List<Test> tests = new ArrayList<Test>();
        int testIndex = 0;
        for (Iterator i = testElements.iterator(); i.hasNext();) {
            Element testElement = (Element) i.next();
            testIndex++;
            try {
                Element descElement = testElement.getChild("desc");
                if (testElement.getChildren("op").size() > 1) {
                    throw new TestParseException("Multiple <op>s in <test>");
                }
                Element opElement = testElement.getChild("op");
                if (opElement == null) {
                    throw new TestParseException("Missing <op> in <test>");
                }
                Attribute nameAttribute = opElement.getAttribute("name");
                if (nameAttribute == null) {
                    throw new TestParseException("Missing name attribute in <op>");
                }
                String arg1 =
                    opElement.getAttribute("arg1") == null
                        ? "A"
                        : opElement.getAttribute("arg1").getValue().trim();
                String arg2 =
                    opElement.getAttribute("arg2") == null
                        ? null
                        : opElement.getAttribute("arg2").getValue().trim();
                String arg3 =
                    opElement.getAttribute("arg3") == null
                        ? null
                        : opElement.getAttribute("arg3").getValue().trim();
                if (arg3 == null && nameAttribute.getValue().trim().equalsIgnoreCase("relate")) {
                    arg3 =
                        opElement.getAttribute("pattern") == null
                            ? null
                            : opElement.getAttribute("pattern").getValue().trim();
                }
                List<String> arguments = new ArrayList<String>();
                if (arg2 != null) {
                    arguments.add(arg2);
                }
                if (arg3 != null) {
                    arguments.add(arg3);
                }
                Result result = toResult(
                        opElement.getTextTrim(),
                        nameAttribute.getValue().trim(),
                        testCase.getTestRun());
                Test test = new Test(
                		testCase, 
                		testIndex,
                		descElement != null ? descElement.getTextTrim() : "", 
                		nameAttribute.getValue().trim(), 
                		arg1, 
                		arguments, 
                		result, 
                		tolerance);

                tests.add(test);
            } catch (Exception e) {
                parsingProblems.add(
                    "An exception occurred while parsing <test> "
                        + testIndex
                        + " in <case> "
                        + caseIndex
                        + " in "
                        + testFile
                        + ": "
                        + e.toString() + "\n" + StringUtil.getStackTrace(e));
            }
        }
        return tests;
    }

    private Result toResult(String value, String name, TestRun testRun)
        throws TestParseException, ParseException {
        // no expected result provided
        if (value.length() == 0) {
          return null; 
        }
        if (isBooleanFunction(name)) {
            return toBooleanResult(value);
        }
        if (isIntegerFunction(name)) {
            return toIntegerResult(value);
        }
        if (isDoubleFunction(name)) {
            return toDoubleResult(value);
        }
        if (isGeometryFunction(name)) {
            return toGeometryResult(value, testRun);
        }
        return null;
        //throw new TestParseException("Unknown operation name '" + name + "'");
    }

    private BooleanResult toBooleanResult(String value) throws TestParseException {
        if (value.equalsIgnoreCase("true")) {
            return new BooleanResult(true);
        } else if (value.equalsIgnoreCase("false")) {
            return new BooleanResult(false);
        } else {
            throw new TestParseException(
                "Expected 'true' or 'false' but encountered '" + value + "'");
        }
    }

    private DoubleResult toDoubleResult(String value) throws TestParseException {
        try {
            return new DoubleResult(Double.valueOf(value));
        } catch (NumberFormatException e) {
            throw new TestParseException("Expected double but encountered '" + value + "'");
        }
    }

    private IntegerResult toIntegerResult(String value) throws TestParseException {
        try {
            return new IntegerResult(Integer.valueOf(value));
        } catch (NumberFormatException e) {
            throw new TestParseException("Expected integer but encountered '" + value + "'");
        }
    }

    private GeometryResult toGeometryResult(String value, TestRun testRun) throws ParseException {
        GeometryFactory geometryFactory = new GeometryFactory(testRun.getPrecisionModel(), 0);
        WKTOrWKBReader wktorbReader = new WKTOrWKBReader(geometryFactory);
        return new GeometryResult(wktorbReader.read(value));
    }

    /**
     *  Creates a List of TestCase's from the given <case> Element's.
     */
    private List parseTestCases(
        List caseElements,
        File testFile,
        TestRun testRun,
        double tolerance)
        throws TestParseException {
        geometryFactory = new GeometryFactory(testRun.getPrecisionModel(), 0, TestCoordinateSequenceFactory.instance());
        wktorbReader = new WKTOrWKBReader(geometryFactory);
        Vector testCases = new Vector();
        int caseIndex = 0;
        for (Iterator i = caseElements.iterator(); i.hasNext();) {
            Element caseElement = (Element) i.next();
            //System.out.println("Line: " + ((LineNumberElement)caseElement).getStartLine());
            caseIndex++;
            try {
                Element descElement = caseElement.getChild("desc");
                Element aElement = caseElement.getChild("a");
                Element bElement = caseElement.getChild("b");
                File aWktFile = wktFile(aElement, testRun);
                File bWktFile = wktFile(bElement, testRun);
                Geometry a = readGeometry(aElement, absoluteWktFile(aWktFile, testRun));
                Geometry b = readGeometry(bElement, absoluteWktFile(bWktFile, testRun));
                TestCase testCase =
                    new TestCase(
                        descElement != null ? descElement.getTextTrim() : "",
                        a,
                        b,
                        aWktFile,
                        bWktFile,
                        testRun,
                        caseIndex,
                        ((LocatedElement)caseElement).getLine());
                List testElements = caseElement.getChildren("test");
                //        if (testElements.size() == 0) {
                //          throw  new TestParseException("Missing <test> in <case>");
                //        }
                List tests = parseTests(testElements, caseIndex, testFile, testCase, tolerance);
                for (Iterator j = tests.iterator(); j.hasNext();) {
                    Test test = (Test) j.next();
                    testCase.add(test);
                }
                testCases.add(testCase);
            } catch (Exception e) {
                parsingProblems.add(
                    "An exception occurred while parsing <case> "
                        + caseIndex
                        + " in "
                        + testFile
                        + ": "
                        + e.toString());
            }
        }
        return testCases;
    }

    /**
     *  Creates a TestRun from the <run> Element.
     */
    private TestRun parseTestRun(Element runElement, File testFile, int runIndex)
        throws TestParseException 
    {
    	
      //----------- <workspace> (optional) ------------------
        File workspace = null;
        if (runElement.getChild("workspace") != null) {
            if (runElement.getChild("workspace").getAttribute("dir") == null) {
                throw new TestParseException("Missing <dir> in <workspace>");
            }
            workspace =
                new File(runElement.getChild("workspace").getAttribute("dir").getValue().trim());
            if (!workspace.exists()) {
                throw new TestParseException("<workspace> does not exist: " + workspace);
            }
            if (!workspace.isDirectory()) {
                throw new TestParseException("<workspace> is not a directory: " + workspace);
            }
        }
        
        //----------- <tolerance> (optional) ------------------
        tolerance = parseTolerance(runElement);
        
        Element descElement = runElement.getChild("desc");

        //----------- <geometryOperation> (optional) ------------------
        geomOp = parseGeometryOperation(runElement);
        
        //----------- <geometryMatcher> (optional) ------------------
        resultMatcher = parseResultMatcher(runElement);
        
        //-----------  <precisionModel> (optional) ----------------
        PrecisionModel precisionModel = parsePrecisionModel(runElement);
        
        //--------------- build TestRun  ---------------------
        TestRun testRun =
            new TestRun(
                descElement != null ? descElement.getTextTrim() : "",
                runIndex,
                precisionModel,
                geomOp,
                resultMatcher,
                testFile);
        testRun.setWorkspace(workspace);
        List caseElements = runElement.getChildren("case");
        if (caseElements.size() == 0) {
            throw new TestParseException("Missing <case> in <run>");
        }
        for (Iterator i = parseTestCases(caseElements, testFile, testRun, tolerance).iterator();
            i.hasNext();
            ) {
            TestCase testCase = (TestCase) i.next();
            testRun.addTestCase(testCase);
        }
        return testRun;
    }

    /**
     * Parses an optional <tt>precisionModel</tt> element.
     * The default is to use a FLOATING model.
     * 
     * @param runElement
     * @return a PrecisionModel instance (default if not specified)
     * @throws TestParseException
     */
    private PrecisionModel parsePrecisionModel(Element runElement)
    	throws TestParseException
    {
      PrecisionModel precisionModel = new PrecisionModel();
      Element precisionModelElement = runElement.getChild("precisionModel");
      if (precisionModelElement == null) {
        return precisionModel;
      }
      Attribute typeAttribute = precisionModelElement.getAttribute("type");
      Attribute scaleAttribute = precisionModelElement.getAttribute("scale");
      if (typeAttribute == null && scaleAttribute == null) {
          throw new TestParseException("Missing type attribute in <precisionModel>");
      }
      if (scaleAttribute != null
          || (typeAttribute != null && typeAttribute.getValue().trim().equalsIgnoreCase("FIXED"))) {
          if (typeAttribute != null
              && typeAttribute.getValue().trim().equalsIgnoreCase("FLOATING")) {
              throw new TestParseException("scale attribute not allowed in floating <precisionModel>");
          }
          precisionModel = createPrecisionModel(precisionModelElement);
      }
      return precisionModel;
    }
    
    private PrecisionModel createPrecisionModel(Element precisionModelElement)
			throws TestParseException {
		Attribute scaleAttribute = precisionModelElement.getAttribute("scale");
		if (scaleAttribute == null) {
			throw new TestParseException(
					"Missing scale attribute in <precisionModel>");
		}
		double scale;
		try {
			scale = scaleAttribute.getDoubleValue();
		} catch (DataConversionException e) {
			throw new TestParseException(
					"Could not convert scale attribute to double: "
							+ scaleAttribute.getValue());
		}
		return new PrecisionModel(scale);
	}


    /**
		 * Parses an optional <tt>geometryOperation</tt> element. 
		 * The default is to leave this unspecified .
		 * 
		 * @param runElement
		 * @return an instance of the GeometryOperation class, if specified, or
		 * null if no geometry operation was specified
		 * @throws TestParseException if a parsing error was encountered
		 */
    private GeometryOperation parseGeometryOperation(Element runElement)
  	throws TestParseException
  {
    Element goElement = runElement.getChild(TAG_geometryOperation);
    if (goElement == null) {
      return null;
    }
    String goClass = goElement.getTextTrim();
    GeometryOperation geomOp = (GeometryOperation) getInstance(goClass, GeometryOperation.class);
    if (geomOp == null) {
    	throw new TestParseException("Could not create instance of GeometryOperation from class " + goClass);
    }
    return geomOp;
  }
 
    /**
		 * Parses an optional <tt>resultMatcher</tt> element. 
		 * The default is to leave this unspecified .
		 * 
		 * @param runElement
		 * @return an instance of the ResultMatcher class, if specified, or
		 *  null if no result matcher was specified
		 * @throws TestParseException if a parsing error was encountered
		 */
    private ResultMatcher parseResultMatcher(Element runElement)
  	throws TestParseException
  {
    Element goElement = runElement.getChild(TAG_resultMatcher);
    if (goElement == null) {
      return null;
    }
    String goClass = goElement.getTextTrim();
    ResultMatcher resultMatcher = (ResultMatcher) getInstance(goClass, ResultMatcher.class);
    if (resultMatcher == null) {
    	throw new TestParseException("Could not create instance of ResultMatcher from class " + goClass);
    }
    return resultMatcher;
  }
 
  private double parseTolerance(Element runElement) throws TestParseException 
    {
		double tolerance = 0.0;
		// Note: the tolerance element applies to the coordinate-by-coordinate
		// comparisons of spatial functions. It does not apply to binary predicates.
		// [Jon Aquino]
		Element toleranceElement = runElement.getChild("tolerance");
		if (toleranceElement != null) {
			try {
				tolerance = Double.parseDouble(toleranceElement.getTextTrim());
			} catch (NumberFormatException e) {
				throw new TestParseException("Could not parse tolerance from string: "
						+ toleranceElement.getTextTrim());
			}
		}
		return tolerance;
	}

  /*
	private GeometryOperation getGeometryOperationInstance(String classname) {
		GeometryOperation op = null;
		try {
			Class goClass = Class.forName(classname);
			if (!(GeometryOperation.class.isAssignableFrom(goClass)))
				return null;
			op = (GeometryOperation) goClass.newInstance();
		} catch (Exception ex) {
			return null;
		}
		return op;
	}
    */
  
  /**
   * Gets an instance of a class with the given name, 
   * and ensures that the class is assignable to a specified baseClass.
   * 
   * @return an instance of the class, if it is assignment-compatible, or
   *  null if the requested class is not assigment-compatible
   */
	private Object getInstance(String classname, Class baseClass) {
		Object o = null;
		try {
			Class goClass = Class.forName(classname);
			if (!(baseClass.isAssignableFrom(goClass)))
				return null;
			o = goClass.newInstance();
		} catch (Exception ex) {
			return null;
		}
		return o;
	}
    
    private File wktFile(Element geometryElement, TestRun testRun) throws TestParseException {
        if (geometryElement == null) {
            return null;
        }
        if (geometryElement.getAttribute("file") == null) {
            return null;
        }
        if (!geometryElement.getTextTrim().equals("")) {
            throw new TestParseException("WKT specified both in-line and in external file");
        }

        File wktFile = new File(geometryElement.getAttribute("file").getValue().trim());
        File absoluteWktFile = absoluteWktFile(wktFile, testRun);

        if (!absoluteWktFile.exists()) {
            throw new TestParseException("WKT file does not exist: " + absoluteWktFile);
        }
        if (absoluteWktFile.isDirectory()) {
            throw new TestParseException("WKT file is a directory: " + absoluteWktFile);
        }

        return wktFile;
    }

    private Geometry readGeometry(Element geometryElement, File wktFile)
        throws FileNotFoundException, ParseException, IOException
    {
      String geomText = null;
      if (wktFile != null) {
        List wktList = getContents(wktFile.getPath());
        geomText = toString(wktList);
      }
      else {
        if (geometryElement == null)
          return null;
        geomText = geometryElement.getTextTrim();
      }
      return wktorbReader.read(geomText);
        /*
        if (isHex(geomText, 6))
          return wkbReader.read(WKBReader.hexToBytes(geomText));
        reurn wktReader.read(geomText);
        */
    }

    private String toString(List stringList) {
        String string = "";
        for (Iterator i = stringList.iterator(); i.hasNext();) {
            String line = (String) i.next();
            string += line + "\n";
        }
        return string;
    }

    private File absoluteWktFile(File wktFile, TestRun testRun) {
        if (wktFile == null) {
            return null;
        }
        File absoluteWktFile = wktFile;
        if (!absoluteWktFile.isAbsolute()) {
            File directory =
                testRun.getWorkspace() != null
                    ? testRun.getWorkspace()
                    : testRun.getTestFile().getParentFile();
            absoluteWktFile = new File(directory + File.separator + absoluteWktFile.getName());
        }
        return absoluteWktFile;
    }
    
    /**
     * Returns a List of the String's in the text file, one per line.
     */
    public static List getContents(String textFileName) throws FileNotFoundException, IOException {
        List contents = new Vector();
        FileReader fileReader = new FileReader(textFileName);
        try(BufferedReader bufferedReader = new BufferedReader(fileReader)){
            String line = bufferedReader.readLine();
            while (line != null) {
                contents.add(line);
                line = bufferedReader.readLine();
            }
        }
        return contents;
    }

}
