package org.hl7.fhir.dstu3.model;



/*
  Copyright (c) 2011+, HL7, Inc.
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification,
  are permitted provided that the following conditions are met:

   * Redistributions of source code must retain the above copyright notice, this
     list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above copyright notice,
     this list of conditions and the following disclaimer in the documentation
     and/or other materials provided with the distribution.
   * Neither the name of HL7 nor the names of its contributors may be used to
     endorse or promote products derived from this software without specific
     prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.

*/
import static org.apache.commons.lang3.StringUtils.*;

import java.math.BigDecimal;
import java.util.UUID;

import org.apache.commons.lang3.*;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hl7.fhir.instance.model.api.*;

import ca.uhn.fhir.model.api.annotation.DatatypeDef;

/**
 * This class represents the logical identity for a resource, or as much of that
 * identity is known. In FHIR, every resource must have a "logical ID" which is
 * defined by the FHIR specification as:
 * <p>
 * <code>
 * Any combination of upper or lower case ASCII letters ('A'..'Z', and 'a'..'z', numerals ('0'..'9'), '-' and '.', with a length limit of 64 characters. (This might be an integer, an un-prefixed OID, UUID or any other identifier pattern that meets these constraints.)
 * </code>
 * </p>
 * <p>
 * This class contains that logical ID, and can optionally also contain a
 * relative or absolute URL representing the resource identity. For example, the
 * following are all valid values for IdType, and all might represent the same
 * resource:
 * </p>
 * <ul>
 * <li><code>123</code> (just a resource's ID)</li>
 * <li><code>Patient/123</code> (a relative identity)</li>
 * <li><code>http://example.com/Patient/123 (an absolute identity)</code></li>
 * <li>
 * <code>http://example.com/Patient/123/_history/1 (an absolute identity with a version id)</code>
 * </li>
 * <li>
 * <code>Patient/123/_history/1 (a relative identity with a version id)</code>
 * </li>
 * </ul>
 * <p>
 * Note that the 64 character
 * limit applies only to the ID portion ("123" in the examples above).
 * </p>
 * <p>
 * In most situations, you only need to populate the resource's ID (e.g.
 * <code>123</code>) in resources you are constructing and the encoder will
 * infer the rest from the context in which the object is being used. On the
 * other hand, the parser will always try to populate the complete absolute
 * identity on objects it creates as a convenience.
 * </p>
 * <p>
 * Regex for ID: [a-z0-9\-\.]{1,64}
 * </p>
 */
@DatatypeDef(name = "id", profileOf=StringType.class)
public final class IdType extends UriType implements IPrimitiveType<String>, IIdType {
  public static final String URN_PREFIX = "urn:";

  /**
   * This is the maximum length for the ID
   */
  public static final int MAX_LENGTH = 64; // maximum length

  private static final long serialVersionUID = 2L;
  private String myBaseUrl;
  private boolean myHaveComponentParts;
  private String myResourceType;
  private String myUnqualifiedId;
  private String myUnqualifiedVersionId;

  /**
   * Create a new empty ID
   */
  public IdType() {
    super();
  }

  /**
   * Create a new ID, using a BigDecimal input. Uses
   * {@link BigDecimal#toPlainString()} to generate the string representation.
   */
  public IdType(BigDecimal thePid) {
    if (thePid != null) {
      setValue(toPlainStringWithNpeThrowIfNeeded(thePid));
    } else {
      setValue(null);
    }
  }

  /**
   * Create a new ID using a long
   */
  public IdType(long theId) {
    setValue(Long.toString(theId));
  }

  /**
   * Create a new ID using a string. This String may contain a simple ID (e.g.
   * "1234") or it may contain a complete URL
   * (http://example.com/fhir/Patient/1234).
   *
   * <p>
   * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally
   * represented in hex), a uuid, an oid, or any other combination of lowercase
   * letters, numerals, "-" and ".", with a length limit of 36 characters.
   * </p>
   * <p>
   * regex: [a-z0-9\-\.]{1,36}
   * </p>
   */
  public IdType(String theValue) {
    setValue(theValue);
  }

