package org.etsi.mts.tdl.standalone;

import de.ugoe.cs.swe.TTCN3StandaloneSetup;
import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.epsilon.evl.execute.UnsatisfiedConstraint;
import org.eclipse.ocl.pivot.ExpressionInOCL;
import org.eclipse.ocl.pivot.utilities.OCL;
import org.eclipse.ocl.pivot.utilities.ParserException;
import org.eclipse.ocl.pivot.utilities.Query;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.etsi.mts.tdl.ComponentInstance;
import org.etsi.mts.tdl.ComponentInstanceRole;
import org.etsi.mts.tdl.Extension;
import org.etsi.mts.tdl.GateType;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.Behaviour;
import org.etsi.mts.tdl.CompoundBehaviour;
import org.etsi.mts.tdl.BoundedLoopBehaviour;
import org.etsi.mts.tdl.AlternativeBehaviour;
import org.etsi.mts.tdl.ConditionalBehaviour;
import org.etsi.mts.tdl.ProcedureCall;
import org.etsi.mts.tdl.Interaction;
import org.etsi.mts.tdl.Message;
import org.etsi.mts.tdl.StructuredDataType;
import org.etsi.mts.tdl.TestConfiguration;
import org.etsi.mts.tdl.TestDescription;
import org.etsi.mts.tdl.tdlPackage;
import org.etsi.mts.tdl.asn2tdl.ASN2TDLTranslator;
import org.etsi.mts.tdl.constraints.evl.Validator;
import org.etsi.mts.tdl.helper.TDLHelper;
import org.etsi.mts.tdl.impl.CompoundBehaviourImpl;
import org.etsi.mts.tdl.json2tdl.JSON2TDLTranslator;
import org.etsi.mts.tdl.json2tdl.TDL2JSONTranslator;
import org.etsi.mts.tdl.openapi2tdl.next.OpenAPI2TDLTranslatorNext;
import org.etsi.mts.tdl.openapi2tdl.next.doc.Doc;
import org.etsi.mts.tdl.tools.to.docx.poi.Generator;
import org.etsi.mts.tdl.transform.AbstractTranslator;
import org.etsi.mts.tdl.ttcn3.Transform;

public class Standalone {
	static String sourceExtension = "tdltx";
	static String targetExtension = "tdltx";
	static String openapiExtension = "yaml";
	static String asnExtension = "asn";
	static String jsonExtension = "json";
	private List<MODE> modes = new ArrayList<>();
	private String path = "";
	private boolean recursive = true;

	enum MODE {
		all, 
		list, debug, evaluateOCL, 
		validateOCL, validate, 
		translate, exportDoc, exportJSON,
		importOpenAPI, importASN1, importJSON,
		openAPIDoc,exportTTCN3,generateDiagrams
	}
	
