// Written in the D programming language.

/**
 *
 * Tom's Obvious, Minimal Language (v1.0.0).
 *
 * License: $(HTTP https://github.com/Kripth/toml/blob/master/LICENSE, MIT)
 * Authors: Kripth
 * References: $(LINK https://github.com/toml-lang/toml/blob/master/README.md)
 * Source: $(HTTP https://github.com/Kripth/toml/blob/master/src/toml/toml.d, toml/_toml.d)
 *
 */
module toml.toml;

import std.algorithm : canFind, min, stripRight;
import std.array : Appender;
import std.ascii : newline;
import std.conv : text, to;
import std.datetime : Date, DateTimeD = DateTime, SysTime, TimeOfDayD = TimeOfDay;
import std.exception : assertThrown, enforce;
import std.string : indexOf, join, replace, strip;
import std.traits : isArray, isAssociativeArray, isFloatingPoint, isIntegral, isNumeric, KeyType;
import std.typecons : Tuple, tuple;
import std.utf : encode, UseReplacementDchar;

import toml.datetime : DateTime, TimeOfDay;

/**
 * Flags that control how a TOML document is parsed and encoded.
 */
enum TOMLOptions {
   none = 0x00,
   unquotedStrings = 0x01, /// allow unquoted strings as values when parsing
}

/**
 * TOML type enumeration.
 */
enum TOML_TYPE : byte {
   STRING, /// Indicates the type of a TOMLValue.
   INTEGER, /// ditto
   FLOAT, /// ditto
   OFFSET_DATETIME, /// ditto
   LOCAL_DATETIME, /// ditto
   LOCAL_DATE, /// ditto
   LOCAL_TIME, /// ditto
   ARRAY, /// ditto
   TABLE, /// ditto
   TRUE, /// ditto
   FALSE /// ditto
}

alias TOMLType = TOML_TYPE;
alias TOMLfloat = TOML_TYPE.FLOAT;

/**
 * Main table of a TOML document.
 * It works as a TOMLValue with the TOML_TYPE.TABLE type.
 */
struct TOMLDocument {

   public TOMLValue[string] table;

@safe scope:

   public this(TOMLValue[string] table) pure {
      this.table = table;
   }

   public this(TOMLValue value) pure {
      this(value.table);
   }

   public string toString() const {
      Appender!string appender;
      foreach (key, value; this.table) {
         appender.put(formatKey(key));
         appender.put(" = ");
         value.append(appender);
         appender.put(newline);
      }
      return appender.data;
   }

   alias table this;

}

/**
 * Value of a TOML value.
 */
struct TOMLValue {

   private union Store {
      string str;
      long integer;
      double floating;
      SysTime offsetDatetime;
      DateTime localDatetime;
      Date localDate;
      TimeOfDay localTime;
      TOMLValue[] array;
      TOMLValue[string] table;
   }

   private Store store;
   private TOML_TYPE _type;

@safe scope:

   public this(T)(T value) {
      static if (is(T == TOML_TYPE)) {
         this._type = value;
      } else {
         this.assign(value);
      }
   }

