package com.thaiopensource.datatype.xsd;

import com.thaiopensource.datatype.xsd.regex.RegexEngine;
import com.thaiopensource.datatype.xsd.regex.RegexSyntaxException;
import org.relaxng.datatype.Datatype;
import org.relaxng.datatype.DatatypeBuilder;
import org.relaxng.datatype.DatatypeException;
import org.relaxng.datatype.DatatypeLibrary;

import java.util.HashMap;
import java.util.Map;

public class DatatypeLibraryImpl implements DatatypeLibrary {
  private final Map<String, DatatypeBase> typeMap = new HashMap<String, DatatypeBase>();
  private final RegexEngine regexEngine;

  static private final String LONG_MAX = "9223372036854775807";
  static private final String LONG_MIN = "-9223372036854775808";
  static private final String INT_MAX = "2147483647";
  static private final String INT_MIN = "-2147483648";
  static private final String SHORT_MAX = "32767";
  static private final String SHORT_MIN = "-32768";
  static private final String BYTE_MAX = "127";
  static private final String BYTE_MIN = "-128";

  static private final String UNSIGNED_LONG_MAX = "18446744073709551615";
  static private final String UNSIGNED_INT_MAX = "4294967295";
  static private final String UNSIGNED_SHORT_MAX = "65535";
  static private final String UNSIGNED_BYTE_MAX = "255";
  // Follow RFC 3066 syntax.
  static private final String LANGUAGE_PATTERN = "[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*";

  public DatatypeLibraryImpl(RegexEngine regexEngine) {
    this.regexEngine = regexEngine;
    typeMap.put("string", new StringDatatype());
    typeMap.put("normalizedString", new CdataDatatype());
    typeMap.put("token", new TokenDatatype());
    typeMap.put("boolean", new BooleanDatatype());
    typeMap.put("untypedAtomic", new UntypedAtomicDatatype());
    typeMap.put("anyAtomicType", new AnyAtomicTypeDatatype());

    DatatypeBase decimalType = new DecimalDatatype();
    typeMap.put("decimal", decimalType);
    DatatypeBase integerType = new IntegerRestrictDatatype(decimalType);
    typeMap.put("integer", integerType);
    typeMap.put("nonPositiveInteger", restrictMax(integerType, "0"));
    typeMap.put("negativeInteger", restrictMax(integerType, "-1"));
    typeMap.put("long", restrictMax(restrictMin(integerType, LONG_MIN), LONG_MAX));
    typeMap.put("int", restrictMax(restrictMin(integerType, INT_MIN), INT_MAX));
    typeMap.put("short", restrictMax(restrictMin(integerType, SHORT_MIN), SHORT_MAX));
    typeMap.put("byte", restrictMax(restrictMin(integerType, BYTE_MIN), BYTE_MAX));
    DatatypeBase nonNegativeIntegerType = restrictMin(integerType, "0");
    typeMap.put("nonNegativeInteger", nonNegativeIntegerType);
    typeMap.put("unsignedLong", restrictMax(nonNegativeIntegerType, UNSIGNED_LONG_MAX));
    typeMap.put("unsignedInt", restrictMax(nonNegativeIntegerType, UNSIGNED_INT_MAX));
    typeMap.put("unsignedShort", restrictMax(nonNegativeIntegerType, UNSIGNED_SHORT_MAX));
    typeMap.put("unsignedByte", restrictMax(nonNegativeIntegerType, UNSIGNED_BYTE_MAX));
    typeMap.put("positiveInteger", restrictMin(integerType, "1"));
    typeMap.put("double", new DoubleDatatype());
    typeMap.put("float", new FloatDatatype());

    typeMap.put("Name", new NameDatatype());
    typeMap.put("QName", new QNameDatatype());

    DatatypeBase ncNameType = new NCNameDatatype();
    typeMap.put("NCName", ncNameType);

    DatatypeBase nmtokenDatatype = new NmtokenDatatype();
    typeMap.put("NMTOKEN", nmtokenDatatype);
    typeMap.put("NMTOKENS", list(nmtokenDatatype));

    typeMap.put("ID", new IdDatatype());
    DatatypeBase idrefType = new IdrefDatatype();
    typeMap.put("IDREF", idrefType);
    typeMap.put("IDREFS", list(idrefType));

    typeMap.put("NOTATION", new QNameDatatype());

    typeMap.put("base64Binary", new Base64BinaryDatatype());
    typeMap.put("hexBinary", new HexBinaryDatatype());
    typeMap.put("anyURI", new AnyUriDatatype());
    typeMap.put("language", new RegexDatatype(LANGUAGE_PATTERN) {
      String getLexicalSpaceKey() {
        return "language";
      }
    });

    typeMap.put("dateTime", new DateTimeDatatype("Y-M-DTt"));
    typeMap.put("time", new DateTimeDatatype("t"));
    typeMap.put("date", new DateTimeDatatype("Y-M-D"));
    typeMap.put("gYearMonth", new DateTimeDatatype("Y-M"));
    typeMap.put("gYear", new DateTimeDatatype("Y"));
    typeMap.put("gMonthDay", new DateTimeDatatype("--M-D"));
    typeMap.put("gDay", new DateTimeDatatype("---D"));
    typeMap.put("gMonth", new DateTimeDatatype("--M"));

    DatatypeBase entityType = new EntityDatatype();
    typeMap.put("ENTITY", entityType);
    typeMap.put("ENTITIES", list(entityType));
    // Partially implemented
    typeMap.put("duration", new DurationDatatype());
  }

  public DatatypeBuilder createDatatypeBuilder(String localName) throws DatatypeException {
    DatatypeBase base = typeMap.get(localName);
    if (base == null)
      throw new DatatypeException();
    if (base instanceof RegexDatatype) {
      try {
        ((RegexDatatype)base).compile(getRegexEngine());
      }
      catch (RegexSyntaxException e) {
        throw new DatatypeException(DatatypeBuilderImpl.localizer.message("regex_internal_error", localName));
      }
    }
    return new DatatypeBuilderImpl(this, base);
  }

  RegexEngine getRegexEngine() throws DatatypeException {
    if (regexEngine == null)
      throw new DatatypeException(DatatypeBuilderImpl.localizer.message("regex_impl_not_found"));
    return regexEngine;
  }

  private static DatatypeBase restrictMax(DatatypeBase base, String limit) {
    try {
      return new MaxInclusiveRestrictDatatype(base, base.getValue(limit, null), limit);
    }
    catch (DatatypeException e) {
      throw new AssertionError();
    }
  }

  private static DatatypeBase restrictMin(DatatypeBase base, String limit) {
    try {
      return new MinInclusiveRestrictDatatype(base, base.getValue(limit, null), limit);
    }
    catch (DatatypeException e) {
      throw new AssertionError();
    }
  }

  private static DatatypeBase list(DatatypeBase base) {
    return new MinLengthRestrictDatatype(new ListDatatype(base), 1);
  }

  public Datatype createDatatype(String type) throws DatatypeException {
    return createDatatypeBuilder(type).createDatatype();
  }
}
