package ca.uhn.fhir.tinder.parser;

import static org.apache.commons.lang.StringUtils.capitalize;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.*;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoFailureException;

import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.resource.Profile;
import ca.uhn.fhir.model.dstu.resource.Profile.ExtensionDefn;
import ca.uhn.fhir.model.dstu.resource.Profile.Structure;
import ca.uhn.fhir.model.dstu.resource.Profile.StructureElement;
import ca.uhn.fhir.model.dstu.resource.Profile.StructureElementDefinition;
import ca.uhn.fhir.model.dstu.resource.Profile.StructureElementDefinitionType;
import ca.uhn.fhir.model.dstu.resource.Profile.StructureSearchParam;
import ca.uhn.fhir.model.dstu.valueset.DataTypeEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.tinder.model.AnyChild;
import ca.uhn.fhir.tinder.model.BaseElement;
import ca.uhn.fhir.tinder.model.BaseRootType;
import ca.uhn.fhir.tinder.model.Child;
import ca.uhn.fhir.tinder.model.Resource;
import ca.uhn.fhir.tinder.model.ResourceBlock;
import ca.uhn.fhir.tinder.model.SearchParameter;
import ca.uhn.fhir.tinder.model.SimpleChild;
import ca.uhn.fhir.tinder.model.Slicing;

public class ProfileParser extends BaseStructureParser {