   public pure nothrow @property @safe @nogc TOML_TYPE type() return const {
      return this._type;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.STRING
    */
   public @property @trusted string str() return const pure {
      enforce!TOMLException(this._type == TOML_TYPE.STRING, "TOMLValue is not a string");
      return this.store.str;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.INTEGER
    */
   public @property @trusted long integer() return const pure {
      enforce!TOMLException(this._type == TOML_TYPE.INTEGER, "TOMLValue is not an integer");
      return this.store.integer;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.FLOAT
    */
   public @property @trusted double floating() return const pure {
      enforce!TOMLException(this._type == TOML_TYPE.FLOAT, "TOMLValue is not a float");
      return this.store.floating;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.TRUE or TOML_TYPE.FALSE
    */
   public @property @trusted bool boolean() return const pure {
      switch (this._type) {
         case TOML_TYPE.TRUE:
            return true;
         case TOML_TYPE.FALSE:
            return false;
         default:
            throw new TOMLException("TOMLValue is not a boolean");
      }
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.OFFSET_DATETIME
    */
   public @property @trusted ref inout(SysTime) offsetDatetime() return inout pure {
      enforce!TOMLException(this.type == TOML_TYPE.OFFSET_DATETIME, "TOMLValue is not an offset datetime");
      return this.store.offsetDatetime;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATETIME
    */
   public @property @trusted ref inout(DateTime) localDatetime() return inout pure {
      enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATETIME, "TOMLValue is not a local datetime");
      return this.store.localDatetime;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATE
    */
   public @property @trusted ref inout(Date) localDate() return inout pure {
      enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATE, "TOMLValue is not a local date");
      return this.store.localDate;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.LOCAL_TIME
    */
   public @property @trusted ref inout(TimeOfDay) localTime() return inout pure {
      enforce!TOMLException(this._type == TOML_TYPE.LOCAL_TIME, "TOMLValue is not a local time");
      return this.store.localTime;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.ARRAY
    */
   public @property @trusted ref inout(TOMLValue[]) array() return inout pure {
      enforce!TOMLException(this._type == TOML_TYPE.ARRAY, "TOMLValue is not an array");
      return this.store.array;
   }

   /**
    * Throws: TOMLException if type is not TOML_TYPE.TABLE
    */
   public @property @trusted ref inout(TOMLValue[string]) table() return inout pure {
      enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table");
      return this.store.table;
   }

   public inout(TOMLValue) opIndex(size_t index) return inout pure {
      return this.array[index];
   }

   public inout(TOMLValue)* opBinaryRight(string op : "in")(string key) return inout pure {
      return key in this.table;
   }

   public inout(TOMLValue) opIndex(string key) return inout pure {
      return this.table[key];
   }

   private static enum opApplyImpl = q{
      int result;
      foreach (string key, ref value; this.table) {
         result = dg(key, value);
         if (result) {
            break;
         }
      }
      return result;
   };

   static if (__VERSION__ >= 2099)
   {
      public int opApply(scope int delegate(string,       ref       TOMLValue) @safe   dg)      @safe              { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @safe   dg)      @safe              { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref       TOMLValue) @safe   dg)      @safe              { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe   dg)      @safe              { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @safe   dg)      @safe const        { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe   dg)      @safe const        { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref       TOMLValue) @safe   pure dg) @safe pure         { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @safe   pure dg) @safe pure         { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref       TOMLValue) @safe   pure dg) @safe pure         { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe   pure dg) @safe pure         { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @safe   pure dg) @safe pure const   { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @safe   pure dg) @safe pure const   { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref       TOMLValue) @system dg)      @system            { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @system dg)      @system            { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref       TOMLValue) @system dg)      @system            { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @system dg)      @system            { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @system dg)      @system const      { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @system dg)      @system const      { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref       TOMLValue) @system pure dg) @system pure       { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @system pure dg) @system pure       { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref       TOMLValue) @system pure dg) @system pure       { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @system pure dg) @system pure       { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string,       ref const TOMLValue) @system pure dg) @system pure const { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, scope ref const TOMLValue) @system pure dg) @system pure const { mixin(opApplyImpl); }
   }
   else
   {
      public int opApply(scope int delegate(string, ref       TOMLValue) @system dg) @system       { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, ref       TOMLValue) @safe dg)   @safe         { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, ref const TOMLValue) @system dg) @system const { mixin(opApplyImpl); }
      public int opApply(scope int delegate(string, ref const TOMLValue) @safe dg)   @safe const   { mixin(opApplyImpl); }
   }

   public void opAssign(T)(T value) pure {
      this.assign(value);
   }

   private void assign(T)(T value) @trusted pure {
      static if (is(T == TOMLValue)) {
         this.store = value.store;
         this._type = value._type;
      } else static if (is(T : string)) {
         this.store.str = value;
         this._type = TOML_TYPE.STRING;
      } else static if (isIntegral!T) {
         this.store.integer = value;
         this._type = TOML_TYPE.INTEGER;
      } else static if (isFloatingPoint!T) {
         this.store.floating = value.to!double;
         this._type = TOML_TYPE.FLOAT;
      } else static if (is(T == SysTime)) {
         this.store.offsetDatetime = value;
         this._type = TOML_TYPE.OFFSET_DATETIME;
      } else static if (is(T == DateTime)) {
         this.store.localDatetime = value;
         this._type = TOML_TYPE.LOCAL_DATETIME;
      } else static if (is(T == DateTimeD)) {
         this.store.localDatetime = DateTime(value.date, TimeOfDay(value.timeOfDay));
         this._type = TOML_TYPE.LOCAL_DATETIME;
      } else static if (is(T == Date)) {
         this.store.localDate = value;
         this._type = TOML_TYPE.LOCAL_DATE;
      } else static if (is(T == TimeOfDay)) {
         this.store.localTime = value;
         this._type = TOML_TYPE.LOCAL_TIME;
      } else static if (is(T == TimeOfDayD)) {
         this.store.localTime = TimeOfDay(value);
         this._type = TOML_TYPE.LOCAL_TIME;
      } else static if (isArray!T) {
         static if (is(T == TOMLValue[])) {
            if (value.length) {
               // verify that every element has the same type
               TOML_TYPE cmp = value[0].type;
               foreach (element; value[1 .. $]) {
                  enforce!TOMLException(element.type == cmp, "Array's values must be of the same type");
               }
            }
            alias data = value;
         } else {
            TOMLValue[] data;
            foreach (element; value) {
               data ~= TOMLValue(element);
            }
         }
         this.store.array = data;
         this._type = TOML_TYPE.ARRAY;
      } else static if (isAssociativeArray!T && is(KeyType!T : string)) {
         static if (is(T == TOMLValue[string])) {
            alias data = value;
         } else {
            TOMLValue[string] data;
            foreach (key, v; value) {
               data[key] = v;
            }
         }
         this.store.table = data;
         this._type = TOML_TYPE.TABLE;
      } else static if (is(T == bool)) {
         _type = value ? TOML_TYPE.TRUE : TOML_TYPE.FALSE;
      } else {
         static assert(0);
      }
   }

   public bool opEquals(T)(scope T value) const @trusted pure {
      static if (is(T == TOMLValue)) {
         if (this._type != value._type) {
            return false;
         }
         final switch (this.type) with (TOML_TYPE) {
            case STRING:
               return this.store.str == value.store.str;
            case INTEGER:
               return this.store.integer == value.store.integer;
            case FLOAT:
               return this.store.floating == value.store.floating;
            case OFFSET_DATETIME:
               return this.store.offsetDatetime == value.store.offsetDatetime;
            case LOCAL_DATETIME:
               return this.store.localDatetime == value.store.localDatetime;
            case LOCAL_DATE:
               return this.store.localDate == value.store.localDate;
            case LOCAL_TIME:
               return this.store.localTime == value.store.localTime;
            case ARRAY:
               return this.store.array == value.store.array;
               //case TABLE: return this.store.table == value.store.table; // causes errors
            case TABLE:
               return this.opEquals(value.store.table);
            case TRUE:
            case FALSE:
               return true;
         }
      } else static if (is(T : string)) {
         return this._type == TOML_TYPE.STRING && this.store.str == value;
      } else static if (isNumeric!T) {
         if (this._type == TOML_TYPE.INTEGER) {
            return this.store.integer == value;
         } else if (this._type == TOML_TYPE.FLOAT) {
            return this.store.floating == value;
         } else {
            return false;
         }
      } else static if (is(T == SysTime)) {
         return this._type == TOML_TYPE.OFFSET_DATETIME && this.store.offsetDatetime == value;
      } else static if (is(T == DateTime)) {
         return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value.dateTime
            && this.store.localDatetime.timeOfDay.fracSecs == value.timeOfDay.fracSecs;
      } else static if (is(T == DateTimeD)) {
         return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value;
      } else static if (is(T == Date)) {
         return this._type == TOML_TYPE.LOCAL_DATE && this.store.localDate == value;
      } else static if (is(T == TimeOfDay)) {
         return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime.timeOfDay == value.timeOfDay
            && this.store.localTime.fracSecs == value.fracSecs;
      } else static if (is(T == TimeOfDayD)) {
         return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime == value;
      } else static if (isArray!T) {
         if (this._type != TOML_TYPE.ARRAY || this.store.array.length != value.length) {
            return false;
         }
         foreach (i, element; this.store.array) {
            if (element != value[i]) {
               return false;
            }
         }
         return true;
      } else static if (isAssociativeArray!T && is(KeyType!T : string)) {
         if (this._type != TOML_TYPE.TABLE || this.store.table.length != value.length) {
            return false;
         }
         foreach (key, v; this.store.table) {
            auto cmp = key in value;
            if (cmp is null || v != *cmp) {
               return false;
            }
         }
         return true;
      } else static if (is(T == bool)) {
         return value ? _type == TOML_TYPE.TRUE : _type == TOML_TYPE.FALSE;
      } else {
         return false;
      }
   }

   size_t toHash() const @trusted @nogc pure nothrow {
      final switch (_type) with (TOML_TYPE) {
         case STRING:
            return hashOf(store.str);
         case INTEGER:
            return hashOf(store.integer);
         case FLOAT:
            return hashOf(store.floating);
         case OFFSET_DATETIME:
            return hashOf(store.offsetDatetime);
         case LOCAL_DATETIME:
            static if (__traits(compiles, () @nogc { hashOf(store.localDatetime); }))
               return hashOf(store.localDatetime);
            else
               return hashOf(tuple(store.localDatetime.date, store.localDatetime.timeOfDay));
         case LOCAL_DATE:
            return hashOf(store.localDate);
         case LOCAL_TIME:
            return hashOf(store.localTime);
         case ARRAY:
            return hashOf(store.array);
         case TABLE:
            return hashOf(store.table);
         case TRUE:
            return hashOf(true);
         case FALSE:
            return hashOf(false);
      }
   }

   public void append(Output)(scope ref Output appender) const @trusted {
      final switch (this._type) with (TOML_TYPE) {
         case STRING:
            appender.put(formatString(this.store.str));
            break;
         case INTEGER:
            appender.put(this.store.integer.to!string);
            break;
         case FLOAT:
            immutable str = this.store.floating.to!string;
            appender.put(str);
            if (!str.canFind('.') && !str.canFind('e')) {
               appender.put(".0");
            }
            break;
         case OFFSET_DATETIME:
            appender.put(this.store.offsetDatetime.toISOExtString());
            break;
         case LOCAL_DATETIME:
            appender.put(this.store.localDatetime.toISOExtString());
            break;
         case LOCAL_DATE:
            appender.put(this.store.localDate.toISOExtString());
            break;
         case LOCAL_TIME:
            appender.put(this.store.localTime.toISOExtString());
            break;
         case ARRAY:
            appender.put("[");
            foreach (i, value; this.store.array) {
               value.append(appender);
               if (i + 1 < this.store.array.length) {
                  appender.put(", ");
               }
            }
            appender.put("]");
            break;
         case TABLE:
            // display as an inline table
            appender.put("{ ");
            size_t i = 0;
            foreach (key, value; this.store.table) {
               appender.put(formatKey(key));
               appender.put(" = ");
               value.append(appender);
               if (++i != this.store.table.length) {
                  appender.put(", ");
               }
            }
            appender.put(" }");
            break;
         case TRUE:
            appender.put("true");
            break;
         case FALSE:
            appender.put("false");
            break;
      }
   }

   public string toString() const {
      Appender!string appender;
      this.append(appender);
      return appender.data;
   }

}

