package com.alibaba.druid.support.json;

import com.alibaba.druid.sql.parser.CharTypes;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class JSONParser {
   private String text;
   private int index = 0;
   private char ch;
   private Token token;
   private String stringValue;
   private long longValue;
   private double doubleValue;

   public JSONParser(String text) {
      this.text = text;
      this.ch = text.charAt(0);
      this.nextToken();
   }

   public Object parse() {
      if (this.token == Token.LBRACE) {
         return this.parseMap();
      } else if (this.token != Token.INT) {
         if (this.token == Token.DOUBLE) {
            Object value = this.doubleValue;
            this.nextToken();
            return value;
         } else if (this.token == Token.STRING) {
            Object value = this.stringValue;
            this.nextToken();
            return value;
         } else if (this.token == Token.LBRACKET) {
            return this.parseArray();
         } else if (this.token == Token.TRUE) {
            this.nextToken();
            return true;
         } else if (this.token == Token.FALSE) {
            this.nextToken();
            return false;
         } else if (this.token == Token.NULL) {
            this.nextToken();
            return null;
         } else {
            throw new IllegalArgumentException("illegal token : " + this.token);
         }
      } else {
         Object value;
         if (this.longValue >= -2147483648L && this.longValue <= 2147483647L) {
            value = (int)this.longValue;
         } else {
            value = this.longValue;
         }

         this.nextToken();
         return value;
      }
   }

   public List<Object> parseArray() {
      this.accept(Token.LBRACKET);
      ArrayList<Object> list = new ArrayList();

      while(this.token != Token.RBRACKET) {
         if (this.token == Token.COMMA) {
            this.nextToken();
         } else {
            Object item = this.parse();
            list.add(item);
         }
      }

      this.accept(Token.RBRACKET);
      return list;
   }

   public Map<String, Object> parseMap() {
      this.accept(Token.LBRACE);
      Map<String, Object> map = new LinkedHashMap();

      while(this.token != Token.RBRACE) {
         if (this.token == Token.COMMA) {
            this.nextToken();
         } else {
            if (this.token != Token.STRING) {
               throw new IllegalArgumentException("illegal json, " + this.token + " : " + this.text);
            }

            String key = this.stringValue;
            this.nextToken();
            this.accept(Token.COLON);
            Object value = this.parse();
            map.put(key, value);
         }
      }

      this.accept(Token.RBRACE);
      return map;
   }

   void accept(Token token) {
      if (this.token == token) {
         this.nextToken();
      } else {
         throw new IllegalArgumentException("illegal token : " + this.token + ", expect " + token);
      }
   }

   final void nextToken() {
      if (this.index == Integer.MIN_VALUE) {
         this.token = Token.EOF;
      } else {
         while(CharTypes.isWhitespace(this.ch)) {
            this.nextChar();
         }

         if (this.index >= this.text.length()) {
            this.token = Token.EOF;
         } else {
            switch (this.ch) {
               case '"':
                  this.scanString();
                  break;
               case ',':
                  this.token = Token.COMMA;
                  this.nextChar();
                  break;
               case ':':
                  this.token = Token.COLON;
                  this.nextChar();
                  break;
               case '[':
                  this.token = Token.LBRACKET;
                  this.nextChar();
                  break;
               case ']':
                  this.token = Token.RBRACKET;
                  this.nextChar();
                  break;
               case '{':
                  this.token = Token.LBRACE;
                  this.nextChar();
                  break;
               case '}':
                  this.token = Token.RBRACE;
                  this.nextChar();
                  break;
               default:
                  if (!isDigit(this.ch) && this.ch != '-') {
                     if (this.text.startsWith("null", this.index)) {
                        this.token = Token.NULL;
                        this.index += 3;
                        this.nextChar();
                        return;
                     }

                     if (this.text.startsWith("true", this.index)) {
                        this.token = Token.TRUE;
                        this.index += 3;
                        this.nextChar();
                        return;
                     }

                     if (this.text.startsWith("false", this.index)) {
                        this.token = Token.FALSE;
                        this.index += 4;
                        this.nextChar();
                        return;
                     }

                     throw new IllegalArgumentException("illegal json char : " + this.ch);
                  }

                  this.scanDigit();
                  return;
            }

         }
      }
   }

   private void scanDigit() {
      boolean isNegate = false;
      if (this.ch == '-') {
         isNegate = true;
         this.nextChar();
      }

      int dotCount = 0;
      StringBuilder digitBuf = new StringBuilder();

      while(true) {
         digitBuf.append(this.ch);
         this.nextChar();
         if (this.ch == '.') {
            ++dotCount;
            digitBuf.append('.');
            this.nextChar();
         } else if (!isDigit(this.ch)) {
            if (dotCount == 0) {
               long longValue = Long.parseLong(digitBuf.toString());
               if (isNegate) {
                  longValue = -longValue;
               }

               this.longValue = longValue;
               this.token = Token.INT;
            } else {
               double doubleValue = Double.parseDouble(digitBuf.toString());
               if (isNegate) {
                  doubleValue = -doubleValue;
               }

               this.doubleValue = doubleValue;
               this.token = Token.DOUBLE;
            }

            return;
         }
      }
   }

   private void scanString() {
      this.nextChar();

      StringBuilder strBuf;
      for(strBuf = new StringBuilder(); this.index < this.text.length(); this.nextChar()) {
         if (this.ch == '"') {
            this.nextChar();
            this.stringValue = strBuf.toString();
            this.token = Token.STRING;
            return;
         }

         if (this.ch == '\\') {
            this.nextChar();
            if (this.ch != '"' && this.ch != '\\' && this.ch != '/') {
               if (this.ch == 'n') {
                  strBuf.append('\n');
               } else if (this.ch == 'r') {
                  strBuf.append('\r');
               } else if (this.ch == 'b') {
                  strBuf.append('\b');
               } else if (this.ch == 'f') {
                  strBuf.append('\f');
               } else if (this.ch == 't') {
                  strBuf.append('\t');
               } else {
                  if (this.ch != 'u') {
                     throw new IllegalArgumentException("illegal string : " + strBuf);
                  }

                  this.nextChar();
                  char c1 = this.ch;
                  this.nextChar();
                  char c2 = this.ch;
                  this.nextChar();
                  char c3 = this.ch;
                  this.nextChar();
                  char c4 = this.ch;
                  int val = Integer.parseInt(new String(new char[]{c1, c2, c3, c4}), 16);
                  strBuf.append((char)val);
               }
            } else {
               strBuf.append(this.ch);
            }
         } else {
            strBuf.append(this.ch);
         }
      }

      throw new IllegalArgumentException("illegal string : " + strBuf);
   }

   static boolean isDigit(char ch) {
      return ch >= '0' && ch <= '9';
   }

   void nextChar() {
      ++this.index;
      if (this.index >= this.text.length()) {
         this.index = Integer.MIN_VALUE;
      } else {
         this.ch = this.text.charAt(this.index);
      }
   }

   static enum Token {
      INT,
      DOUBLE,
      STRING,
      BOOLEAN,
      TRUE,
      FALSE,
      NULL,
      EOF,
      LBRACE("{"),
      RBRACE("}"),
      LBRACKET("["),
      RBRACKET("]"),
      COMMA(","),
      COLON(":");

      public final String name;

      private Token() {
         this(null);
      }

      private Token(String name) {
         this.name = name;
      }
   }
}
