package org.hl7.fhir.instance.hapi.validation;

import static org.apache.commons.lang3.StringUtils.isBlank;

import java.io.*;
import java.util.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.StructureDefinition;
import org.hl7.fhir.instance.utils.IResourceValidator.BestPracticeWarningLevel;
import org.hl7.fhir.instance.utils.IResourceValidator.IdStatus;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.w3c.dom.*;
import org.xml.sax.InputSource;

import com.google.gson.*;

import ca.uhn.fhir.context.*;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.validation.IValidationContext;
import ca.uhn.fhir.validation.IValidatorModule;

public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule {

  private static FhirContext ourHl7OrgCtx;
  private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class);
  private BestPracticeWarningLevel myBestPracticeWarningLevel;
  private DocumentBuilderFactory myDocBuilderFactory;
  private StructureDefinition myStructureDefintion;
  private IValidationSupport myValidationSupport;

  /**
   * Constructor
   * 
   * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport
   * validation support}
   */
  public FhirInstanceValidator() {
    this(new DefaultProfileValidationSupport());
  }

  /**
   * Constructor which uses the given validation support
   * 
   * @param theValidationSupport
   *          The validation support
   */
  public FhirInstanceValidator(IValidationSupport theValidationSupport) {
    myDocBuilderFactory = DocumentBuilderFactory.newInstance();
    myDocBuilderFactory.setNamespaceAware(true);
    myValidationSupport = theValidationSupport;
  }

  private String determineResourceName(Document theDocument) {
    Element root = null;

    NodeList list = theDocument.getChildNodes();
    for (int i = 0; i < list.getLength(); i++) {
      if (list.item(i) instanceof Element) {
        root = (Element) list.item(i);
        break;
      }
    }
    root = theDocument.getDocumentElement();
    return root.getLocalName();
  }

  /**
   * Returns the "best practice" warning level (default is
   * {@link BestPracticeWarningLevel#Hint}).
   * <p>
   * The FHIR Instance Validator has a number of checks for best practices in
   * terms of FHIR usage. If this setting is set to
   * {@link BestPracticeWarningLevel#Error}, any resource data which does not
   * meet these best practices will be reported at the ERROR level. If this
   * setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
   * guielines will be ignored.
   * </p>
   * 
   * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)}
   */
  public BestPracticeWarningLevel getBestPracticeWarningLevel() {
    return myBestPracticeWarningLevel;
  }

  /**
   * Returns the {@link IValidationSupport validation support} in use by this
   * validator. Default is an instance of
   * {@link DefaultProfileValidationSupport} if the no-arguments constructor for
   * this object was used.
   */
  public IValidationSupport getValidationSupport() {
    return myValidationSupport;
  }

  /**
   * Sets the "best practice warning level". When validating, any deviations
   * from best practices will be reported at this level.
   * <p>
   * The FHIR Instance Validator has a number of checks for best practices in
   * terms of FHIR usage. If this setting is set to
   * {@link BestPracticeWarningLevel#Error}, any resource data which does not
   * meet these best practices will be reported at the ERROR level. If this
   * setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
   * guielines will be ignored.
   * </p>
   * 
   * @param theBestPracticeWarningLevel
   *          The level, must not be <code>null</code>
   */
  public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) {
    Validate.notNull(theBestPracticeWarningLevel);
    myBestPracticeWarningLevel = theBestPracticeWarningLevel;
  }

  public void setStructureDefintion(StructureDefinition theStructureDefintion) {
    myStructureDefintion = theStructureDefintion;
  }

  /**
   * Sets the {@link IValidationSupport validation support} in use by this
   * validator. Default is an instance of
   * {@link DefaultProfileValidationSupport} if the no-arguments constructor for
   * this object was used.
   */
  public void setValidationSupport(IValidationSupport theValidationSupport) {
    myValidationSupport = theValidationSupport;
  }

  protected List<ValidationMessage> validate(final FhirContext theCtx, String theInput, EncodingEnum theEncoding) {
    HapiWorkerContext workerContext = new HapiWorkerContext(theCtx, myValidationSupport);

    org.hl7.fhir.instance.validation.InstanceValidator v;
    try {
      v = new org.hl7.fhir.instance.validation.InstanceValidator(workerContext);
    } catch (Exception e) {
      throw new ConfigurationException(e);
    }

	//    v.setShouldCheckForIdPresence(false);
//    v.setRequireResourceId(false);
    v.setResourceIdRule(IdStatus.OPTIONAL);
    v.setBestPracticeWarningLevel(myBestPracticeWarningLevel);
    v.setAnyExtensionsAllowed(true);
    v.setSuppressLoincSnomedMessages(true);

    List<ValidationMessage> messages = new ArrayList<ValidationMessage>();

    if (theEncoding == EncodingEnum.XML) {
      Document document;
      try {
        DocumentBuilder builder = myDocBuilderFactory.newDocumentBuilder();
        InputSource src = new InputSource(new StringReader(theInput));
        document = builder.parse(src);
      } catch (Exception e2) {
        ourLog.error("Failure to parse XML input", e2);
        ValidationMessage m = new ValidationMessage();
        m.setLevel(IssueSeverity.FATAL);
        m.setMessage("Failed to parse input, it does not appear to be valid XML:" + e2.getMessage());
        return Collections.singletonList(m);
      }

      String resourceName = determineResourceName(document);
      StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : loadProfileOrReturnNull(messages, theCtx, resourceName);
      if (profile != null) {
        try {
          v.validate(messages, document, profile);
        } catch (Exception e) {
          throw new InternalErrorException("Unexpected failure while validating resource", e);
        }
      }
    } else if (theEncoding == EncodingEnum.JSON) {
      Gson gson = new GsonBuilder().create();
      JsonObject json = gson.fromJson(theInput, JsonObject.class);

      String resourceName = json.get("resourceType").getAsString();
      StructureDefinition profile = myStructureDefintion != null ? myStructureDefintion : loadProfileOrReturnNull(messages, theCtx, resourceName);
      if (profile != null) {
        try {
          v.validate(messages, json, profile);
        } catch (Exception e) {
          throw new InternalErrorException("Unexpected failure while validating resource", e);
        }
      }
    } else {
      throw new IllegalArgumentException("Unknown encoding: " + theEncoding);
    }

    for (int i = 0; i < messages.size(); i++) {
      ValidationMessage next = messages.get(i);
      if ("Binding has no source, so can't be checked".equals(next.getMessage())) {
        messages.remove(i);
        i--;
      }
    }
    return messages;
  }

  @Override
  protected List<ValidationMessage> validate(IValidationContext<?> theCtx) {
    return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding());
  }

  static FhirContext getHl7OrgDstu2Ctx(FhirContext theCtx) {
    if (theCtx.getVersion().getVersion() == FhirVersionEnum.DSTU2_HL7ORG) {
      return theCtx;
    }
    FhirContext retVal = ourHl7OrgCtx;
    if (retVal == null) {
      retVal = FhirContext.forDstu2Hl7Org();
      ourHl7OrgCtx = retVal;
    }
    return retVal;
  }

  static StructureDefinition loadProfileOrReturnNull(List<ValidationMessage> theMessages, FhirContext theCtx,
      String theResourceName) {
    if (isBlank(theResourceName)) {
      if (theMessages != null) {
        theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL)
            .setMessage("Could not determine resource type from request. Content appears invalid."));
      }
      return null;
    }

    String profileClasspath = theCtx.getVersion().getPathToSchemaDefinitions().replace("/schema", "/profile");
    String profileCpName = profileClasspath + '/' + theResourceName.toLowerCase() + ".profile.xml";
    String profileText;
    try {
      InputStream inputStream = FhirInstanceValidator.class.getResourceAsStream(profileCpName);
      if (inputStream == null) {
        if (theMessages != null) {
          theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL)
              .setMessage("No profile found for resource type " + theResourceName));
          return null;
        } else {
          return null;
        }
      }
      profileText = IOUtils.toString(inputStream, "UTF-8");
    } catch (IOException e1) {
      if (theMessages != null) {
        theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL)
            .setMessage("No profile found for resource type " + theResourceName));
      }
      return null;
    }
    StructureDefinition profile = getHl7OrgDstu2Ctx(theCtx).newXmlParser().parseResource(StructureDefinition.class,
        profileText);
    return profile;
  }

}