private string formatKey(scope return string str) pure @safe {
   foreach (c; str) {
      if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '-' && c != '_') {
         return formatString(str);
      }
   }
   return str;
}

private string formatString(scope return inout(char)[] str) pure @safe {
   Appender!string appender;
   appender.put('"');
   foreach (c; str) {
      switch (c) {
         case '"':
            appender.put("\\\"");
            break;
         case '\\':
            appender.put("\\\\");
            break;
         case '\b':
            appender.put("\\b");
            break;
         case '\f':
            appender.put("\\f");
            break;
         case '\n':
            appender.put("\\n");
            break;
         case '\r':
            appender.put("\\r");
            break;
         case '\t':
            appender.put("\\t");
            break;
         default:
            appender.put(c);
      }
   }
   appender.put('"');
   return appender.data;
}

/**
 * Parses a TOML document.
 * Params:
 *  data = String in toml format to parse. Slices out of this will be returned
 *    _iff_ `unquotedStrings` is enabled in the options and a `string` is passed
 *    into this function.
 *  options = Parsing option
 *
 * Returns: a TOMLDocument with the parsed data
 * Throws:
 *       TOMLParserException when the document's syntax is incorrect
 */
TOMLDocument parseTOML(scope const(char)[] data, TOMLOptions options = TOMLOptions.none) @safe {
   return parseTOMLImpl!true(data, options);
}
/// ditto
TOMLDocument parseTOML(scope return string data, TOMLOptions options = TOMLOptions.none) @safe {
   return parseTOMLImpl!false(data, options);
}

