package org.etsi.mts.tdl.asn2tdl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

import org.etsi.mts.tdl.Annotation;
import org.etsi.mts.tdl.AnnotationType;
import org.etsi.mts.tdl.CollectionDataType;
import org.etsi.mts.tdl.Constraint;
import org.etsi.mts.tdl.ConstraintType;
import org.etsi.mts.tdl.DataElementMapping;
import org.etsi.mts.tdl.DataType;
import org.etsi.mts.tdl.EnumDataType;
import org.etsi.mts.tdl.Member;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.SimpleDataInstance;
import org.etsi.mts.tdl.StructuredDataType;
import org.etsi.mts.tdl.tdlFactory;
import org.etsi.mts.tdl.tdlPackage;
import org.etsi.mts.tdl.transform.AbstractTranslator;

import com.beanit.asn1bean.compiler.model.AsnAny;
import com.beanit.asn1bean.compiler.model.AsnBitString;
import com.beanit.asn1bean.compiler.model.AsnBoolean;
import com.beanit.asn1bean.compiler.model.AsnCharacterString;
import com.beanit.asn1bean.compiler.model.AsnChoice;
import com.beanit.asn1bean.compiler.model.AsnDefinedType;
import com.beanit.asn1bean.compiler.model.AsnElementType;
import com.beanit.asn1bean.compiler.model.AsnEnum;
import com.beanit.asn1bean.compiler.model.AsnInteger;
import com.beanit.asn1bean.compiler.model.AsnModel;
import com.beanit.asn1bean.compiler.model.AsnModule;
import com.beanit.asn1bean.compiler.model.AsnNamedNumber;
import com.beanit.asn1bean.compiler.model.AsnNull;
import com.beanit.asn1bean.compiler.model.AsnObjectIdentifier;
import com.beanit.asn1bean.compiler.model.AsnOctetString;
import com.beanit.asn1bean.compiler.model.AsnParameter;
import com.beanit.asn1bean.compiler.model.AsnSelectionType;
import com.beanit.asn1bean.compiler.model.AsnSequenceOf;
import com.beanit.asn1bean.compiler.model.AsnSequenceSet;
import com.beanit.asn1bean.compiler.model.AsnTaggedType;
import com.beanit.asn1bean.compiler.model.AsnType;
import com.beanit.asn1bean.compiler.parser.ASNLexer;
import com.beanit.asn1bean.compiler.parser.ASNParser;

import antlr.debug.MessageEvent;
import antlr.debug.MessageListener;
import antlr.debug.ParserListener;
import antlr.debug.ParserMatchEvent;
import antlr.debug.ParserTokenEvent;
import antlr.debug.SemanticPredicateEvent;
import antlr.debug.SyntacticPredicateEvent;
import antlr.debug.TraceEvent;

public class ASN2TDLTranslator extends AbstractTranslator {
	public enum PASS {
		TYPES,
		PROPERTIES,
		SELECTION,
		MAPPINGS
	}
	public void translate(String filename) throws Exception {

		AsnModel model = getAsnModelFromAsn1File(filename);
		drm = getTypeFor("SOURCE_MAPPING", tdlPackage.Literals.DATA_RESOURCE_MAPPING);
		drm.setResourceURI("\""+new File(filename).getName()+"\"");

		for (String moduleName : model.modulesByName.keySet()) {
			//TODO: handle multiple modules?
			Package nestedPackage = tdlFactory.eINSTANCE.createPackage();
			nestedPackage.setName("generated_from_"+cleanName(moduleName));
			//TODO: test multiple packages per resource?
			//getTargetResource().getContents().add(nestedPackage);
			//TODO: use as "currentPackage"
			getGeneratedPackage().getNestedPackage().add(nestedPackage);
//			initTargetResource(source, cleanName(moduleName), "tdl");
			AsnModule module = model.modulesByName.get(moduleName);
			//TODO: handle imports?
			translateTypes(module);
//			translateValues(module);
		}
	}

	private void translateTypes(AsnModule module) {
		if (module.typesByName!=null) {
			//First pass: types
			translateTypes(module, PASS.TYPES);
			//Second pass: properties
			translateTypes(module, PASS.PROPERTIES);
			//Third pass: selection
			translateTypes(module, PASS.SELECTION);

			//Fourth pass - data element mappings
			for (String e : module.typesByName.keySet()) {
				DataType dataType = getTypeFor(e, tdlPackage.Literals.DATA_TYPE);
				DataElementMapping mapping = getTypeFor(e+"_MAPPING", tdlPackage.Literals.DATA_ELEMENT_MAPPING);
				mapping.setMappableDataElement(dataType);
				mapping.setElementURI("\""+e+"\"");
				mapping.setDataResourceMapping(drm);

			}			
		}
	}