	public static void main(String[] args) throws Exception {
		Standalone app = new Standalone();
		//TODO: expose as arguments
		
//		app.modes.add(MODE.importJSON);
//		app.modes.add(MODE.exportJSON);
		
		app.path = "examples/openapi/reqres.yaml";
//		app.modes.add(MODE.importOpenAPI);
//		app.modes.add(MODE.list);
//		app.path = "examples/json/model.json";
		app.path = "examples/validation/Naming.tdltx";
		app.path = "examples/validation/Example.tdltx";
		
		if (args.length < 1) {
			System.out.println("Usage: standalone [path] [modes]\n"
					+"  path\tPath to the source(s) to process\n"
					+"  modes\tOne or more modes to apply\n"
					+"Modes supported: "+Arrays.asList(MODE.values()));
			return;
		}
		app.path = args[0];

		if (args.length == 2) {
			for (String a : args[1].split(",")) {
				try {
					app.modes.add(MODE.valueOf(MODE.class, a));
				} catch (Exception e) {
					System.err.println("ERROR: "+e.getLocalizedMessage());
				}
			}
		} else {
			System.err.println("ERROR: No mode selected!");
			return;
		}
		
		app.recursive = false;
//		app.modes.add(MODE.list);
//		app.modes.add(MODE.validate);
//		app.modes.add(MODE.exportDoc);
		
//		app.modes.add(MODE.exportTTCN3);
//		targetExtension = "tdl";
//		app.modes.add(MODE.translate);
		
//		app.path = "examples/validation/Naming.tdltx.tdl";
//		app.modes.add(MODE.validateOCL);
		
		if (app.selected(MODE.list)) {
			String path = pathOrDefault(app.path, "examples/basics");
//			app.processElements(path, app::listElements);
			app.processElements(path, app::listElements);
		}
		
		//TODO: may fail
		//TODO: may fail due to literal value use without data type
		if (app.selected(MODE.validate)) {
			String path = pathOrDefault(app.path, "examples/basics");
			app.processElements(path, app::validate);
		}

		//TODO: may fail
		if (app.selected(MODE.translate)) {
			String path = pathOrDefault(app.path, "examples/basics");
			app.processElements(path, app::translate);
		}
		
		if (app.selected(MODE.importJSON)) {
			String path = pathOrDefault(app.path, "examples/json");
			app.processElements(path, jsonExtension, app::importJSON);
		}

		if (app.selected(MODE.exportJSON)) {
			String path = pathOrDefault(app.path, "examples/json/model.json-generated.tdltx");
//			path = "examples/json/e-line-examples-c-vlan-bridge.json-generated.tdltx";
			app.processElements(path, jsonExtension, app::exportJSON);
		}

		
		if (app.selected(MODE.exportDoc)) {
			String path = pathOrDefault(app.path, "examples/validation/Example.tdltx");
			app.processElements(path, app::exportDoc);
		}

		if (app.selected(MODE.exportTTCN3)) {
			String path = pathOrDefault(app.path, "examples/validation/Example.tdltx");
			app.processElements(path, app::exportTTCN3);
		}

		if (app.selected(MODE.validateOCL)) {
			String path = pathOrDefault(app.path, "examples/validation/Example.tdltx");
			app.processElements(path, app::validateOCL);
		}
		
		//TODO: can only be used in isolation as it interferes with OCL delegates afterwards..
		if (app.selected(MODE.evaluateOCL)) {
			String path = pathOrDefault(app.path, "examples/validation/Example.tdltx");
			app.processElements(path, app::evaluateOCLInline);
		}

		if (app.selected(MODE.importOpenAPI)) {
			String path = pathOrDefault(app.path, "examples/openapi");
			app.processElements(path, openapiExtension, app::importOpenAPI);
			//NOTE: keep in mind weird errors if legacy OpenAPI importer is imported as well in manifest (especially if it is first)
		}

		if (app.selected(MODE.openAPIDoc)) {
			String path = pathOrDefault(app.path, "examples/openapi");
			app.processElements(path, openapiExtension, app::documentOpenAPI);
			//NOTE: keep in mind weird errors if legacy OpenAPI importer is imported as well in manifest (especially if it is first)
		}

		if (app.selected(MODE.importASN1)) {
			String path = pathOrDefault(app.path, "examples/asn1");
			app.processElements(path, asnExtension, app::importASN1);
		}

		if (app.selected(MODE.generateDiagrams)) {
			String path = pathOrDefault(app.path, "examples/validation/Example.tdltx");
			app.processElements(path, asnExtension, app::generateDiagrams);
		}

		
		if (app.selected(MODE.debug)) {
			//individual file without dependencies
			app.processElements("examples/basics/Sample.tdltx", app::listElements);

			//individual file with missing dependencies
			app.processElements("examples/basics/ImportSample.tdltx", app::listElements);
			//validation will fail due to unresolved items
		}		
		
		//TODO: minimal example for TO translation
		//TODO: minimal example for TTCN-3 generation
		//TODO: clean up keywords automatically
	}

	private static String pathOrDefault(String path, String defaultPath) {
		return path.isEmpty() ? defaultPath : path;
	}

	private boolean selected(MODE m) {
		return modes.contains(MODE.all) || modes.contains(m);
	}

	private void processElements(String path, Consumer<String> operation) throws Exception {
		TDLHelper.resetResourceSet();
		processElements(path, sourceExtension, operation);
	}
	