private TOMLDocument parseTOMLImpl(bool dupData)(scope const(char)[] data, TOMLOptions options = TOMLOptions.none) @safe {
   size_t index = 0;

   /**
    * Throws a TOMLParserException at the current line and column.
    */
   void error(string message) {
      if (index >= data.length) {
         index = data.length;
      }
      size_t i, line, column;
      while (i < index) {
         if (data[i++] == '\n') {
            line++;
            column = 0;
         } else {
            column++;
         }
      }
      throw new TOMLParserException(message, line + 1, column);
   }

   /**
    * Throws a TOMLParserException throught the error function if
    * cond is false.
    */
   void enforceParser(bool cond, lazy string message) {
      if (!cond) {
         error(message);
      }
   }

   TOMLValue[string] _ret;
   auto current = (() @trusted => &_ret)();

   string[][] tableNames;

   void setImpl(scope TOMLValue[string]* table, string[] keys, string[] original, TOMLValue value) {
      auto ptr = keys[0] in *table;
      if (keys.length == 1) {
         // should not be there
         enforceParser(ptr is null, "Key is already defined");
         (*table)[keys[0]] = value;
      } else {
         // must be a table
         if (ptr !is null) {
            enforceParser((*ptr).type == TOML_TYPE.TABLE, join(original[0 .. $ - keys.length],
                  ".") ~ " is already defined and is not a table");
         } else {
            (*table)[keys[0]] = (TOMLValue[string]).init;
         }
         setImpl((() @trusted => &((*table)[keys[0]].table()))(), keys[1 .. $], original, value);
      }
   }

   void set(string[] keys, TOMLValue value) {
      setImpl(current, keys, keys, value);
   }

   /**
    * Removes whitespace characters and comments.
    * Return: whether there's still data to read
    */
   bool clear(bool clear_newline = true)() {
      static if (clear_newline) {
         enum chars = " \t\r\n";
      } else {
         enum chars = " \t\r";
      }
      if (index < data.length) {
         if (chars.canFind(data[index])) {
            index++;
            return clear!clear_newline();
         } else if (data[index] == '#') {
            // skip until end of line
            while (++index < data.length && data[index] != '\n') {
            }
            static if (clear_newline) {
               index++; // point at the next character
               return clear();
            } else {
               return true;
            }
         } else {
            return true;
         }
      } else {
         return false;
      }
   }

   /**
    * Indicates whether the given character is valid in an unquoted key.
    */
   bool isValidKeyChar(immutable char c) {
      return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_';
   }

   string readQuotedString(bool multiline)() {
      Appender!string ret;
      bool backslash = false;
      while (index < data.length) {
         static if (!multiline) {
            enforceParser(data[index] != '\n', "Unterminated quoted string");
         }
         if (backslash) {
            void readUnicode(size_t size)() {
               enforceParser(index + size < data.length, "Invalid UTF-8 sequence");
               char[4] buffer;
               immutable len = encode!(UseReplacementDchar.yes)(buffer, cast(dchar)to!ulong(data[index + 1 .. index + 1 + size], 16));
               ret.put(buffer[0 .. len].idup);
               index += size;
            }

            switch (data[index]) {
               case '"':
                  ret.put('"');
                  break;
               case '\\':
                  ret.put('\\');
                  break;
               case 'b':
                  ret.put('\b');
                  break;
               case 't':
                  ret.put('\t');
                  break;
               case 'n':
                  ret.put('\n');
                  break;
               case 'f':
                  ret.put('\f');
                  break;
               case 'r':
                  ret.put('\r');
                  break;
               case 'u':
                  readUnicode!4();
                  break;
               case 'U':
                  readUnicode!8();
                  break;
               default:
                  static if (multiline) {
                     index++;
                     if (clear()) {
                        // remove whitespace characters until next valid character
                        index--;
                        break;
                     }
                  }
                  enforceParser(false, "Invalid escape sequence: '\\" ~ (index < data.length ? [cast(immutable) data[index]] : "EOF") ~ "'");
            }
            backslash = false;
         } else {
            if (data[index] == '\\') {
               backslash = true;
            } else if (data[index] == '"') {
               // string closed
               index++;
               static if (multiline) {
                  // control that the string is really closed
                  if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") {
                     index += 2;
                     return ret.data.stripFirstLine;
                  } else {
                     ret.put("\"");
                     continue;
                  }
               } else {
                  return ret.data;
               }
            } else {
               static if (multiline) {
                  mixin(doLineConversion);
               }
               ret.put(data[index]);
            }
         }
         index++;
      }
      error("Expecting \" (double quote) but found EOF");
      assert(0);
   }

   string readSimpleQuotedString(bool multiline)() {
      Appender!string ret;
      while (index < data.length) {
         static if (!multiline) {
            enforceParser(data[index] != '\n', "Unterminated quoted string");
         }
         if (data[index] == '\'') {
            // closed
            index++;
            static if (multiline) {
               // there must be 3 of them
               if (index + 2 <= data.length && data[index .. index + 2] == "''") {
                  index += 2;
                  return ret.data.stripFirstLine;
               } else {
                  ret.put("'");
               }
            } else {
               return ret.data;
            }
         } else {
            static if (multiline) {
               mixin(doLineConversion);
            }
            ret.put(data[index++]);
         }
      }
      error("Expecting ' (single quote) but found EOF");
      assert(0);
   }

   const(char)[] removeUnderscores(scope return const(char)[] strInput, scope const(char)[][] ranges...) @safe {
      bool checkRange(char c) {
         foreach (range; ranges) {
            if (c >= range[0] && c <= range[1]) {
               return true;
            }
         }
         return false;
      }

      auto str = strInput;

      bool underscore = false;
      for (size_t i = 0; i < str.length; i++) {
         if (str[i] == '_') {
            if (underscore || i == 0 || i == str.length - 1 || !checkRange(str[i - 1]) || !checkRange(str[i + 1])) {
               throw new Exception("");
            }
            str = str[0 .. i] ~ str[i + 1 .. $];
            i--;
            underscore = true;
         } else {
            underscore = false;
         }
      }
      return str;
   }

   TOMLValue readSpecial() {
      immutable start = index;
      while (index < data.length && !"\t\r\n,]}#".canFind(data[index])) {
         index++;
      }
      const(char)[] ret = data[start .. index].stripRight(' ');
      enforceParser(ret.length > 0, "Invalid empty value");
      switch (ret) {
         case "true":
            return TOMLValue(true);
         case "false":
            return TOMLValue(false);
         case "inf":
         case "+inf":
            return TOMLValue(double.infinity);
         case "-inf":
            return TOMLValue(-double.infinity);
         case "nan":
         case "+nan":
            return TOMLValue(double.nan);
         case "-nan":
            return TOMLValue(-double.nan);
         default:
            const original = ret;
            try {
               if (ret.length >= 10 && ret[4] == '-' && ret[7] == '-') {
                  // date or datetime
                  if (ret.length >= 19 && (ret[10] == 'T' || ret[10] == ' ') && ret[13] == ':' && ret[16] == ':') {
                     // datetime
                     if (ret[10] == ' ') {
                        ret = ret[0 .. 10] ~ 'T' ~ ret[11 .. $];
                     }
                     if (ret[19 .. $].canFind("-") || ret[$ - 1] == 'Z') {
                        // has timezone
                        return TOMLValue(SysTime.fromISOExtString(ret));
                     } else {
                        // is space allowed instead of T?
                        return TOMLValue(DateTime.fromISOExtString(ret));
                     }
                  } else {
                     return TOMLValue(Date.fromISOExtString(ret));
                  }
               } else if (ret.length >= 8 && ret[2] == ':' && ret[5] == ':') {
                  return TOMLValue(TimeOfDay.fromISOExtString(ret));
               }
               if (ret.length > 2 && ret[0] == '0') {
                  switch (ret[1]) {
                     case 'x':
                        return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "09", "AZ", "az"), 16));
                     case 'o':
                        return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "08"), 8));
                     case 'b':
                        return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "01"), 2));
                     default:
                        break;
                  }
               }
               if (ret.canFind('.') || ret.canFind('e') || ret.canFind('E')) {
                  return TOMLValue(to!double(removeUnderscores(ret, "09")));
               } else {
                  if (ret[0] != '0' || ret.length == 1) {
                     return TOMLValue(to!long(removeUnderscores(ret, "09")));
                  }
               }
            } catch (Exception) {
            }
            // not a valid value at this point
            if (options & TOMLOptions.unquotedStrings) {
               static if (dupData)
                  return TOMLValue(original.idup);
               else
                  return TOMLValue((() @trusted => cast(string) original)());
            } else {
               error(text("Invalid type: '", original.idup, "'"));
            }
            assert(0);
      }
   }

   string readKey() {
      enforceParser(index < data.length, "Key declaration expected but found EOF");
      string ret;
      if (data[index] == '"') {
         index++;
         ret = readQuotedString!false();
      } else if (data[index] == '\'') {
         index++;
         ret = readSimpleQuotedString!false();
      } else {
         Appender!string appender;
         while (index < data.length && isValidKeyChar(data[index])) {
            appender.put(data[index++]);
         }
         ret = appender.data;
         enforceParser(ret.length != 0, "Key is empty or contains invalid characters");
      }
      return ret;
   }

   string[] readKeys() {
      string[] keys;
      index--;
      do {
         index++;
         clear!false();
         keys ~= readKey();
         clear!false();
      }
      while (index < data.length && data[index] == '.');
      enforceParser(keys.length != 0, "Key cannot be empty");
      return keys;
   }

   TOMLValue readValue() {
      if (index < data.length) {
         switch (data[index++]) {
            case '"':
               if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") {
                  index += 2;
                  return TOMLValue(readQuotedString!true());
               } else {
                  return TOMLValue(readQuotedString!false());
               }
            case '\'':
               if (index + 2 <= data.length && data[index .. index + 2] == "''") {
                  index += 2;
                  return TOMLValue(readSimpleQuotedString!true());
               } else {
                  return TOMLValue(readSimpleQuotedString!false());
               }
            case '[':
               clear();
               TOMLValue[] array;
               bool comma = true;
               while (data[index] != ']') { //TODO check range error
                  enforceParser(comma, "Elements of the array must be separated with a comma");
                  array ~= readValue();
                  clear!false(); // spaces allowed between elements and commas
                  if (data[index] == ',') { //TODO check range error
                     index++;
                     comma = true;
                  } else {
                     comma = false;
                  }
                  clear(); // spaces and newlines allowed between elements
               }
               index++;
               return TOMLValue(array);
            case '{':
               clear!false();
               TOMLValue[string] table;
               bool comma = true;
               while (data[index] != '}') { //TODO check range error
                  enforceParser(comma, "Elements of the table must be separated with a comma");
                  auto keys = readKeys();
                  enforceParser(clear!false() && data[index++] == '=' && clear!false(), "Expected value after key declaration");
                  setImpl((() @trusted => &table)(), keys, keys, readValue());
                  enforceParser(clear!false(), "Expected ',' or '}' but found " ~ (index < data.length ? "EOL" : "EOF"));
                  if (data[index] == ',') {
                     index++;
                     comma = true;
                  } else {
                     comma = false;
                  }
                  clear!false();
               }
               index++;
               return TOMLValue(table);
            default:
               index--;
               break;
         }
      }
      return readSpecial();
   }

   void readKeyValue(string[] keys) {
      if (clear()) {
         enforceParser(data[index++] == '=', "Expected '=' after key declaration");
         if (clear!false()) {
            set(keys, readValue());
            // there must be nothing after the key/value declaration except comments and whitespaces
            if (clear!false())
               enforceParser(data[index] == '\n', "Invalid characters after value declaration: " ~ data[index]);
         } else {
            //TODO throw exception (missing value)
         }
      } else {
         //TODO throw exception (missing value)
      }
   }

   void next() @safe {
      if (data[index] == '[') {
         // reset base
         current = (() @trusted => &_ret)();
         index++;
         bool array = false;
         if (index < data.length && data[index] == '[') {
            index++;
            array = true;
         }
         string[] keys = readKeys();
         enforceParser(index < data.length && data[index++] == ']', "Invalid " ~ (array ? "array" : "table") ~ " key declaration");
         if (array) {
            enforceParser(index < data.length && data[index++] == ']', "Invalid array key declaration");
         }
         if (!array) {
            //TODO only enforce if every key is a table
            enforceParser(!tableNames.canFind(keys), "Table name has already been directly defined");
            tableNames ~= keys;
         }
         void update(string key, bool allowArray = true) {
            if (key !in *current) {
               set([key], TOMLValue(TOML_TYPE.TABLE));
            }
            auto ret = (*current)[key];
            if (ret.type == TOML_TYPE.TABLE) {
               current = (() @trusted => &((*current)[key].table()))();
            } else if (allowArray && ret.type == TOML_TYPE.ARRAY) {
               current = (() @trusted => &((*current)[key].array[$ - 1].table()))();
            } else {
               error("Invalid type");
            }
         }

         foreach (immutable key; keys[0 .. $ - 1]) {
            update(key);
         }
         if (array) {
            auto exist = keys[$ - 1] in *current;
            if (exist) {
               //TODO must be an array
               (*exist).array ~= TOMLValue(TOML_TYPE.TABLE);
            } else {
               set([keys[$ - 1]], TOMLValue([TOMLValue(TOML_TYPE.TABLE)]));
            }
            current = (() @trusted => &((*current)[keys[$ - 1]].array[$ - 1].table()))();
         } else {
            update(keys[$ - 1], false);
         }
      } else {
         readKeyValue(readKeys());
      }

   }

   while (clear()) {
      next();
   }

   return TOMLDocument(_ret);

}

