package org.etsi.mts.tdl.to2tdl;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.etsi.mts.tdl.Comment;
import org.etsi.mts.tdl.ComponentInstance;
import org.etsi.mts.tdl.ComponentInstanceRole;
import org.etsi.mts.tdl.ComponentType;
import org.etsi.mts.tdl.CompoundBehaviour;
import org.etsi.mts.tdl.Connection;
import org.etsi.mts.tdl.DataInstanceUse;
import org.etsi.mts.tdl.GateInstance;
import org.etsi.mts.tdl.GateReference;
import org.etsi.mts.tdl.GateType;
import org.etsi.mts.tdl.LiteralValueUse;
import org.etsi.mts.tdl.Member;
import org.etsi.mts.tdl.Message;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.StructuredDataType;
import org.etsi.mts.tdl.Target;
import org.etsi.mts.tdl.TestConfiguration;
import org.etsi.mts.tdl.TestDescription;
import org.etsi.mts.tdl.tdlFactory;
import org.etsi.mts.tdl.tdlPackage;
import org.etsi.mts.tdl.helper.TDLHelper;
import org.etsi.mts.tdl.structuredobjectives.Content;
import org.etsi.mts.tdl.structuredobjectives.DataReference;
import org.etsi.mts.tdl.structuredobjectives.EntityReference;
import org.etsi.mts.tdl.structuredobjectives.EventOccurrenceSpecification;
import org.etsi.mts.tdl.structuredobjectives.LiteralValue;
import org.etsi.mts.tdl.structuredobjectives.StructuredTestObjective;
import org.etsi.mts.tdl.structuredobjectives.Value;
import org.etsi.mts.tdl.transform.AbstractTranslator;
public class TO2TDLTranslator extends AbstractTranslator {

	public void transform(Package p) {
		List<StructuredTestObjective> stos = EcoreUtil2.getAllContentsOfType(p, StructuredTestObjective.class);
		//TODO: difficult to read, unless broken into sub-packages
//		for (var sto : stos) {
//			transform(sto);
//		}

		//TODO: add validation both as pre-check option and in transformation logging
		
		//TODO: extract generic and specific functionality
		//specific can be customised for different configurations but also for differnet inputs 
		
		//broken into separate iterations for readability
		for (StructuredTestObjective sto : stos) {
			transformData(sto);
		}
		//TODO: in case split even further
		for (StructuredTestObjective sto : stos) {
			transformConfiguration(sto);
			transformBehaviour(sto);
		}
//		for (var sto : stos) {
//			transformBehaviour(sto);
//		}

	}

	private void transform(StructuredTestObjective sto) {
		//TODO: break down into packages? or rearrange later?
		System.out.println(sto.getName());
		transformData(sto);
		transformConfiguration(sto);
		transformBehaviour(sto);
	}

	private void transformBehaviour(StructuredTestObjective sto) {
		List<EntityReference> ers = EcoreUtil2.getAllContentsOfType(sto, EntityReference.class);
		String configurationName = getConfigurationName(ers);
		TestConfiguration configuration = getTypeFor(configurationName , tdlPackage.Literals.TEST_CONFIGURATION);
		
		TestDescription td = getTypeFor("TD_"+sto.getName(), tdlPackage.Literals.TEST_DESCRIPTION);
		td.setTestConfiguration(configuration);
		td.getTestObjective().add(sto);
		td.setBehaviourDescription(tdlFactory.eINSTANCE.createBehaviourDescription());
		CompoundBehaviour topLevelBehaviour = tdlFactory.eINSTANCE.createCompoundBehaviour();
		topLevelBehaviour.setBlock(tdlFactory.eINSTANCE.createBlock());
		td.getBehaviourDescription().setBehaviour(topLevelBehaviour);
		
		//TODO: for all event occurrences -> done
		//TODO: add designated blocks? especially for new syntax...
		//TODO: messages vs procedures?
		//TODO: also repeated?
		//TODO: also variants?
		//TODO: also templates?
		//TODO: direction based on verbs and/or qualifiers 
		List<EventOccurrenceSpecification> eos = EcoreUtil2.getAllContentsOfType(sto, EventOccurrenceSpecification.class);
		for (EventOccurrenceSpecification eo : eos) {
			Message message = transformMessage(eo, configuration);
			topLevelBehaviour.getBlock().getBehaviour().add(message);
		}
		//TODO: what about more abstract behaviours, e.g. establish connection, close connection, etc.
		//		-> is it the case to generate / use "keyword" based approach
		//		   where TDs/functions are generated / referenced for each event
		//		   -> an option can then inline the referenced functions (or even keep them separate?)
		//		   -> using sub-configurations with mappings can add further indirection/flexibility but may be too much overhead
		//		      -> the main configuration can then even be composed from the sub configurations using part 7 (even more overhead) 
		//		   -> a construct may be needed to link an event to a TD
		//		   -> alternatively annotations can be used, but a structured approach can be useful for traceability as well
		//TODO: what about negative behaviours? e.g. not closes / does not close?

	}

