package org.etsi.mts.tdl.constraints.evl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EValidator;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EObjectValidator;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.epsilon.common.parse.Region;
import org.eclipse.epsilon.common.parse.problem.ParseProblem;
import org.eclipse.epsilon.common.util.StringProperties;
import org.eclipse.epsilon.emc.emf.EmfModel;
import org.eclipse.epsilon.emc.emf.EmfUtil;
import org.eclipse.epsilon.emc.emf.InMemoryEmfModel;
import org.eclipse.epsilon.eol.EolModule;
import org.eclipse.epsilon.eol.exceptions.EolRuntimeException;
import org.eclipse.epsilon.eol.exceptions.models.EolModelLoadingException;
import org.eclipse.epsilon.eol.models.IModel;
import org.eclipse.epsilon.eol.tools.MathTool;
import org.eclipse.epsilon.eol.types.AbstractToolNativeTypeDelegate;
import org.eclipse.epsilon.eol.types.EolClasspathNativeTypeDelegate;
import org.eclipse.epsilon.etl.EtlModule;
import org.eclipse.epsilon.evl.EvlModule;
import org.eclipse.epsilon.evl.IEvlFixer;
import org.eclipse.epsilon.evl.IEvlModule;
import org.eclipse.epsilon.evl.emf.validation.CompositeEValidator;
import org.eclipse.epsilon.evl.emf.validation.EvlValidator;
import org.eclipse.epsilon.evl.emf.validation.EvlValidator.ValidationProblemListener;
import org.eclipse.epsilon.evl.execute.UnsatisfiedConstraint;
import org.eclipse.epsilon.profiling.Profiler;
import org.eclipse.epsilon.profiling.ProfilerTargetSummary;
import org.eclipse.epsilon.profiling.ProfilingExecutionListener;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.ocl.pivot.utilities.OCL;
import org.eclipse.ocl.pivot.utilities.PivotUtil;
import org.eclipse.ocl.pivot.validation.ComposedEValidator;
import org.eclipse.ocl.xtext.completeocl.validation.CompleteOCLEObjectValidator;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.resource.XtextResource;
import org.etsi.mts.tdl.tdlPackage;
import org.etsi.mts.tdl.resources.ResourceHandler;
import org.etsi.mts.tdl.structuredobjectives.StructuredObjectivesPackage;
import org.etsi.mts.tdl.extendedconfigurations.ExtendedConfigurationsPackage;
import org.osgi.framework.Bundle;


@SuppressWarnings("unused")
public class Validator {
	
	private static final List<EPackage> packages = List.of(tdlPackage.eINSTANCE, StructuredObjectivesPackage.eINSTANCE, ExtendedConfigurationsPackage.eINSTANCE);
	
	private static void registerValidator(EPackage ePackage, EValidator validator) {
		EValidator existingValidator = EValidator.Registry.INSTANCE.getEValidator(ePackage);
		if (existingValidator instanceof CompositeEValidator) {
		    ((CompositeEValidator) existingValidator).getDelegates().add(validator);
		} else {
		    if (existingValidator == null) {
		        existingValidator = EObjectValidator.INSTANCE;
		    }
		    CompositeEValidator newValidator = new CompositeEValidator();
		    newValidator.getDelegates().add(existingValidator);
		    newValidator.getDelegates().add(validator);
		    EValidator.Registry.INSTANCE.put(ePackage, newValidator);
		}
	}

