package org.etsi.mts.tdl.transform;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.EcoreUtil2;
import org.etsi.mts.tdl.Annotation;
import org.etsi.mts.tdl.AnnotationType;
import org.etsi.mts.tdl.CollectionDataType;
import org.etsi.mts.tdl.Comment;
import org.etsi.mts.tdl.Constraint;
import org.etsi.mts.tdl.ConstraintType;
import org.etsi.mts.tdl.DataElementMapping;
import org.etsi.mts.tdl.DataResourceMapping;
import org.etsi.mts.tdl.DataType;
import org.etsi.mts.tdl.Element;
import org.etsi.mts.tdl.ElementImport;
import org.etsi.mts.tdl.EnumDataType;
import org.etsi.mts.tdl.Extension;
import org.etsi.mts.tdl.NamedElement;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.PackageableElement;
import org.etsi.mts.tdl.Parameter;
import org.etsi.mts.tdl.ParameterMapping;
import org.etsi.mts.tdl.SimpleDataInstance;
import org.etsi.mts.tdl.SimpleDataType;
import org.etsi.mts.tdl.StructuredDataInstance;
import org.etsi.mts.tdl.StructuredDataType;
import org.etsi.mts.tdl.tdlFactory;
import org.etsi.mts.tdl.tdlPackage;

public abstract class AbstractTranslator {
	protected DataResourceMapping drm;
	protected DataResourceMapping drmTarget;
	private Package generatedPackage;
	protected SimpleDataType stringType;
	private Resource targetResource;
	protected SimpleDataType referencedType;
	protected boolean useQualifiers = true;

	protected boolean fullPrefix = false;
	protected String sourceMappingTag = "SOURCE_MAPPING";
	protected String targetMappingTag = "TARGET_MAPPING";

	public AbstractTranslator() {
		super();
	}

	public abstract void translate(String targetFilename) throws Exception;
	
	public void initTargetResource(String name) {
		generatedPackage = tdlFactory.eINSTANCE.createPackage();
		generatedPackage.setName("generated_from_"+name);
		targetResource.getContents().add(generatedPackage);
		stringType = getSimpleDataTypeFor("String");
		referencedType = getSimpleDataTypeFor("TODO_RESOLVE_REFERENCED");
	}
	
	public void addImports(Package p) {
		generatedPackage.getImport().addAll(p.getImport());
		ElementImport sourceImport = tdlFactory.eINSTANCE.createElementImport();
		sourceImport.setImportedPackage(p);
		generatedPackage.getImport().add(sourceImport);
	}