	private void processElements(String path, String extension, Consumer<String> operation) throws Exception {
		if (recursive) {
			processElementsRecursive(path, extension, operation);
		} else {
			processElementsFlat(path, extension, operation);
		}
	}

	private void processElementsRecursive(String path, String extension, Consumer<String> operation) throws Exception {
		System.out.println("Processing recursively: "+path);
		if (extension.contains("tdl")) {
			Files.walk(Path.of(path))
				.filter(e->e.getFileName().toString().endsWith(extension))
				.forEach(e->TDLHelper.load(e.toAbsolutePath().toString()));
			TDLHelper.link();
			TDLHelper.check();
		}
		Files.walk(Path.of(path))
				.filter(e->e.getFileName().toString().endsWith(extension))
				.forEach(e->operation.accept(e.toAbsolutePath().toString()));
	}

	private void processElementsFlat(String path, String extension, Consumer<String> operation) throws Exception {
		File target = new File(path);
		//TODO: make recursive?
		if (target.isDirectory()) {
			FilenameFilter filter = (FilenameFilter) (dir, name) -> name.endsWith(extension);
			File[] files = target.listFiles(filter);
			Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
			//load all resources first
			if (extension.contains("tdl")) {
				for (File f : files) {
					System.out.println("Loading: "+f.getAbsolutePath());
					TDLHelper.load(f.getAbsolutePath());
				}
				TDLHelper.link();
				TDLHelper.check();
			}
			for (File f : files) {
				System.out.println("Processing: "+f.getAbsolutePath());
				operation.accept(f.getAbsolutePath());
			}
		} else {
			if (extension.contains("tdl")) {
				System.out.println("Loading: "+target.getAbsolutePath());
				TDLHelper.load(target.getAbsolutePath());
				TDLHelper.link();
				TDLHelper.check();
			}
			System.out.println("Processing: "+target.getAbsolutePath());
			operation.accept(target.getAbsolutePath());
		}
	}

	private void listElements(String path) {
		Resource resource = TDLHelper.load(path);
		Package p = (Package) resource.getContents().get(0);
		System.out.println("Package: "+p.getName());
		p.getNestedPackage().forEach(e -> {
			System.out.println("  " + e.getName());
			System.out.println("    " + e.eClass().getName() + " : " + e.getQualifiedName());
		});
		p.getPackagedElement().forEach(e -> {
			System.out.println("  " + e.getName());
			System.out.println("    " + e.eClass().getName() + " : " + e.getQualifiedName());
			if (e instanceof StructuredDataType) {
				EList<Extension> extension = ((StructuredDataType) e).getExtension();
				if (!extension.isEmpty()) {
					System.out.println("    Extends: " + extension.stream()
						.map(x-> x.getExtending().getName())
						.collect(Collectors.joining(", "))
					);
				}
			}
		});
	}
	