	public void validateOCL2(Resource r) {
		try {
			OCL ocl = OCL.newInstance(EPackage.Registry.INSTANCE);
			URI uri = ResourceHandler.getSourceUri(this.getClass(), "org.etsi.mts.tdl.constraints", "/ocl/tdl-constraints.ocl");
			org.eclipse.emf.common.util.URI oclURI = org.eclipse.emf.common.util.URI.createURI(uri.toString());
			Resource asResource = ocl.parse(oclURI);
			ComposedEValidator newEValidator = ComposedEValidator.install(tdlPackage.eINSTANCE);
			newEValidator.addChild(new CompleteOCLEObjectValidator(tdlPackage.eINSTANCE, oclURI));

			TDLDiagnostician diagnostician = new TDLDiagnostician();
			Diagnostic diagnostics = diagnostician.validate(r);
			System.out.println(diagnostics.getChildren().size());
			// Print the diagnostics
			if (diagnostics.getSeverity() != Diagnostic.OK) {
				String formattedDiagnostics = PivotUtil.formatDiagnostics(diagnostics, "In");
				System.out.println("Validation: "+formattedDiagnostics);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	public void validateOCL(Resource r) {
		//TODO: does not quite work yet
		ComposedEValidator oclValidator = ComposedEValidator.install(tdlPackage.eINSTANCE);
		try {
			URI uri = ResourceHandler.getSourceUri(this.getClass(), "org.etsi.mts.tdl.constraints", "/ocl/tdl-constraints.ocl");
			org.eclipse.emf.common.util.URI oclURI = org.eclipse.emf.common.util.URI.createURI(uri.toString());
			
			CompleteOCLEObjectValidator validator = new CompleteOCLEObjectValidator(tdlPackage.eINSTANCE,
					oclURI);
			// Validate the entire Resource containing the library
			//TODO: overrides other validators
			registerValidator(tdlPackage.eINSTANCE, validator);
//			EValidator.Registry.INSTANCE.put(tdlPackage.eINSTANCE, validator);

			TDLDiagnostician diagnostician = new TDLDiagnostician();
			Diagnostic diagnostics = diagnostician.validate(r);
			System.out.println(diagnostics.getChildren().size());
			// Print the diagnostics
			if (diagnostics.getSeverity() != Diagnostic.OK) {
				String formattedDiagnostics = PivotUtil.formatDiagnostics(diagnostics, "In");
				System.out.println("Validation: "+formattedDiagnostics);
				System.out.printf("Validation: %s\n", formattedDiagnostics);
//				debugPrintf ("Validation: %\n", formattedDiagnostics);
			}
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public List<UnsatisfiedConstraint> validate(Resource r) throws Exception {
		List<UnsatisfiedConstraint> violations = new ArrayList<>();
		String prefix = "tdl-generated-ETSI-ES-203-119-";
		String suffix = "-fixed.evl";
		//TODO: Change versions to published? 
		List<String> constraints = List.of(
				prefix+"1-V1.5.1"+suffix,
				prefix+"4-V1.4.1"+suffix,
				prefix+"7-V1.2.1"+suffix
				);
		try {
			//TODO: This is very important otherwise all kinds of weird problems might occur (in case it may be put in the handler as well)
			EcoreUtil.resolveAll(r);

			//TODO: Run all, maintain generated and fixed for differencing between versions and manual interventions
			//FIXED: Meta-models not resolved in plugin mode? -> fixed
			IModel tdlModel = getTDLModel(r, true, false);
			tdlModel.load();
			List<IModel> dependencies = new ArrayList<>();
			//TODO: this can cause duplicate definitions, resolveAll above makes this unnecessary
//			System.out.println("Loading validation dependencies for: "+r.getURI().path());
//			for (Resource d : r.getResourceSet().getResources()) {
//				if (d!=r) {
//					System.out.println("  Loading "+d.getURI().path());
//					IModel dModel = getTDLModel(d, true, false);
//					dModel.load();
//					dependencies.add(dModel);
//				}
//			}
//			String source = "epsilon/constraints/tdl.evl";

			for (String part : constraints) {
				String source = "epsilon/constraints/"+part;
				URI uri = ResourceHandler.getSourceUri(this.getClass(), "org.etsi.mts.tdl.constraints", source);
				
				EvlModule module = new EvlModule();
				module.parse(uri);
				
				//DONE: For some reason default classpath delegate does not work -> add custom delegate
//				module.getContext().getNativeTypeDelegates().add(new EolClasspathNativeTypeDelegate());
				module.getContext().getNativeTypeDelegates().add(new XtextBridgeNativeTypeDelegate());
				//FIXED: Defer warnings in case necessary -> not at present
//				module.getContext().setWarningStream(new PrintStream("Epsilon.log"));
				
				//TODO: integrate error reporting
				if (module.getParseProblems().size() > 0) {
					System.err.println("Parse errors occured...");
					for (ParseProblem problem : module.getParseProblems()) {
						System.err.println(problem.toString());
					}
				}
				
//				dumpConstraints(module);
				module.getContext().getModelRepository().addModel(tdlModel);
				
				//add dependencies
				module.getContext().getModelRepository().addModels(dependencies);
				
				//execute
				module.execute();
				
				EvlModule m = (EvlModule) module;
				violations.addAll(m.getContext().getUnsatisfiedConstraints());
				
				//filter only within resource
				violations = violations.stream()
					.filter(v -> ((EObject)v.getInstance()).eResource() == r)
					.collect(Collectors.toList());

//				dumpViolations(violations);
				
				//TODO: Needed? New API does not provide it
//				module.reset();
			}
			tdlModel.dispose();
			for (IModel d : dependencies) {
				d.dispose();
			}
			
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			//TODO: Error message refinements
			e.printStackTrace();
			System.err.println(e.getMessage()
					.replaceAll("\n.+epsilon/", "\n  at (epsilon/")
//					.replaceAll("\\.", "")
//					.replaceAll("evl@", "evl:")
//					.replaceAll(":(\\d+).+", ":$1)")
//					.replaceAll("-", "")
//					.replaceAll("\n.+/", "\n  at (")
					);
			throw(e);
//			e.printStackTrace();
		}
		return violations;
	}


	public void dumpViolations(List<UnsatisfiedConstraint> violations, boolean includeLocation) {
		for (UnsatisfiedConstraint constraint : violations) {
			System.out.println("  Validator: " + constraint.getMessage());
			//TODO: this can only work with XtextResources
			if (includeLocation && ((EObject) constraint.getInstance()).eResource() instanceof XtextResource) {
				ICompositeNode node = NodeModelUtils.findActualNodeFor((EObject) constraint.getInstance());
				String text = "Line "+node.getStartLine()+"-"+node.getEndLine()+ ": "+node.getText().trim();
				System.out.println("    " + text);
			} else {
				//TODO: handle other resources
			}
		}
	}

	private void dumpConstraints(EvlModule module) {
		//Dump -> extract to separate functionality
		for (var cx : module.getDeclaredConstraintContexts()) {
			System.out.println(cx);
			for (var cs : cx.getConstraints()) {
				System.out.println("  "+cs);
				System.out.println("    check:"+getSnippet(module.getFile(), cs.getCheckBlock().getBody().getRegion()));
				System.out.println("    message:"+getSnippet(module.getFile(), cs.getMessageBlock().getBody().getRegion()));
			}
		}
	}

	private String getSnippet(File file, Region region) {
		String snippet = "";
		try {
			List<String> lines = Files.readAllLines(Path.of(file.getPath()));
			lines.add(0, "");
			lines = lines.subList(region.getStart().getLine(), region.getEnd().getLine()+1);
//			System.out.println(lines);
			lines.set(lines.size()-1, lines.get(lines.size()-1).substring(0, region.getEnd().getColumn()));
			lines.set(0, lines.get(0).substring(region.getStart().getColumn()-1));
			lines = lines.stream()
					.filter(l -> !l.trim().startsWith("//"))
					.collect(Collectors.toList());
			snippet = String.join(" ", lines)
					.replaceAll("\\s+", " ")
					;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return snippet;
	}

	//TODO: extract to shared library
	public IModel getTDLModel(Resource resource, boolean read, boolean write) throws Exception {
		EmfModel model;
		//FIXED: meta-models not resolved in plugin mode? -> seems to work now
//		model = new InMemoryEmfModel(resource);
		model = new InMemoryEmfModel("TDL", resource, packages);
		model.setStoredOnDisposal(write);
		model.setReadOnLoad(read);
		model.setCachingEnabled(true);
		model.setMetamodelUris(List.of(tdlPackage.eNS_URI, StructuredObjectivesPackage.eNS_URI, ExtendedConfigurationsPackage.eNS_URI));
		return model;
	}

}
