package org.etsi.mts.tdl.transform;

import java.util.List;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Predicate;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.resource.Resource;
import org.etsi.mts.tdl.Annotation;
import org.etsi.mts.tdl.AnnotationType;
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.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.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 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 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("\\.", "_");
		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 void annotateWith(final DataType generatedType, String annotationName) {
		AnnotationType annotationType = getTypeFor(getCleanName(annotationName), tdlPackage.Literals.ANNOTATION_TYPE);
		if (!generatedType.getAnnotation().stream().anyMatch(a->a.getKey()==annotationType)) {
			Annotation annotation = tdlFactory.eINSTANCE.createAnnotation();
			annotation.setKey(annotationType);
			generatedType.getAnnotation().add(annotation);
		}
	}

	protected void 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);
		}
	}
	
	protected DataElementMapping addDataElementMapping(String uri, DataType dataType, String tag) {
		DataElementMapping mapping = getTypeFor(dataType.getName()+"_"+tag, tdlPackage.Literals.DATA_ELEMENT_MAPPING);
		mapping.setMappableDataElement(dataType);
		mapping.setElementURI(uri);
		mapping.setDataResourceMapping(drm);
		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;
	}

}