	private Message transformMessage(EventOccurrenceSpecification eo, TestConfiguration configuration) {
		Message message = tdlFactory.eINSTANCE.createMessage();
		
		//TODO: resolve gate references properly -> done
		//TODO: check event type to determine direction
		ComponentInstance source = getContentWithName(eo.getEntityReference().getEntity().getName(), configuration, ComponentInstance.class).get();
		ComponentInstance target;
		if (!eo.getOppositeEntityReference().isEmpty()) {
			//TODO: handle multiple targets?
			target = getContentWithName(eo.getOppositeEntityReference().get(0).getEntity().getName(), configuration, ComponentInstance.class).get();
		} else {
			target = getContentWithName(source.getName()+"_Tester", configuration, ComponentInstance.class).get();
		}
		
		Connection connection = getConnectionBetween(source, target, configuration).get();
		
		System.out.println("Connection between: "+source.getName() + " and "+target.getName());
		//TODO: check order of gate references -> done
		GateReference sg;
		GateReference tg;
		if (connection.getEndPoint().get(0).getComponent()==source) {
			sg = connection.getEndPoint().get(0);
			tg = connection.getEndPoint().get(1);
		} else {
			sg = connection.getEndPoint().get(1);
			tg = connection.getEndPoint().get(0);
		}

		
		System.out.println("  -> Interaction between: "+sg.getComponent().getName() + " and "+tg.getComponent().getName());
		System.out.println("     For event: " + NodeModelUtils.getNode(eo).getText());

		//TODO: remove default gates once issue has been solved
		//sg = configuration.getConnection().get(0).getEndPoint().get(0);
		//tg = configuration.getConnection().get(0).getEndPoint().get(1);

		message.setSourceGate(sg);

		Target targets = tdlFactory.eINSTANCE.createTarget();
		message.getTarget().add(targets);
		targets.setTargetGate(tg);

		if (eo.getEventReference().getEvent().getName().contains("receiv")) {
			message.setSourceGate(tg);
			targets.setTargetGate(sg);
		}
		
		//TODO: resolve arguments properly
		LiteralValueUse du = tdlFactory.eINSTANCE.createLiteralValueUse();
		if (eo.getEventArgument()!=null) {
			du.setValue("\""+NodeModelUtils.getNode(eo.getEventArgument()).getText().replaceAll("\"", "\\\\\"")+"\"");
		} else {
			du.setValue("\"TODO: Empty argument?\"");
		}
		message.setArgument(du);
		
		//TODO: for debugging
		Comment debug = tdlFactory.eINSTANCE.createComment();
		debug.setBody("\"Event: "+NodeModelUtils.getNode(eo).getText().replaceAll("\"", "\\\\\"")+"\"");
		message.getComment().add(debug);
		
		return message;
	}

	private void transformData(StructuredTestObjective sto) {
		List<EventOccurrenceSpecification> eos = EcoreUtil2.getAllContentsOfType(sto, EventOccurrenceSpecification.class);
		for (EventOccurrenceSpecification eo : eos) {
			if (eo.getEventArgument() instanceof LiteralValue) {
				transform((LiteralValue) eo.getEventArgument());
			}
			//TODO: handle predefined values and other arguments
		}
	}