	private void validate(String path) {
		Resource resource = TDLHelper.load(path);
		Validator validator = new Validator();
		//TODO: make robust against unresolved imports
		//TODO: report parsing / resolution errors
		try {
			List<UnsatisfiedConstraint> violations = validator.validate(resource);
			validator.dumpViolations(violations, true);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void validateOCL(String path) {
		Resource resource = TDLHelper.load(path);
		Validator validator = new Validator();
		validator.validateOCL2(resource);
	}
	
	private void evaluateOCLInline(String path) {
		Resource resource = TDLHelper.load(path);
		OCL ocl = OCL.newInstance(TDLHelper.getResourceSet());
		EClass contextEClass = tdlPackage.Literals.GATE_TYPE;
		try {
			ExpressionInOCL query = ocl.createQuery(contextEClass,
					"self.allDataTypes()"
					);
			Query queryEval = ocl.createQuery(query);
			for (var o : EcoreUtil2.getAllContentsOfType(resource.getContents().get(0), GateType.class)) {
				System.out.println(o.allDataTypes());
				System.out.println(queryEval.evaluateUnboxed(o));
			}
		} catch (ParserException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	private void translate(String path) {
		Resource resource = TDLHelper.load(path);
		try {
			Resource target = TDLHelper.create(resource.getURI().path() + "." + targetExtension);
			target.getContents().addAll(EcoreUtil.copyAll(resource.getContents()));
			TDLHelper.store(target, true);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void importData(String path, AbstractTranslator translator) {
		File source = new File(path);
		try {
			translator.setTargetResource(TDLHelper.create(source.getAbsolutePath()+"-generated."+targetExtension));
			translator.initTargetResource(translator.cleanName(source.getName()));
			translator.translate(source.getAbsolutePath());
			TDLHelper.store(translator.getTargetResource(), true);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	private void importOpenAPI(String path) {
		importData(path, new OpenAPI2TDLTranslatorNext());
	}

	private void documentOpenAPI(String path) {
		Doc doc = new Doc();
		try {
			//TODO: make configurable
			doc.processModel(path, true);
			Files.writeString(Path.of(path+"-RQ-ICS-TSS.md"), doc.getContent());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void generateDiagrams(String path) {
		Resource resource = TDLHelper.load(path);
		//export to external generator
		EObject model = resource.getContents().get(0);
		generateConfiguration(path, model);
        System.out.println();
        generateBehaviour(path, model);

		/*
		 *  @startuml
            component Tester {
            portout p1c
            portout p2c
            portout p3c
            }

            component SUT {
            portin p1d
            portin p2d
            portin p3d
            }
            p1c <--> p1d
            p2c <--> p2d
            p3c <--> p3d
            @enduml

		 */
	}

    private void generateConfiguration(String path, EObject model) {
        List<TestConfiguration> testConfigurations = EcoreUtil2.getAllContentsOfType(model, TestConfiguration.class);
		String d = "@startuml\n";
		for (TestConfiguration tc : testConfigurations) {
			d += "rectangle \"Test Configuration\\n"+tc.getName()+"\" {\n";
			for (var i : tc.getComponentInstance()) {
				if (i.getRole() == ComponentInstanceRole.TESTER) {
					d += processComponentInstance(i);
				}
			}
			for (var i : tc.getComponentInstance()) {
				if (i.getRole() == ComponentInstanceRole.SUT) {
					d += processComponentInstance(i);
				}
			}
			for (var i : tc.getConnection()) {
				var e0 = i.getEndPoint().get(0);
				var e1 = i.getEndPoint().get(1);
				if (e0.getComponent().getRole().equals(ComponentInstanceRole.SUT)) {
					var e = e1;
					e1 = e0;
					e0 = e;
				}
				//TODO: ports need to be configuration prefixed if only one diagram
				String c = ""+e0.getComponent().getName()+"_"+e0.getGate().getName()+""
						+" <--> "
						+""+e1.getComponent().getName()+"_"+e1.getGate().getName()+""
						+"\n";
				d+=c;
			}
			d +="\n}\n";
		}
		d += "@enduml";
		System.out.println(d);
		try {
			//TODO: make configurable
			Files.writeString(Path.of(path+"-config.md"), d);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }

    private void generateBehaviour(String path, EObject model) {
        List<TestDescription> testDescriptions = EcoreUtil2.getAllContentsOfType(model, TestDescription.class);

        String d = "";
		for (TestDescription td : testDescriptions) {
			d += "@startuml\n";
            d += "hide footbox\n";
            d += "title "+td.getName()+"\n";
            TestConfiguration tc = td.getTestConfiguration();
			for (var i : tc.getComponentInstance()) {
				if (i.getRole() == ComponentInstanceRole.TESTER) {
                    d+="participant "+tc.getName()+"_"+i.getName()+" [\n  == "
                    +i.getRole().toString()
                    +"\n  ----\n  \"\""
                    +i.getName()
                    +" : "
                    +i.getType().getName()
                    +"\"\"\n]\n\n";
				}
			}
			for (var i : tc.getComponentInstance()) {
				if (i.getRole() == ComponentInstanceRole.SUT) {
                    d+="participant "+tc.getName()+"_"+i.getName()+" [\n  == "
                    +i.getRole().toString()
                    +"\n  ----\n  \"\""
                    +i.getName()
                    +" : "
                    +i.getType().getName()
                    +"\"\"\n]\n\n";
				}
			}
            
            if (td.getBehaviourDescription()!= null) {
                var b = td.getBehaviourDescription().getBehaviour();
				d += processBehaviour(b);
            }

            d += "@enduml\n";
            //TODO: need to split behaviours..
		}
		System.out.println(d);
		try {
			//TODO: make configurable
			Files.writeString(Path.of(path+"-behaviour.md"), d);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }

    private <T> T castObject(Class<T> clazz, Object object) {
        return (T) object;
    }

    private String processBehaviour(Behaviour b) {
    	String d = "' Not supported yet: "+b.getClass().getName();
    	switch (b.eClass().getClassifierID()) {
		case tdlPackage.COMPOUND_BEHAVIOUR:
			d = processBehaviour((CompoundBehaviour)b);
			break;
		case tdlPackage.MESSAGE:
			d = processBehaviour((Message)b);
			break;
		default:
			break;
		}
        return d;
    }

    private String processBehaviour(CompoundBehaviour b) {
        String d = "";
        for (var nested : b.getBlock().getBehaviour()) {
            d += processBehaviour(nested);
        }
        //TODO: handle prefixes and indentation?
        return d;
    }

    private String processBehaviour(Message b) {
        var sc = b.getSourceGate().getComponent();
        var c = sc.container().getName()+"_"+sc.getName();
        var tc = b.getTarget().get(0).getTargetGate().getComponent();
        var t = tc.container().getName()+"_"+tc.getName();
        var a = b.getArgument();
        String d = "";
        String at = NodeModelUtils.findActualNodeFor(a).getText();
        //TODO: use node model untils to get text (needs processing)
        d += c+" -> "+t+" : "+at.replaceAll("\n", "\\n")+"\n";
        return d;
    }


	private String processComponentInstance(ComponentInstance i) {
		String c = "component \"<<"+i.getRole().getName()+">>\\n"+i.getName()+":"+i.getType().getName()+"\" { \n";
		String portType = "portout";
		if (i.getRole().equals(ComponentInstanceRole.SUT)) {
			portType = "portin";
		}
		for (var g : i.getType().getGateInstance()) {
			c+="  "+portType+" \""+g.getName()+":"+g.getType().getName()+"\" as "+i.getName()+"_"+g.getName()+"\n";
		}
		c+="\n}\n";
		return c;
	}

	
	private void importASN1(String path) {
		importData(path, new ASN2TDLTranslator());
	}

	private void importJSON(String path) {
		importData(path, new JSON2TDLTranslator());
	}

	private void exportJSON(String path) {
		Resource resource = TDLHelper.load(path);
		TDL2JSONTranslator translator = new TDL2JSONTranslator();
		translator.transform((Package) resource.getContents().get(0));
	}

	private void exportDoc(String path) {
		Resource resource = TDLHelper.load(path);
		Generator generator = new Generator();
		String target = path+".docx";
		try {
			generator.generate(
					resource, target, 
					"Generated from "+resource.getURI().lastSegment(), 
					"TO_4_TABLE_TEMPLATE_EDITHELP"
					);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void exportTTCN3(String path) {
		try {
			new TTCN3StandaloneSetup().createInjectorAndDoEMFRegistration();
			Resource resource = TDLHelper.load(path);
			Transform transformer = new Transform();
			Resource ir = TDLHelper.create(path+"-generated.ttcn3m");
			transformer.transform(resource, ir);
			//This is important otherwise ghost references may occur
			ir.unload();
			Resource tr = TDLHelper.create(path+"-generated.ttcn3");
			transformer.transform(resource, tr);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	
//	private void importOpenAPIAll(String path) {
//		File target = new File(path);
//		if (target.isDirectory()) {
//			FilenameFilter filter = (FilenameFilter) (dir, name) -> name.endsWith(sourceExtension);
//			File[] files = target.listFiles(filter);
//			Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
//			for (File f : files) {
//				importOpenAPISingle(f.getAbsolutePath());
//			}
//		} else {
//			importOpenAPISingle(target.getAbsolutePath());
//		}
//	}

}
