import { decode as _decode } from '../decode.js';
import {
  Token,
  Type
} from '../token.js';
import { decodeCodePointsArray } from '../byte-utils.js';
import { decodeErrPrefix } from '../common.js';
class Tokenizer {
  constructor(data, options = {}) {
    this.pos = 0;
    this.data = data;
    this.options = options;
    this.modeStack = ['value'];
    this.lastToken = '';
  }
  done() {
    return this.pos >= this.data.length;
  }
  ch() {
    return this.data[this.pos];
  }
  currentMode() {
    return this.modeStack[this.modeStack.length - 1];
  }
  skipWhitespace() {
    let c = this.ch();
    while (c === 32 || c === 9 || c === 13 || c === 10) {
      c = this.data[++this.pos];
    }
  }
  expect(str) {
    if (this.data.length - this.pos < str.length) {
      throw new Error(`${ decodeErrPrefix } unexpected end of input at position ${ this.pos }`);
    }
    for (let i = 0; i < str.length; i++) {
      if (this.data[this.pos++] !== str[i]) {
        throw new Error(`${ decodeErrPrefix } unexpected token at position ${ this.pos }, expected to find '${ String.fromCharCode(...str) }'`);
      }
    }
  }
  parseNumber() {
    const startPos = this.pos;
    let negative = false;
    let float = false;
    const swallow = chars => {
      while (!this.done()) {
        const ch = this.ch();
        if (chars.includes(ch)) {
          this.pos++;
        } else {
          break;
        }
      }
    };
    if (this.ch() === 45) {
      negative = true;
      this.pos++;
    }
    if (this.ch() === 48) {
      this.pos++;
      if (this.ch() === 46) {
        this.pos++;
        float = true;
      } else {
        return new Token(Type.uint, 0, this.pos - startPos);
      }
    }
    swallow([
      48,
      49,
      50,
      51,
      52,
      53,
      54,
      55,
      56,
      57
    ]);
    if (negative && this.pos === startPos + 1) {
      throw new Error(`${ decodeErrPrefix } unexpected token at position ${ this.pos }`);
    }
    if (!this.done() && this.ch() === 46) {
      if (float) {
        throw new Error(`${ decodeErrPrefix } unexpected token at position ${ this.pos }`);
      }
      float = true;
      this.pos++;
      swallow([
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57
      ]);
    }
    if (!this.done() && (this.ch() === 101 || this.ch() === 69)) {
      float = true;
      this.pos++;
      if (!this.done() && (this.ch() === 43 || this.ch() === 45)) {
        this.pos++;
      }
      swallow([
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57
      ]);
    }
    const numStr = String.fromCharCode.apply(null, this.data.subarray(startPos, this.pos));
    const num = parseFloat(numStr);
    if (float) {
      return new Token(Type.float, num, this.pos - startPos);
    }
    if (this.options.allowBigInt !== true || Number.isSafeInteger(num)) {
      return new Token(num >= 0 ? Type.uint : Type.negint, num, this.pos - startPos);
    }
    return new Token(num >= 0 ? Type.uint : Type.negint, BigInt(numStr), this.pos - startPos);
  }
  parseString() {
    if (this.ch() !== 34) {
      throw new Error(`${ decodeErrPrefix } unexpected character at position ${ this.pos }; this shouldn't happen`);
    }
    this.pos++;
    for (let i = this.pos, l = 0; i < this.data.length && l < 65536; i++, l++) {
      const ch = this.data[i];
      if (ch === 92 || ch < 32 || ch >= 128) {
        break;
      }
      if (ch === 34) {
        const str = String.fromCharCode.apply(null, this.data.subarray(this.pos, i));
        this.pos = i + 1;
        return new Token(Type.string, str, l);
      }
    }
    const startPos = this.pos;
    const chars = [];
    const readu4 = () => {
      if (this.pos + 4 >= this.data.length) {
        throw new Error(`${ decodeErrPrefix } unexpected end of unicode escape sequence at position ${ this.pos }`);
      }
      let u4 = 0;
      for (let i = 0; i < 4; i++) {
        let ch = this.ch();
        if (ch >= 48 && ch <= 57) {
          ch -= 48;
        } else if (ch >= 97 && ch <= 102) {
          ch = ch - 97 + 10;
        } else if (ch >= 65 && ch <= 70) {
          ch = ch - 65 + 10;
        } else {
          throw new Error(`${ decodeErrPrefix } unexpected unicode escape character at position ${ this.pos }`);
        }
        u4 = u4 * 16 + ch;
        this.pos++;
      }
      return u4;
    };
    const readUtf8Char = () => {
      const firstByte = this.ch();
      let codePoint = null;
      let bytesPerSequence = firstByte > 239 ? 4 : firstByte > 223 ? 3 : firstByte > 191 ? 2 : 1;
      if (this.pos + bytesPerSequence > this.data.length) {
        throw new Error(`${ decodeErrPrefix } unexpected unicode sequence at position ${ this.pos }`);
      }
      let secondByte, thirdByte, fourthByte, tempCodePoint;
      switch (bytesPerSequence) {
      case 1:
        if (firstByte < 128) {
          codePoint = firstByte;
        }
        break;
      case 2:
        secondByte = this.data[this.pos + 1];
        if ((secondByte & 192) === 128) {
          tempCodePoint = (firstByte & 31) << 6 | secondByte & 63;
          if (tempCodePoint > 127) {
            codePoint = tempCodePoint;
          }
        }
        break;
      case 3:
        secondByte = this.data[this.pos + 1];
        thirdByte = this.data[this.pos + 2];
        if ((secondByte & 192) === 128 && (thirdByte & 192) === 128) {
          tempCodePoint = (firstByte & 15) << 12 | (secondByte & 63) << 6 | thirdByte & 63;
          if (tempCodePoint > 2047 && (tempCodePoint < 55296 || tempCodePoint > 57343)) {
            codePoint = tempCodePoint;
          }
        }
        break;
      case 4:
        secondByte = this.data[this.pos + 1];
        thirdByte = this.data[this.pos + 2];
        fourthByte = this.data[this.pos + 3];
        if ((secondByte & 192) === 128 && (thirdByte & 192) === 128 && (fourthByte & 192) === 128) {
          tempCodePoint = (firstByte & 15) << 18 | (secondByte & 63) << 12 | (thirdByte & 63) << 6 | fourthByte & 63;
          if (tempCodePoint > 65535 && tempCodePoint < 1114112) {
            codePoint = tempCodePoint;
          }
        }
      }
      if (codePoint === null) {
        codePoint = 65533;
        bytesPerSequence = 1;
      } else if (codePoint > 65535) {
        codePoint -= 65536;
        chars.push(codePoint >>> 10 & 1023 | 55296);
        codePoint = 56320 | codePoint & 1023;
      }
      chars.push(codePoint);
      this.pos += bytesPerSequence;
    };
    while (!this.done()) {
      const ch = this.ch();
      let ch1;
      switch (ch) {
      case 92:
        this.pos++;
        if (this.done()) {
          throw new Error(`${ decodeErrPrefix } unexpected string termination at position ${ this.pos }`);
        }
        ch1 = this.ch();
        this.pos++;
        switch (ch1) {
        case 34:
        case 39:
        case 92:
        case 47:
          chars.push(ch1);
          break;
        case 98:
          chars.push(8);
          break;
        case 116:
          chars.push(9);
          break;
        case 110:
          chars.push(10);
          break;
        case 102:
          chars.push(12);
          break;
        case 114:
          chars.push(13);
          break;
        case 117:
          chars.push(readu4());
          break;
        default:
          throw new Error(`${ decodeErrPrefix } unexpected string escape character at position ${ this.pos }`);
        }
        break;
      case 34:
        this.pos++;
        return new Token(Type.string, decodeCodePointsArray(chars), this.pos - startPos);
      default:
        if (ch < 32) {
          throw new Error(`${ decodeErrPrefix } invalid control character at position ${ this.pos }`);
        } else if (ch < 128) {
          chars.push(ch);
          this.pos++;
        } else {
          readUtf8Char();
        }
      }
    }
    throw new Error(`${ decodeErrPrefix } unexpected end of string at position ${ this.pos }`);
  }
  parseValue() {
    switch (this.ch()) {
    case 123:
      this.modeStack.push('obj-start');
      this.pos++;
      return new Token(Type.map, Infinity, 1);
    case 91:
      this.modeStack.push('array-start');
      this.pos++;
      return new Token(Type.array, Infinity, 1);
    case 34: {
        return this.parseString();
      }
    case 110:
      this.expect([
        110,
        117,
        108,
        108
      ]);
      return new Token(Type.null, null, 4);
    case 102:
      this.expect([
        102,
        97,
        108,
        115,
        101
      ]);
      return new Token(Type.false, false, 5);
    case 116:
      this.expect([
        116,
        114,
        117,
        101
      ]);
      return new Token(Type.true, true, 4);
    case 45:
    case 48:
    case 49:
    case 50:
    case 51:
    case 52:
    case 53:
    case 54:
    case 55:
    case 56:
    case 57:
      return this.parseNumber();
    default:
      throw new Error(`${ decodeErrPrefix } unexpected character at position ${ this.pos }`);
    }
  }
  next() {
    this.skipWhitespace();
    switch (this.currentMode()) {
    case 'value':
      this.modeStack.pop();
      return this.parseValue();
    case 'array-value': {
        this.modeStack.pop();
        if (this.ch() === 93) {
          this.pos++;
          this.skipWhitespace();
          return new Token(Type.break, undefined, 1);
        }
        if (this.ch() !== 44) {
          throw new Error(`${ decodeErrPrefix } unexpected character at position ${ this.pos }, was expecting array delimiter but found '${ String.fromCharCode(this.ch()) }'`);
        }
        this.pos++;
        this.modeStack.push('array-value');
        this.skipWhitespace();
        return this.parseValue();
      }
    case 'array-start': {
        this.modeStack.pop();
        if (this.ch() === 93) {
          this.pos++;
          this.skipWhitespace();
          return new Token(Type.break, undefined, 1);
        }
        this.modeStack.push('array-value');
        this.skipWhitespace();
        return this.parseValue();
      }
    case 'obj-key':
      if (this.ch() === 125) {
        this.modeStack.pop();
        this.pos++;
        this.skipWhitespace();
        return new Token(Type.break, undefined, 1);
      }
      if (this.ch() !== 44) {
        throw new Error(`${ decodeErrPrefix } unexpected character at position ${ this.pos }, was expecting object delimiter but found '${ String.fromCharCode(this.ch()) }'`);
      }
      this.pos++;
      this.skipWhitespace();
    case 'obj-start': {
        this.modeStack.pop();
        if (this.ch() === 125) {
          this.pos++;
          this.skipWhitespace();
          return new Token(Type.break, undefined, 1);
        }
        const token = this.parseString();
        this.skipWhitespace();
        if (this.ch() !== 58) {
          throw new Error(`${ decodeErrPrefix } unexpected character at position ${ this.pos }, was expecting key/value delimiter ':' but found '${ String.fromCharCode(this.ch()) }'`);
        }
        this.pos++;
        this.modeStack.push('obj-value');
        return token;
      }
    case 'obj-value': {
        this.modeStack.pop();
        this.modeStack.push('obj-key');
        this.skipWhitespace();
        return this.parseValue();
      }
    default:
      throw new Error(`${ decodeErrPrefix } unexpected parse state at position ${ this.pos }; this shouldn't happen`);
    }
  }
}
function decode(data, options) {
  options = Object.assign({ tokenizer: new Tokenizer(data, options) }, options);
  return _decode(data, options);
}
export {
  decode,
  Tokenizer
};