	private void transformConfiguration(StructuredTestObjective sto) {
		//TODO: infer from all participating entities in the TP
		//		collect all participating entities for the TP
		//		generate name based on sorted entities
		//		infer connections based on interactions (bi-directional)
		//		assumptions:
		//			- one default gate per component
		//			- if no opposite entity, assumed tester (or other entity if only two entities)
		//			- for every event occurrence check if a connection exists and create otherwise

		List<EventOccurrenceSpecification> eos = EcoreUtil2.getAllContentsOfType(sto, EventOccurrenceSpecification.class);
		boolean oldWay = false;
		if (oldWay) {
			for (EventOccurrenceSpecification eo : eos) {
				//TODO: summarise workflow for all steps
				transform(eo.getEntityReference());
				if (!eo.getOppositeEntityReference().isEmpty()) {
					//TODO: handle opposite entity references
					for (EntityReference oe : eo.getOppositeEntityReference()) {
						transform(oe);
					}
				} else {
					//TODO: consider using the same type instead!?
					transform(eo.getEntityReference(), "_Tester");
				}
				//TODO: create configuration
				transformConfiguration(eo);
			}
		}
		//Component and Gate Types
		List<EntityReference> ers = EcoreUtil2.getAllContentsOfType(sto, EntityReference.class);
		for (EntityReference er : ers) {
			transform(er);
		}

		//Test Configuration
		String configurationName = getConfigurationName(ers);
		TestConfiguration configuration = getTypeFor(configurationName , tdlPackage.Literals.TEST_CONFIGURATION);

		//Component Instances
		for (EntityReference er : ers) {
			transformComponentInstance(configuration, er);
		}		

		//Connections and Implicit Testers?
		for (EventOccurrenceSpecification eo : eos) {
			ComponentInstance i = getContentWithName(eo.getEntityReference().getEntity().getName(), configuration, ComponentInstance.class).get();
			if (!eo.getOppositeEntityReference().isEmpty()) {
				//Opposite entity references
				for (EntityReference oe : eo.getOppositeEntityReference()) {
					ComponentInstance opposite = getContentWithName(oe.getEntity().getName(), configuration, ComponentInstance.class).get();
					transformConnection(configuration, i, opposite);
				}
			} else {
				//TODO: Implicit tester?
				//TODO: check also type of event
				transformComponentInstance(configuration, eo.getEntityReference(), "_Tester");
				ComponentInstance opposite = getContentWithName(eo.getEntityReference().getEntity().getName()+"_Tester", configuration, ComponentInstance.class).get();
				transformConnection(configuration, i, opposite);
			}
		}
		
		//TODO: for debugging
		String body = "\"See "+sto.getName()+"\"";
		Comment source = tdlFactory.eINSTANCE.createComment();
		source.setBody(body);
		configuration.getComment().add(source);
	}

	private String getConfigurationName(List<EntityReference> ers) {
		Set<String> participatingEntityNames = ers.stream()
			.map(e->e.getEntity().getName())
			.sorted()
			.collect(Collectors.toSet());
		String configurationName = "TC_"+String.join("_", participatingEntityNames);
		return configurationName;
	}

	private void transformComponentInstance(TestConfiguration configuration, EntityReference er) {
		transformComponentInstance(configuration, er, "");
	}
	
	private void transformComponentInstance(TestConfiguration configuration, EntityReference er, String suffix) {
		String componentTypeName = er.getEntity().getName()+"_Type";
		String instanceName = er.getEntity().getName()+suffix;

		ComponentType componentType = getTypeFor(componentTypeName, tdlPackage.Literals.COMPONENT_TYPE);		

		Optional<ComponentInstance> optional = getContentWithName(instanceName, configuration, ComponentInstance.class);
		ComponentInstance i;
		if (optional.isPresent()) {
			i = optional.get();
		} else {
			i = (ComponentInstance) tdlFactory.eINSTANCE.create(tdlPackage.Literals.COMPONENT_INSTANCE);
			i.setName(instanceName);
			i.setType(componentType);
			//TODO: check entity type to assign role
			//TODO: alternatively based on naming or other criteria?
			//TODO: log assumptions
			//name-based
			if (instanceName.equals("IUT") || instanceName.equals("SUT")) {
				i.setRole(ComponentInstanceRole.SUT);
			} else {
				i.setRole(ComponentInstanceRole.TESTER);
			}
			if (instanceName.contains("Tester")) {
				i.setRole(ComponentInstanceRole.TESTER);
			}
			configuration.getComponentInstance().add(i);
		}
	}
	