	private void translateTypes(AsnModule module, PASS pass) {
		System.out.println("PASS: "+pass.name());
		for (String e : module.typesByName.keySet()) {
			AsnType type = module.typesByName.get(e);
			System.out.println("  Type: "+type.name);
			if (type.parameters!=null) {
				//TODO: parameters?
				for (AsnParameter p : type.parameters) {
					System.out.println("    "+p.dummyReference+" : "+p.paramGovernor);
				}
			}
			translateType(type, "", pass);
		}
	}
	
	private DataType translateType(AsnType type, String prefix, PASS pass) {
		final DataType generatedType;
		if (type instanceof AsnSequenceSet) {
			generatedType = getStructuredDataTypeFor(type.name);
			if (pass!=PASS.TYPES) {
				for (AsnElementType c : ((AsnSequenceSet) type).componentTypes) {
					translateMember((StructuredDataType)generatedType, c, pass);						
				}
			}
		} else if (type instanceof AsnChoice) {
			generatedType = getStructuredDataTypeFor(type.name);

			if (pass!=PASS.TYPES) {
				constrainWith(generatedType, "union");
				//annotateWith(generatedType, "CHOICE");
				for (AsnElementType c : ((AsnChoice) type).componentTypes) {
					translateMember((StructuredDataType)generatedType, c, pass);						
				}
			}
		} else if (type instanceof AsnSequenceOf) {
			//TODO: annotate collection further?
			//TODO: attribute SEQUENCE OF SEQUENCE {...}
			//		Type TYPE___attribute___collection___item
			//		Collection TYPE___attribute___collection of ...___item 
			generatedType = getTypeFor(cleanName(type.name), tdlPackage.Literals.COLLECTION_DATA_TYPE);
			if (pass!=PASS.TYPES) {
				AsnElementType componentType = ((AsnSequenceOf) type).componentType;
				String itemTypePrefix = "";
				if (componentType.name.isEmpty() && 
						componentType.typeReference instanceof AsnSequenceSet) {
					itemTypePrefix = generatedType.getName()+"___item";
					//TODO: process internals
				}
				DataType itemType = translateType(componentType, itemTypePrefix, pass);
				((CollectionDataType) generatedType).setItemType(itemType);
				System.out.println("  Flat collection: "+generatedType.getName() +" of " +itemType.getName());
			} else {
				AsnElementType componentType = ((AsnSequenceOf) type).componentType;
				DataType itemType = translateType(componentType, "", pass);
				((CollectionDataType) generatedType).setItemType(itemType);
				System.out.println("  Extended collection: "+generatedType.getName() +" of " +itemType.getName());
			}
		} else if (type instanceof AsnElementType) {
			AsnElementType eType = ((AsnElementType) type);
			if (eType.definedType!=null) {
				//TODO: handle defined type separately?
				DataType existingType = findElementOfType(eType.definedType.typeName, tdlPackage.Literals.DATA_TYPE);
				if (existingType != null) {
					generatedType = existingType;
				} else {
					//TODO: what shall be the default?
					generatedType = getStructuredDataTypeFor(eType.definedType.typeName);
				}
			} else if (eType.typeReference!=null) {
				if (eType.typeReference.name.isEmpty() && 
						(eType.typeReference instanceof AsnSequenceSet
								|| eType.typeReference instanceof AsnChoice
								|| eType.typeReference instanceof AsnSequenceOf
								//TODO: Others?
				)) {
					//TODO: CAREFUL HERE!!
					eType.typeReference.name = prefix+eType.name;
				}
				
				generatedType = translateType(eType.typeReference, "", pass);
			} else {
				//TODO: handle properly (usually due to "|" present in definition - not supported by parser) 
				generatedType = getStructuredDataTypeFor(prefix+cleanName(type.name));
				System.out.println("  Element type: "+type.name+" : " +type.getClass().getSimpleName()+" not supported yet");
			}
		} else if (type instanceof AsnTaggedType) {
			generatedType = translateTagged(type, pass);
		} else if (type instanceof AsnDefinedType) {
			//TODO: handle somehow?
			generatedType = getSimpleDataTypeFor(type.name);
		} else if (type instanceof AsnEnum && pass!=PASS.PROPERTIES) {
			//TODO: add enum values from ((AsnEnum)type).namedNumberList.namedNumbers
			generatedType = getEnumDataTypeFor(type.name);
//			annotateWith(generatedType, "ENUM");
			for (AsnNamedNumber literal : ((AsnEnum)type).namedNumberList.namedNumbers) {
				SimpleDataInstance enumLiteral = tdlFactory.eINSTANCE.createSimpleDataInstance();
				enumLiteral.setName(cleanName(literal.name));
				enumLiteral.setDataType(generatedType);
				((EnumDataType)generatedType).getValue().add(enumLiteral);
			}
		} else if (type instanceof AsnCharacterString) {
			String typeName = type.name;
			if (typeName.isEmpty()) {
				typeName = ((AsnCharacterString)type).stringtype;
			}
			if (typeName.isEmpty()) {
				typeName = type.getClass().getSimpleName();
			}
			generatedType = getSimpleDataTypeFor(typeName);
			if (!type.name.isEmpty()) {
				//TODO: expand further?
				if (((AsnCharacterString)type).stringtype.isEmpty()) {
					constrainWith(generatedType, type.getClass().getSimpleName());
				} else {
					constrainWith(generatedType, ((AsnCharacterString)type).stringtype);
				}
			}
		} else if (type instanceof AsnInteger 
				|| type instanceof AsnBitString 
				|| type instanceof AsnCharacterString
				|| type instanceof AsnBoolean
				|| type instanceof AsnOctetString
				|| type instanceof AsnObjectIdentifier
				|| type instanceof AsnAny
				|| type instanceof AsnNull
				) {
//			String typeName = type.getClass().getSimpleName();
			String typeName = type.name;
			if (typeName.isEmpty()) {
				typeName = type.getClass().getSimpleName();
			}
			generatedType = getSimpleDataTypeFor(typeName);
			if (!type.name.isEmpty()) {
				constrainWith(generatedType, type.getClass().getSimpleName());
			}
		} else if (type instanceof AsnSelectionType) {
			//TODO: document handling, applicable to Choice only, also illustrate with example
			String typeName = type.name;
			String propertyName = ((AsnSelectionType) type).selectionID;
			if (pass==PASS.SELECTION) {
				if (((AsnSelectionType)type).type instanceof AsnDefinedType) {
					String selectionType = ((AsnDefinedType) ((AsnSelectionType)type).type).typeName;
					StructuredDataType dataType = getStructuredDataTypeFor(selectionType);
					Member selectedMember = findContentWithName(cleanName(propertyName), dataType, tdlPackage.Literals.MEMBER);
					if (selectedMember != null) {
						generatedType = selectedMember.getDataType();
					} else {
						System.out.println("  Selection for "+propertyName+" in mapping for type: "+selectionType+" not found");				
						generatedType = getSimpleDataTypeFor(typeName);
					}
				} else {
					System.out.println("  Selection for "+type.name+" : " +((AsnSelectionType)type).type.getClass().getSimpleName()+" not supported yet");
					generatedType = getSimpleDataTypeFor(typeName);
				}
			} else {
				System.out.println("  Skipping Selection for "+propertyName+"...");
				generatedType = getSimpleDataTypeFor("Selection");
			}
		} else {
			String typeName = type.name;
			if (typeName.isEmpty()) {
				typeName = type.getClass().getSimpleName();
			}
			//TODO: determine if simple, if no type name?!
			generatedType = getStructuredDataTypeFor(typeName);
//			generatedType = getSimpleDataTypeFor(typeName);
			System.out.println("  Type: "+type.name+" : " +type.getClass().getSimpleName()+" not supported yet");
		}
		return generatedType;
	}