  /**
   * Constructor
   *
   * @param theResourceType
   *          The resource type (e.g. "Patient")
   * @param theIdPart
   *          The ID (e.g. "123")
   */
  public IdType(String theResourceType, BigDecimal theIdPart) {
    this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart));
  }

  /**
   * Constructor
   *
   * @param theResourceType
   *          The resource type (e.g. "Patient")
   * @param theIdPart
   *          The ID (e.g. "123")
   */
  public IdType(String theResourceType, Long theIdPart) {
    this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart));
  }

  /**
   * Constructor
   *
   * @param theResourceType
   *          The resource type (e.g. "Patient")
   * @param theId
   *          The ID (e.g. "123")
   */
  public IdType(String theResourceType, String theId) {
    this(theResourceType, theId, null);
  }

  /**
   * Constructor
   *
   * @param theResourceType
   *          The resource type (e.g. "Patient")
   * @param theId
   *          The ID (e.g. "123")
   * @param theVersionId
   *          The version ID ("e.g. "456")
   */
  public IdType(String theResourceType, String theId, String theVersionId) {
    this(null, theResourceType, theId, theVersionId);
  }

  /**
   * Constructor
   *
   * @param theBaseUrl
   *          The server base URL (e.g. "http://example.com/fhir")
   * @param theResourceType
   *          The resource type (e.g. "Patient")
   * @param theId
   *          The ID (e.g. "123")
   * @param theVersionId
   *          The version ID ("e.g. "456")
   */
  public IdType(String theBaseUrl, String theResourceType, String theId, String theVersionId) {
    myBaseUrl = theBaseUrl;
    myResourceType = theResourceType;
    myUnqualifiedId = theId;
    myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null);
    myHaveComponentParts = true;
    if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) {
      myHaveComponentParts = false;
    }
  }

  /**
   * Creates an ID based on a given URL
   */
  public IdType(UriType theUrl) {
    setValue(theUrl.getValueAsString());
  }

  public void applyTo(IBaseResource theResource) {
    if (theResource == null) {
      throw new NullPointerException("theResource can not be null");
    } else {
      theResource.setId(new IdType(getValue()));
    }
  }

  /**
   * @deprecated Use {@link #getIdPartAsBigDecimal()} instead (this method was
   *             deprocated because its name is ambiguous)
   */
  @Deprecated
  public BigDecimal asBigDecimal() {
    return getIdPartAsBigDecimal();
  }

  @Override
  public IdType copy() {
    return new IdType(getValue());
  }

  @Override
  public boolean equals(Object theArg0) {
    if (!(theArg0 instanceof IdType)) {
      return false;
    }
    return StringUtils.equals(getValueAsString(), ((IdType)theArg0).getValueAsString());
  }

  /**
   * Returns true if this IdType matches the given IdType in terms of resource
   * type and ID, but ignores the URL base
   */
  @SuppressWarnings("deprecation")
  public boolean equalsIgnoreBase(IdType theId) {
    if (theId == null) {
      return false;
    }
    if (theId.isEmpty()) {
      return isEmpty();
    }
    return ObjectUtils.equals(getResourceType(), theId.getResourceType())
      && ObjectUtils.equals(getIdPart(), theId.getIdPart())
      && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart());
  }

  /**
   * Returns the portion of this resource ID which corresponds to the server
   * base URL. For example given the resource ID
   * <code>http://example.com/fhir/Patient/123</code> the base URL would be
   * <code>http://example.com/fhir</code>.
   * <p>
   * This method may return null if the ID contains no base (e.g. "Patient/123")
   * </p>
   */
  @Override
  public String getBaseUrl() {
    return myBaseUrl;
  }

  /**
   * Returns only the logical ID part of this ID. For example, given the ID
   * "http://example,.com/fhir/Patient/123/_history/456", this method would
   * return "123".
   */
  @Override
  public String getIdPart() {
    return myUnqualifiedId;
  }

  /**
   * Returns the unqualified portion of this ID as a big decimal, or
   * <code>null</code> if the value is null
   *
   * @throws NumberFormatException
   *           If the value is not a valid BigDecimal
   */
  public BigDecimal getIdPartAsBigDecimal() {
    String val = getIdPart();
    if (isBlank(val)) {
      return null;
    }
    return new BigDecimal(val);
  }

  /**
   * Returns the unqualified portion of this ID as a {@link Long}, or
   * <code>null</code> if the value is null
   *
   * @throws NumberFormatException
   *           If the value is not a valid Long
   */
  @Override
  public Long getIdPartAsLong() {
    String val = getIdPart();
    if (isBlank(val)) {
      return null;
    }
    return Long.parseLong(val);
  }

  @Override
  public String getResourceType() {
    return myResourceType;
  }

  /**
   * Returns the value of this ID. Note that this value may be a fully qualified
   * URL, a relative/partial URL, or a simple ID. Use {@link #getIdPart()} to
   * get just the ID portion.
   *
   * @see #getIdPart()
   */
  @Override
  public String getValue() {
    String retVal = super.getValue();
    if (retVal == null && myHaveComponentParts) {

      if (isLocal() || isUrn()) {
        return myUnqualifiedId;
      }

      StringBuilder b = new StringBuilder();
      if (isNotBlank(myBaseUrl)) {
        b.append(myBaseUrl);
        if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') {
          b.append('/');
        }
      }

      if (isNotBlank(myResourceType)) {
        b.append(myResourceType);
      }

      if (b.length() > 0 && isNotBlank(myUnqualifiedId)) {
        b.append('/');
      }

      if (isNotBlank(myUnqualifiedId)) {
        b.append(myUnqualifiedId);
      } else if (isNotBlank(myUnqualifiedVersionId)) {
        b.append('/');
      }

      if (isNotBlank(myUnqualifiedVersionId)) {
        b.append('/');
        b.append("_history");
        b.append('/');
        b.append(myUnqualifiedVersionId);
      }
      retVal = b.toString();
      super.setValue(retVal);
    }
    return retVal;
  }

  @Override
  public String getValueAsString() {
    return getValue();
  }

  @Override
  public String getVersionIdPart() {
    return myUnqualifiedVersionId;
  }

  public Long getVersionIdPartAsLong() {
    if (!hasVersionIdPart()) {
      return null;
    } else {
      return Long.parseLong(getVersionIdPart());
    }
  }

  /**
   * Returns true if this ID has a base url
   *
   * @see #getBaseUrl()
   */
  public boolean hasBaseUrl() {
    return isNotBlank(myBaseUrl);
  }

  @Override
  public int hashCode() {
    HashCodeBuilder b = new HashCodeBuilder();
    b.append(getValueAsString());
    return b.toHashCode();
  }

  @Override
  public boolean hasIdPart() {
    return isNotBlank(getIdPart());
  }

  @Override
  public boolean hasResourceType() {
    return isNotBlank(myResourceType);
  }

  @Override
  public boolean hasVersionIdPart() {
    return isNotBlank(getVersionIdPart());
  }

  /**
   * Returns <code>true</code> if this ID contains an absolute URL (in other
   * words, a URL starting with "http://" or "https://"
   */
  @Override
  public boolean isAbsolute() {
    if (StringUtils.isBlank(getValue())) {
      return false;
    }
    return isUrlAbsolute(getValue());
  }

  @Override
  public boolean isEmpty() {
    return super.isEmpty() && isBlank(getValue());
  }

  @Override
  public boolean isIdPartValid() {
    String id = getIdPart();
    if (StringUtils.isBlank(id)) {
      return false;
    }
    if (id.length() > 64) {
      return false;
    }
    for (int i = 0; i < id.length(); i++) {
      char nextChar = id.charAt(i);
      if (nextChar >= 'a' && nextChar <= 'z') {
        continue;
      }
      if (nextChar >= 'A' && nextChar <= 'Z') {
        continue;
      }
      if (nextChar >= '0' && nextChar <= '9') {
        continue;
      }
      if (nextChar == '-' || nextChar == '.') {
        continue;
      }
      return false;
    }
    return true;
  }

  /**
   * Returns <code>true</code> if the unqualified ID is a valid {@link Long}
   * value (in other words, it consists only of digits)
   */
  @Override
  public boolean isIdPartValidLong() {
    return isValidLong(getIdPart());
  }

  /**
   * Returns <code>true</code> if the ID is a local reference (in other words,
   * it begins with the '#' character)
   */
  @Override
  public boolean isLocal() {
    return defaultString(myUnqualifiedId).startsWith("#");
  }

  public boolean isUrn() {
    return defaultString(myUnqualifiedId).startsWith(URN_PREFIX);
  }

  @Override
  public boolean isVersionIdPartValidLong() {
    return isValidLong(getVersionIdPart());
  }

  /**
   * Set the value
   *
   * <p>
   * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally
   * represented in hex), a uuid, an oid, or any other combination of lowercase
   * letters, numerals, "-" and ".", with a length limit of 36 characters.
   * </p>
   * <p>
   * regex: [a-z0-9\-\.]{1,36}
   * </p>
   */
  @Override
  public IdType setValue(String theValue) {
    // TODO: add validation
    super.setValue(theValue);

    myHaveComponentParts = false;

    if (StringUtils.isBlank(theValue)) {
      myBaseUrl = null;
      super.setValue(null);
      myUnqualifiedId = null;
      myUnqualifiedVersionId = null;
      myResourceType = null;
    } else if (theValue.charAt(0) == '#' && theValue.length() > 1) {
      super.setValue(theValue);
      myBaseUrl = null;
      myUnqualifiedId = theValue;
      myUnqualifiedVersionId = null;
      myResourceType = null;
      myHaveComponentParts = true;
    } else if (theValue.startsWith(URN_PREFIX)) {
      myBaseUrl = null;
      myUnqualifiedId = theValue;
      myUnqualifiedVersionId = null;
      myResourceType = null;
      myHaveComponentParts = true;
    } else {
      int vidIndex = theValue.indexOf("/_history/");
      int idIndex;
      if (vidIndex != -1) {
        myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length());
        idIndex = theValue.lastIndexOf('/', vidIndex - 1);
        myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex);
      } else {
        idIndex = theValue.lastIndexOf('/');
        myUnqualifiedId = theValue.substring(idIndex + 1);
        myUnqualifiedVersionId = null;
      }

      myBaseUrl = null;
      if (idIndex <= 0) {
        myResourceType = null;
      } else {
        int typeIndex = theValue.lastIndexOf('/', idIndex - 1);
        if (typeIndex == -1) {
          myResourceType = theValue.substring(0, idIndex);
        } else {
          if (typeIndex > 0 && '/' == theValue.charAt(typeIndex - 1)) {
            typeIndex = theValue.indexOf('/', typeIndex + 1);
          }
          if (typeIndex >= idIndex) {
            // e.g. http://example.org/foo
            // 'foo' was the id but we're making that the resource type. Nullify the id part because we don't have an id.
            // Also set null value to the super.setValue() and enable myHaveComponentParts so it forces getValue() to properly
            // recreate the url
            myResourceType = myUnqualifiedId;
            myUnqualifiedId = null;
            super.setValue(null);
            myHaveComponentParts = true;
          } else {
            myResourceType = theValue.substring(typeIndex + 1, idIndex);
          }

          if (typeIndex > 4) {
            myBaseUrl = theValue.substring(0, typeIndex);
          }

        }
      }

    }
    return this;
  }

  /**
   * Set the value
   *
   * <p>
   * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally
   * represented in hex), a uuid, an oid, or any other combination of lowercase
   * letters, numerals, "-" and ".", with a length limit of 36 characters.
   * </p>
   * <p>
   * regex: [a-z0-9\-\.]{1,36}
   * </p>
   */
  @Override
  public void setValueAsString(String theValue) {
    setValue(theValue);
  }

  @Override
  public String toString() {
    return getValue();
  }

  /**
   * Returns a new IdType containing this IdType's values but with no server
   * base URL if one is present in this IdType. For example, if this IdType
   * contains the ID "http://foo/Patient/1", this method will return a new
   * IdType containing ID "Patient/1".
   */
  @Override
  public IdType toUnqualified() {
    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }
    return new IdType(getResourceType(), getIdPart(), getVersionIdPart());
  }

  @Override
  public IdType toUnqualifiedVersionless() {
    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }
    return new IdType(getResourceType(), getIdPart());
  }

  @Override
  public IdType toVersionless() {
    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }
    return new IdType(getBaseUrl(), getResourceType(), getIdPart(), null);
  }

  @Override
  public IdType withResourceType(String theResourceName) {
    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }
    return new IdType(theResourceName, getIdPart(), getVersionIdPart());
  }

  /**
   * Returns a view of this ID as a fully qualified URL, given a server base and
   * resource name (which will only be used if the ID does not already contain
   * those respective parts). Essentially, because IdType can contain either a
   * complete URL or a partial one (or even jut a simple ID), this method may be
   * used to translate into a complete URL.
   *
   * @param theServerBase
   *          The server base (e.g. "http://example.com/fhir")
   * @param theResourceType
   *          The resource name (e.g. "Patient")
   * @return A fully qualified URL for this ID (e.g.
   *         "http://example.com/fhir/Patient/1")
   */
  @Override
  public IdType withServerBase(String theServerBase, String theResourceType) {
    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }
    return new IdType(theServerBase, theResourceType, getIdPart(), getVersionIdPart());
  }

  /**
   * Creates a new instance of this ID which is identical, but refers to the
   * specific version of this resource ID noted by theVersion.
   *
   * @param theVersion The actual version string, e.g. "1". If theVersion is blank or null, returns the same as {@link #toVersionless()}}
   * @return A new instance of IdType which is identical, but refers to the
   *         specific version of this resource ID noted by theVersion.
   */
  @Override
  public IdType withVersion(String theVersion) {
    if (isBlank(theVersion)) {
      return toVersionless();
    }

    if (isLocal() || isUrn()) {
      return new IdType(getValueAsString());
    }

    String existingValue = getValue();

    int i = existingValue.indexOf("_history");
    String value;
    if (i > 1) {
      value = existingValue.substring(0, i - 1);
    } else {
      value = existingValue;
    }

    return new IdType(value + '/' + "_history" + '/' + theVersion);
  }

  private static boolean isUrlAbsolute(String theValue) {
    String value = theValue.toLowerCase();
    return value.startsWith("http://") || value.startsWith("https://");
  }

  private static boolean isValidLong(String id) {
    if (StringUtils.isBlank(id)) {
      return false;
    }
    for (int i = 0; i < id.length(); i++) {
      if (Character.isDigit(id.charAt(i)) == false) {
        return false;
      }
    }
    return true;
  }

  /**
   * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new,
   * randomly created UUID generated by {@link UUID#randomUUID()}
   */
  public static IdType newRandomUuid() {
    return new IdType("urn:uuid:" + UUID.randomUUID().toString());
  }

  /**
   * Retrieves the ID from the given resource instance
   */
  public static IdType of(IBaseResource theResouce) {
    if (theResouce == null) {
      throw new NullPointerException("theResource can not be null");
    } else {
      IIdType retVal = theResouce.getIdElement();
      if (retVal == null) {
        return null;
      } else if (retVal instanceof IdType) {
        return (IdType) retVal;
      } else {
        return new IdType(retVal.getValue());
      }
    }
  }

  private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) {
    if (theIdPart == null) {
      throw new NullPointerException("BigDecimal ID can not be null");
    }
    return theIdPart.toPlainString();
  }

  private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) {
    if (theIdPart == null) {
      throw new NullPointerException("Long ID can not be null");
    }
    return theIdPart.toString();
  }

  public String fhirType() {
    return "id";
  }

  @Override
  public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) {
    if (isNotBlank(theVersionIdPart)) {
      Validate.notBlank(theResourceType, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated");
      Validate.notBlank(theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated");
    }
    if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) {
      Validate.notBlank(theResourceType, "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated");
    }

    setValue(null);

    myBaseUrl = theBaseUrl;
    myResourceType = theResourceType;
    myUnqualifiedId = theIdPart;
    myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null);
    myHaveComponentParts = true;

    return this;
  }

}