	private void transformConfiguration(EventOccurrenceSpecification eo) {
		String componentTypeName = eo.getEntityReference().getEntity().getName()+"_Type";
		if (!eo.getOppositeEntityReference().isEmpty()) {
			//TODO: handle opposite entity references
			for (EntityReference oe : eo.getOppositeEntityReference()) {
				//transform(oe);
			}
		} else {
			String configurationName = componentTypeName + "_" + componentTypeName + "_Tester";
			TestConfiguration configuration = getTypeFor(configurationName , tdlPackage.Literals.TEST_CONFIGURATION);
			
			transformComponentInstance(configuration, eo.getEntityReference());
			transformComponentInstance(configuration, eo.getEntityReference(), "_Tester");

			ComponentInstance i = getContentWithName(eo.getEntityReference().getEntity().getName(), configuration, ComponentInstance.class).get();
			ComponentInstance tester = getContentWithName(eo.getEntityReference().getEntity().getName()+"_Tester", configuration, ComponentInstance.class).get();
			tester.setRole(ComponentInstanceRole.TESTER);
			
			transformConnection(configuration, i, tester);
		}
	}

	private void transformConnection(TestConfiguration configuration, ComponentInstance i, ComponentInstance ti) {
		//TODO: more gates?
		Optional<Connection> optionalConnection = getConnectionBetween(i, ti, configuration);
		Connection c;
		if (optionalConnection.isPresent()) {
			c = optionalConnection.get();
		} else {
			c = (Connection) tdlFactory.eINSTANCE.create(tdlPackage.Literals.CONNECTION);
			GateReference gr1 = (GateReference) tdlFactory.eINSTANCE.create(tdlPackage.Literals.GATE_REFERENCE);
			gr1.setComponent(i);
			gr1.setGate(i.getType().getGateInstance().get(0));
			GateReference gr2 = (GateReference) tdlFactory.eINSTANCE.create(tdlPackage.Literals.GATE_REFERENCE);
			gr2.setComponent(ti);
			gr2.setGate(ti.getType().getGateInstance().get(0));
			c.getEndPoint().add(gr1);
			c.getEndPoint().add(gr2);
			configuration.getConnection().add(c);
		}
	}

	private Optional<Connection> getConnectionBetween(ComponentInstance i, ComponentInstance ti, TestConfiguration configuration) {
		Optional<Connection> optionalConnection = getContentWithPredicate(
				e -> 
					(e.getEndPoint().get(0).getComponent().equals(i) && 
					e.getEndPoint().get(1).getComponent().equals(ti)) ||
					(e.getEndPoint().get(1).getComponent().equals(i) && 
					e.getEndPoint().get(0).getComponent().equals(ti))
					//TODO: check gates too?
			, configuration, Connection.class);
		return optionalConnection;
	}

	private void transform(EntityReference entityReference) {
		transform(entityReference, "");
	}
	
	private void transform(EntityReference entityReference, String suffix) {
		// TODO Auto-generated method stub
		String componentTypeName = entityReference.getEntity().getName()+"_Type"+suffix;
		String gateTypeName = entityReference.getEntity().getName()+"_GateType";
		//TODO: this is not necessarily type safe, return depends on assignment

		//TODO: extract?
		GateType gateType = getTypeFor(gateTypeName, tdlPackage.Literals.GATE_TYPE);
		EventOccurrenceSpecification eo = (EventOccurrenceSpecification) entityReference.container();
		Value a = eo.getEventArgument();
		if (a instanceof LiteralValue) {
			String name = getQualifiedName((LiteralValue)a);
			StructuredDataType generatedType = getStructuredDataTypeFor(name);
			gateType.getDataType().add(generatedType);
		}
		
		ComponentType componentType = getTypeFor(componentTypeName, tdlPackage.Literals.COMPONENT_TYPE);
		//TODO: at least one gate
		String gateName = "g";
		Optional<GateInstance> optional = componentType.getGateInstance().stream()
				.filter(e -> e.getName().equals(gateName))
				.findFirst();
		GateInstance generatedGate;
		if (optional.isPresent()) {
			generatedGate = (GateInstance) optional.get();
		} else {
			generatedGate = tdlFactory.eINSTANCE.createGateInstance();
			generatedGate.setName(gateName);
			generatedGate.setType(gateType);
			componentType.getGateInstance().add(generatedGate);
		}
	}