	protected DataType getDataTypeFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.DATA_TYPE);
	}

	protected SimpleDataType getSimpleDataTypeFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.SIMPLE_DATA_TYPE);
	}

	protected EnumDataType getEnumDataTypeFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.ENUM_DATA_TYPE);
	}

	protected SimpleDataInstance getSimpleDataInstanceFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.SIMPLE_DATA_INSTANCE);
	}

	protected SimpleDataInstance getEnumDataInstanceFor(String name, EnumDataType type) {
		return getContentWithName(getCleanName(name), type, tdlPackage.Literals.SIMPLE_DATA_INSTANCE);
	}

	protected StructuredDataType getStructuredDataTypeFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.STRUCTURED_DATA_TYPE);
	}
	
	protected StructuredDataInstance getStructuredDataInstanceFor(String name) {
		return getTypeFor(getCleanName(name), tdlPackage.Literals.STRUCTURED_DATA_INSTANCE);
	}
	
	protected CollectionDataType getCollectionDataTypeFor(DataType itemType) {
		CollectionDataType collectionType = getTypeFor(itemType.getName() + "_collection",
				tdlPackage.Literals.COLLECTION_DATA_TYPE);
		collectionType.setItemType(itemType);
		return collectionType;
	}

	protected <T extends DataType> void addSuperType(T type, T superType) {
		if (superType == null)
			return;
		List<Extension> ext = null;
		if (type instanceof SimpleDataType) {
			Extension extension = ((SimpleDataType) type).getExtension();
			if (extension != null)
				ext = Collections.singletonList(extension);
		}
		if (type instanceof StructuredDataType)
			ext = ((StructuredDataType) type).getExtension();
		if (ext == null)
			return;
		for (Extension e : ext) {
			if (e.getExtending().equals(superType))
				return;
		}
		Extension e = tdlFactory.eINSTANCE.createExtension();
		e.setExtending(superType);
		if (type instanceof SimpleDataType)
			((SimpleDataType) type).setExtension(e);
		if (type instanceof StructuredDataType)
			((StructuredDataType) type).getExtension().add(e);
	}


	protected String idStartDigitRegex = "\\A\\d";
	protected String idInvalidCharRegex = "\\W";
	public static String cleanName(String name) {
		//TODO: use keywords filter?
//		List<String> keywords = List.of("Message", "Time", "Type", "type", "name", "instance", "size", "component");
		//TODO: is this redundant now for TDLtx?
//		List<String> keywords = List.of("Message", "Time", "Type", "instance", "size", "component");
//		if (keywords.contains(name)) {
//			name = "^"+name;
//		}
		name = name.replaceAll("-", "_")
				.replaceAll(" ", "_")
				.replaceAll(":", "_")
				.replaceAll("@", "_")
				.replaceAll("\\.", "_")
				.replaceFirst("^(\\d)", "_\\1");
		return name;
	}
	public String getCleanName(String name) {
		return cleanName(name);
	}

	@SuppressWarnings("unchecked")
	protected <T extends PackageableElement> T getTypeFor(String name, EClass targetType) {
		String cleanName = getCleanName(name);
		//TODO: move to ASN2TDL specialisation
		TreeMap<String, String> mappings = new TreeMap<>();
		mappings.put("AsnInteger", "Integer");
		mappings.put("AsnBoolean", "Boolean");
//		mappings.put("AsnCharacterString", "String");
		mappings.put("AsnOctetString", "OCTETSTRING");
		mappings.put("AsnBitString", "BITSTRING");
		mappings.put("AsnReal", "Real");
		cleanName = mappings.getOrDefault(cleanName, cleanName);
		T generatedType = findElementOfType(cleanName, targetType);
		if (generatedType == null) {
			generatedType = (T) tdlFactory.eINSTANCE.create(targetType);
			generatedType.setName(cleanName);
			generatedPackage.getPackagedElement().add(generatedType);
		}
		return generatedType;
	}

	@SuppressWarnings("unchecked")
	protected <T extends PackageableElement> T findElementOfType(String name, EClass targetType) {
		Optional<PackageableElement> optional = generatedPackage.getPackagedElement().stream()
				.filter(e -> 
					targetType.isInstance(e) &&
					e.getName().equals(getCleanName(name)))
				.findFirst();
		T generatedType = null;
		if (optional.isPresent()) {
			generatedType = (T) optional.get();
		}
		return generatedType;
	}

	@SuppressWarnings("unchecked")
	protected <T extends NamedElement> T findContentWithName(String name, NamedElement container, EClass targetType) {
		Optional<T> optional = container.eContents().stream()
				.filter(e->targetType.isInstance(e))
				.map(e->(T) e)
				.filter(e -> e.getName().equals(name))
				.findFirst();
		T content = null;
		if (optional.isPresent()) {
			content = (T) optional.get();
		}
		return content;
	}

	
	@SuppressWarnings("unchecked")
	protected <T extends NamedElement> T getContentWithName(String name, NamedElement container, EClass targetType) {
		String cleanName = getCleanName(name);
		T content = findContentWithName(cleanName, container, targetType);
		if (content == null) {
			content = (T) tdlFactory.eINSTANCE.create(targetType);
			content.setName(cleanName);
		}
		return content;
	}

	@SuppressWarnings("unchecked")
	protected <T extends Element> Optional<T> getContentWithName(String name, NamedElement container, Class<T> type) {
		Optional<T> optional = container.eContents().stream()
				.filter(e->type.isInstance(e))
				.map(e->(T) e)
				.filter(e -> e.getName().equals(name))
				.findFirst();
		return optional;
	}
	
	@SuppressWarnings("unchecked")
	protected <T extends Element> Optional<T> getContentWithPredicate(Predicate<? super T> predicate, NamedElement container, Class<T> type) {
		Optional<T> optional = container.eContents().stream()
				.filter(e->type.isInstance(e))
				.map(e->(T) e)
				.filter(predicate)
				.findFirst();
		return optional;
	}
	protected Annotation getAnnotation(final Element element, String annotationName) {
		AnnotationType annotationType = getTypeFor(getCleanName(annotationName), tdlPackage.Literals.ANNOTATION_TYPE);
		Optional<Annotation> optional = element.getAnnotation().stream().filter(a->a.getKey()==annotationType).findFirst();
		return optional.orElseGet(() -> {
			Annotation annotation = tdlFactory.eINSTANCE.createAnnotation();
			annotation.setKey(annotationType);
			element.getAnnotation().add(annotation);
			return annotation;
		});
	}

	protected Comment getComment(final Element element, String noteName) {
		Optional<Comment> optional = element.getComment().stream().filter(a->a.getName()==noteName).findFirst();
		return optional.orElseGet(() -> {
			Comment comment = tdlFactory.eINSTANCE.createComment();
			comment.setName(noteName);
			element.getComment().add(comment);
			return comment;
		});
	}
	
	protected void annotateWith(final Element element, String annotationName, String annotationValue) {
		Annotation annotation = getAnnotation(element, annotationName);
		annotation.setValue(annotationValue);
	}
	
	protected void annotateWith(final Element element, String annotationName) {
		getAnnotation(element, annotationName);
	}

	protected void noteWith(final Element element, String commentName, String body) {
		Comment comment = getComment(element, commentName);
		comment.setBody(body);
	}

	protected Constraint constrainWith(final DataType generatedType, String constraintName) {
		ConstraintType constraintType = getTypeFor(getCleanName(constraintName), tdlPackage.Literals.CONSTRAINT_TYPE);
		if (!generatedType.getConstraint().stream().anyMatch(a->a.getType()==constraintType)) {
			Constraint constraint = tdlFactory.eINSTANCE.createConstraint();
			constraint.setType(constraintType);
			generatedType.getConstraint().add(constraint);
			return constraint;
		}
		//TODO: return existing constraint?
		return null;
	}
	
	protected DataElementMapping addDataElementMapping(String uri, DataType dataType, String tag, DataResourceMapping resourceMapping) {
		DataElementMapping mapping = getTypeFor(dataType.getName()+"_"+tag, tdlPackage.Literals.DATA_ELEMENT_MAPPING);
		mapping.setMappableDataElement(dataType);
		mapping.setElementURI(uri);
		mapping.setDataResourceMapping(resourceMapping);
		return mapping;
	}
	
	protected void addParameterMapping(DataElementMapping mapping, Parameter p, String uri) {
		Optional<ParameterMapping> opm = getContentWithPredicate(e->e.getParameter() == p, mapping, ParameterMapping.class);
		if (opm.isEmpty()) {
			ParameterMapping pm = (ParameterMapping) tdlFactory.eINSTANCE.create(tdlPackage.Literals.PARAMETER_MAPPING);
			pm.setParameter(p);
			pm.setParameterURI(uri.replaceAll("\\^", ""));
			mapping.getParameterMapping().add(pm);
		}
	}

	public Resource getTargetResource() {
		return targetResource;
	}

	public void setTargetResource(Resource targetResource) {
		this.targetResource = targetResource;
	}

	public Package getGeneratedPackage() {
		return generatedPackage;
	}

	public void setGeneratedPackage(Package generatedPackage) {
		this.generatedPackage = generatedPackage;
	}

	public void ensureUniqueNames(Package p) {
		Set<String> names = new HashSet<>();
		for (PackageableElement pe: p.getPackagedElement()) {
			makeUnique(pe, names);
			if (pe instanceof EnumDataType)
				for (PackageableElement l: ((EnumDataType) pe).getValue()) 
					makeUnique(l, names);
			else if (pe instanceof Package) {
				ensureUniqueNames((Package) pe);
			}
		}
		
	}

	private void makeUnique(NamedElement e, Set<String> names) {
		String name = e.getName();
		int i = 1;
		String newName = name;
		while (names.contains(newName)) {
			newName = name + "_" + i++;
		}
		if (!newName.equals(name))
			e.setName(newName);
		names.add(newName);
	}

}