	private DataType translateTagged(AsnType type, PASS pass) {
		final DataType generatedType;
		AsnTaggedType taggedType = ((AsnTaggedType) type);
		if (taggedType.typeReference!=null) {
			if (taggedType.typeReference.name.isEmpty()) {
				//TODO: check if this is always the case
				taggedType.typeReference.name = taggedType.name;
			}
			generatedType = translateType(taggedType.typeReference, "", pass);
			constrainWith(generatedType, taggedType.tag.clazz);
			//TODO: handle tagged types
		} else {
			generatedType = null;
			System.out.println("  Tagged type: "+type.name+" : " +type.getClass().getSimpleName()+" not supported yet");
		}
		return generatedType;
	}

	private void translateMember(StructuredDataType generatedType, AsnElementType c, PASS pass) {
		Member m = getContentWithName(c.name, generatedType, tdlPackage.Literals.MEMBER);
		String prefix = generatedType.getName()+"___";
		DataType dataType = translateType(c, prefix, pass);
		m.setDataType(dataType);
		if (dataType.getName().isEmpty()) {
			if (generatedType.getName().isEmpty()) {
				System.out.println("EMPTY___");
			}
			dataType.setName(generatedType.getName()+"___"+cleanName(c.name));
		}  
		m.setIsOptional(c.isOptional);
		generatedType.getMember().add(m);
	}

	private void translateValues(AsnModule module) {
		if (module.asnValueAssignmentsByName!=null) {
			module.asnValueAssignmentsByName.forEach((name, v) -> {
				String desc = "";
				if (v.type instanceof AsnDefinedType) {
					desc = ((AsnDefinedType)v.type).typeName;
				}
				System.out.println("   Value: "+name + " : "+desc);
			});
		}
	}

	private AsnModel getAsnModelFromAsn1File(String inputFileName) throws Exception {
		AsnModel model = new AsnModel();
		try (InputStream stream = new BufferedInputStream(Files.newInputStream(Paths.get(inputFileName)))) {
			ASNLexer lexer = new ASNLexer(stream);
			ASNParser parser = new ASNParser(lexer);
			//TODO: capture and show parsing/lexing errors - currently only critical errors shown
			parser.module_definitions(model);
		}
		return model;
	}
}