private @property string stripFirstLine(string data) pure @safe {
   size_t i = 0;
   while (i < data.length && data[i] != '\n') {
      i++;
   }
   if (data[0 .. i].strip.length == 0) {
      return data[i + 1 .. $];
   } else {
      return data;
   }
}

version (Windows) {
   // convert posix's line ending to windows'
   private enum doLineConversion = q{
      if(data[index] == '\n' && index != 0 && data[index-1] != '\r') {
         index++;
         ret.put("\r\n");
         continue;
      }
   };
} else {
   // convert windows' line ending to posix's
   private enum doLineConversion = q{
      if(data[index] == '\r' && index + 1 < data.length && data[index+1] == '\n') {
         index += 2;
         ret.put("\n");
         continue;
      }
   };
}

/**
 * Exception thrown on generic TOML errors.
 */
class TOMLException : Exception {

   public this(string message, string file = __FILE__, size_t line = __LINE__) pure @safe scope {
      super(message, file, line);
   }

}

/**
 * Exception thrown during the parsing of TOML document.
 */
class TOMLParserException : TOMLException {

   private Tuple!(size_t, "line", size_t, "column") _position;

   public this(string message, size_t line, size_t column, string file = __FILE__, size_t _line = __LINE__) pure @safe scope {
      super(message ~ " (" ~ to!string(line) ~ ":" ~ to!string(column) ~ ")", file, _line);
      this._position.line = line;
      this._position.column = column;
   }

   /**
    * Gets the position (line and column) where the parsing expection
    * has occured.
    */
   public pure nothrow @property @safe @nogc auto position() scope {
      return this._position;
   }

}