	public ProfileParser(String theVersion, String theBaseDir) {
		super(theVersion, theBaseDir);
	}

	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ProfileParser.class);

	private ExtensionDefn findExtension(Profile theProfile, String theCode) {
		for (ExtensionDefn next : theProfile.getExtensionDefn()) {
			if (theCode.equals(next.getCode().getValue())) {
				return next;
			}
		}
		return null;
	}

	@Override
	protected String getFilenameSuffix() {
		return "";
	}

	@Override
	protected String getTemplate() {
		return "dstu".equals(getVersion()) ? "/vm/resource_dstu.vm" : "/vm/resource.vm";
	}

	public void parseSingleProfile(File theProfile, String theHttpUrl) throws MojoFailureException {
		String profileString;
		try {
			profileString = IOUtils.toString(new FileReader(theProfile));
		} catch (IOException e) {
			throw new MojoFailureException("Failed to load: " + theProfile, e);
		}
		
		FhirContext ctx = new FhirContext(Profile.class);
		Profile profile = ctx.newXmlParser().parseResource(Profile.class, profileString);
		try {
			parseSingleProfile(profile, theHttpUrl);
		} catch (Exception e) {
			throw new MojoFailureException("Failed to parse profile", e);
		}
	}
	
	
	public void parseBaseResources(List<String> theBaseResourceNames, String theHttpUrl) throws MojoFailureException {
		FhirContext fhirContext = new FhirContext(Profile.class);

		for (String nextFileName : theBaseResourceNames) {
			ourLog.info("Parsing file: {}", nextFileName);

			Profile profile;
			try {
				profile = (Profile) fhirContext.newXmlParser().parseResource(IOUtils.toString(new FileReader(nextFileName)));
			} catch (Exception e) {
				throw new MojoFailureException("Failed to load or parse file: " + nextFileName, e);
			}

			try {
				parseSingleProfile(profile, theHttpUrl);
			} catch (Exception e) {
				throw new MojoFailureException("Failed to process file: " + nextFileName, e);
			}
		}

		// for (int i = 0; i < theBaseResourceNames.size(); i++) {
		// theBaseResourceNames.set(i,
		// theBaseResourceNames.get(i).toLowerCase());
		// }
		//
		// try {
		//
		// Bundle bundle =
		// fhirContext.newXmlParser().parseBundle(IOUtils.toString(getClass().getResourceAsStream("/prof/allprofiles.xml")));
		// TreeSet<String> allProfiles = new TreeSet<String>();
		// for (BundleEntry nextResource : bundle.getEntries() ) {
		// Profile nextProfile = (Profile) nextResource.getResource();
		// allProfiles.add(nextProfile.getName().getValue());
		// if
		// (theBaseResourceNames.contains(nextProfile.getName().getValue().toLowerCase())){
		// parseSingleProfile(nextProfile,
		// bundle.getLinkBase().getValueNotNull());
		// }
		// }
		//
		// ourLog.info("Base profiles found: {}", allProfiles);
		//
		// } catch (Exception e) {
		// throw new MojoFailureException("Failed to load base resources", e);
		// }
	}

	public BaseRootType parseSingleProfile(Profile theProfile, String theUrlTOThisProfile) throws Exception {
		BaseRootType retVal = null;
		for (Structure nextStructure : theProfile.getStructure()) {

			int elemIdx = 0;
			Map<String, BaseElement> elements = new HashMap<String, BaseElement>();
			for (StructureElement next : nextStructure.getElement()) {

				BaseElement elem;
				if (elemIdx == 0) {
					retVal = new Resource();
					retVal.setProfile(theProfile.getIdentifier().getValue());
					if (retVal.getProfile() == null) {
						retVal.setProfile(theUrlTOThisProfile);
					}

					for (StructureSearchParam nextParam : nextStructure.getSearchParam()) {
						SearchParameter param = new SearchParameter();
						param.setName(nextParam.getName().getValue());
						
						String path = defaultString(nextParam.getXpath().getValue());
						path=path.replace("/f:", ".").replace("f:", "");
						param.setPath(path);
						
						param.setType(nextParam.getType().getValue());
						param.setDescription(nextParam.getDocumentation().getValue());
						retVal.addSearchParameter(param);
					}

					addResource(retVal);
					elem = retVal;
					// below StringUtils.isBlank(type) || type.startsWith("=")
				} else {
					if (next.getDefinition().getType().isEmpty()) {
						elem = new ResourceBlock();
						// } else if (type.startsWith("@")) {
						// elem = new ResourceBlockCopy();
					} else if (next.getDefinition().getType().get(0).getCode().getValue().equals("*")) {
						elem = new AnyChild();
//					} else if (next.getDefinition().getType().get(0).getCode().getValue().equals("Extension")) {
//						elem = new UndeclaredExtensionChild();
					} else {
						elem = new SimpleChild();
					}

				}

				boolean allResourceReferences = next.getDefinition().getType().size() > 0;
				for (StructureElementDefinitionType nextType : next.getDefinition().getType()) {
					if (nextType.getCode().getValueAsEnum() != DataTypeEnum.RESOURCEREFERENCE) {
						allResourceReferences = false;
					}
				}

				populateNewElement(next, elem, allResourceReferences);

				StructureElementDefinition definition = next.getDefinition();

				BaseElement parentElement = elements.get(elem.getElementParentName());
				if (next.getSlicing().getDiscriminator().getValue() != null) {
					Slicing slicing = new Slicing();
					slicing.setDiscriminator(next.getSlicing().getDiscriminator().getValue());
					if (parentElement.getChildElementNameToSlicing().get(elem.getName()) != null) {
						throw new ConfigurationException("Found multiple slicing definitions for path: " + next.getPath().getValue());
					}
					parentElement.getChildElementNameToSlicing().put(elem.getName(), slicing);
					continue;
				}

				Slicing childIsSliced = parentElement != null ? parentElement.getChildElementNameToSlicing().get(elem.getName()) : null;

				/*
				 * Profiles come with a number of standard elements which are generally ignored because they are boilerplate, unless the definition is somehow changing their behaviour (e.g. through
				 * slices)
				 */
				if (next.getPath().getValue().endsWith(".contained")) {
					continue;
				}
				if (next.getPath().getValue().endsWith(".text")) {
					continue;
				}
				if (next.getPath().getValue().endsWith(".extension")) {
					if (childIsSliced != null) {
						if (!"url".equals(childIsSliced.getDiscriminator())) {
							throw new ConfigurationException("Extensions must be sliced on 'url' discriminator. Found: " + next.getSlicing().getDiscriminator().getValue());
						}
						if (next.getDefinition().getType().size() != 1 || next.getDefinition().getType().get(0).getCode().getValueAsEnum() != DataTypeEnum.EXTENSION) {
							throw new ConfigurationException("Extension slices must have a single type with a code of 'Extension'");
						}

						String name = next.getName().getValue();
						if (StringUtils.isBlank(name)) {
							throw new ConfigurationException("Extension slices must have a 'name' defined, none found at path: " + next.getPath());
						}
						elem.setName(name);
						elem.setElementName(name);
						
//						elem = new Extension();
//						populateNewElement(next, elem, allResourceReferences);
						
						String profile = next.getDefinition().getType().get(0).getProfile().getValueAsString();
						if (isBlank(profile)) {
							throw new ConfigurationException("Extension slice for " + next.getPath().getValue() + " has no profile specified in its type");
						}
						if (profile.startsWith("#")) {
							Profile.ExtensionDefn extension = findExtension(theProfile, profile.substring(1));
							if (extension == null) {
								throw new ConfigurationException("Unknown local extension reference: " + profile);
							}
							ourLog.info("Element at path {} is using extension {}", next.getPath(), profile);
							definition = extension.getDefinition();
							String extensionUrl = theUrlTOThisProfile + profile;
							elem.setExtensionUrl(extensionUrl);
						} else {
							// TODO: implement this
							throw new ConfigurationException("Extensions specified outside of the given profile are not yet supported");
						}
					} else {
						continue;
					}
				}
				if (next.getPath().getValue().endsWith(".modifierExtension")) {
					continue;
				}

				for (StructureElementDefinitionType nextType : definition.getType()) {
					if (nextType.getCode().getValueAsEnum() == DataTypeEnum.RESOURCEREFERENCE) {
						if (nextType.getProfile().getValueAsString().startsWith("http://hl7.org/fhir/profiles/")) {
							elem.getType().add(capitalize(nextType.getProfile().getValueAsString().substring("http://hl7.org/fhir/profiles/".length())));
						} else {
							// TODO: implement this.. we need to be able to
							// reference other profiles
							throw new ConfigurationException("Profile type not yet supported");
						}
					} else {
						elem.getType().add(capitalize(nextType.getCode().getValue()) + "Dt");
					}

				}

				elem.setBinding(definition.getBinding().getName().getValue());
				elem.setShortName(definition.getShort().getValue());
				elem.setDefinition(definition.getFormal().getValue());
				elem.setRequirement(definition.getRequirements().getValue());
				elem.setCardMin(definition.getMin().getValueAsString());
				elem.setCardMax(definition.getMax().getValue());

				if (elem instanceof Child) {
					Child child = (Child) elem;
					elements.put(elem.getName(), elem);
					if (parentElement == null) {
						throw new Exception("Can't find element " + elem.getElementParentName() + "  -  Valid values are: " + elements.keySet());
					}
					parentElement.addChild(child);

					/*
					 * Find simple setters
					 */
					scanForSimpleSetters(child);
				} else {
					BaseRootType res = (BaseRootType) elem;
					elements.put(res.getName(), res);
				}

				elemIdx++;
			}

		}

		return retVal;
	}

	private void populateNewElement(StructureElement next, BaseElement elem, boolean allResourceReferences) {
		elem.setName(next.getPath().getValue());
		elem.setElementNameAndDeriveParentElementName(next.getPath().getValue());
		elem.setResourceRef(allResourceReferences);
	}

	public static void main(String[] args) throws Exception {
		IParser parser = new FhirContext(Profile.class).newXmlParser();
		ProfileParser pp = new ProfileParser("dev",".");
		
		String str = IOUtils.toString(new FileReader("../hapi-tinder-test/src/test/resources/profile/organization.xml"));
		Profile prof = parser.parseResource(Profile.class, str);
		pp.parseSingleProfile(prof, "http://foo");

		str = IOUtils.toString(new FileReader("../hapi-tinder-test/src/test/resources/profile/patient.xml"));
		prof = parser.parseResource(Profile.class, str);
		pp.parseSingleProfile(prof, "http://foo");

		pp.markResourcesForImports();
		pp.writeAll(new File("target/gen/test/resource"), null,"test");

	}

}