	private void transform(LiteralValue lv) {
		System.out.println("    "+NodeModelUtils.findActualNodeFor(lv).getText());
		String name = getQualifiedName(lv);
		StructuredDataType generatedType = getStructuredDataTypeFor(name);
		transformMembers(generatedType, lv.getContent());
	}

	private String getQualifiedName(LiteralValue lv) {
		String name = lv.getName();
		if (useQualifiers) {
			List<String> bodies = lv.getComment().stream()
					.map(e->e.getBody())
					.filter(e->!e.equals("the") && !e.equals("a"))
					.collect(Collectors.toList());
			if (!bodies.isEmpty()) {
				name = String.join("_", bodies)+"_"+name;
			}
		}
		return name;
	}

	private void transformMembers(StructuredDataType generatedType, EList<Content> allContent) {
		List<String> memberNames = generatedType.getMember().stream()
				.map(e -> e.getName())
				.collect(Collectors.toList());

		//TODO: warn if members exist but have different types
		//TODO: without filter, effectively union
		List<Content> content = allContent.stream()
//				.filter(e->!memberNames.contains(e.getName()))
				.filter(e->!e.getName().equals("omit")) //TODO: why the filter?
				.collect(Collectors.toList());

		for (Content c : content) {
			Member m;
			if (memberNames.contains(c.getName())) {
				m = generatedType.getMember().get(memberNames.indexOf(c.getName())); 
			} else { 
				m = tdlFactory.eINSTANCE.createMember();
				m.setName(c.getName());
			}
			//TODO: check types
			if (!c.getContent().isEmpty()) {
				m.setDataType(transform(generatedType, c));
			} else if (c.getValue() instanceof LiteralValue) {
				if (((LiteralValue) c.getValue()).getName().equals("B")) {
					m.setDataType(getSimpleDataTypeFor("BITSTRING"));
				} else {
					m.setDataType(stringType);;
				}
				//TODO: structured types as well?
			} else if (c.getValue() instanceof DataReference) {
				if (((DataReference) c.getValue()).getContent() instanceof DataInstanceUse) {
					DataInstanceUse dataInstanceUse = (DataInstanceUse)((DataReference) c.getValue()).getContent();
//					System.out.println(dataInstanceUse.getDataInstance().getName()+":"+dataInstanceUse.getDataInstance().getDataType());
					//m.setDataType(getSimpleDataTypeFor("TODO_RESOLVE_TYPE_FOR_"+dataInstanceUse.getDataInstance().getName()));;
					m.setDataType(dataInstanceUse.getDataInstance().getDataType());
				} else {
					m.setDataType(referencedType);;
				}
			} else {
				//otherwise default to string
				m.setDataType(stringType);;
			}
			generatedType.getMember().add(m);
		}
	}

	private StructuredDataType transform(StructuredDataType containerType, Content c) {
//		StructuredDataType generatedType = getStructuredDataTypeFor(c.container().getName()+"_"+c.getName());
		StructuredDataType generatedType = getStructuredDataTypeFor(containerType.getName()+"_"+c.getName());
		transformMembers(generatedType, c.getContent());
		return generatedType;
	}

	@Override
	public void translate(String filename) throws Exception {
		Resource sr = TDLHelper.load(filename);
		Resource tr = TDLHelper.create(filename+"_generated.tdltx");
		Package p = (Package) sr.getContents().get(0);
		setTargetResource(tr);
		initTargetResource("generated_from_"+p.getName());
		addImports(p);
		transform(p);
		TDLHelper.store(tr, false);
	}
}
