/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.pdx;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonParser.NumberType;
import com.fasterxml.jackson.core.JsonToken;

import org.apache.geode.cache.RegionService;
import org.apache.geode.cache.client.internal.ProxyCache;
import org.apache.geode.cache.client.internal.UserAttributes;
import org.apache.geode.pdx.internal.json.JSONToPdxMapper;
import org.apache.geode.pdx.internal.json.PdxInstanceHelper;
import org.apache.geode.pdx.internal.json.PdxInstanceSortedHelper;
import org.apache.geode.pdx.internal.json.PdxListHelper;
import org.apache.geode.pdx.internal.json.PdxToJSON;
import org.apache.geode.util.internal.GeodeGlossary;


/**
 * <p>
 * JSONFormatter has a static method {@link JSONFormatter#fromJSON(String, String...)} to convert a
 * JSON document into a {@link PdxInstance} and a static method
 * {@link JSONFormatter#toJSON(PdxInstance)} to convert a {@link PdxInstance} into a JSON Document.
 * <p>
 * Using these methods an applications may convert a JSON document into a PdxInstance for storing in
 * the cache. Indexes can then be defined on the PdxInstances so that queries can be performed using
 * OQL. Queries will return PdxInstances and these can be turned back back into JSON documents using
 * JSONFormatter.
 * <p>
 * JSONFormatter treats values in a json document as number(byte, short, int, long..), string,
 * array, object, 'true', 'false' or 'null'. These correspond to the following java types:
 *
 * <table summary="">
 * <tr>
 * <th>JSON</th>
 * <th>Java</th>
 * </tr>
 * <tr>
 * <td>object</td>
 * <td>{@link PdxInstance}</td>
 * </tr>
 * <tr>
 * <td>arrays</td>
 * <td>{@link java.util.LinkedList}</td>
 * </tr>
 * <tr>
 * <td>BigDecimal</td>
 * <td>{@link BigDecimal}</td>
 * </tr>
 * <tr>
 * <td>BigInterger</td>
 * <td>{@link BigInteger}</td>
 * </tr>
 * <tr>
 * <td>Double</td>
 * <td>double</td>
 * </tr>
 * <tr>
 * <td>float</td>
 * <td>float</td>
 * </tr>
 * <tr>
 * <td>boolean</td>
 * <td>boolean</td>
 * </tr>
 * <tr>
 * <td>Integer</td>
 * <td>byte, short or int</td>
 * </tr>
 * <tr>
 * <td>null</td>
 * <td>null</td>
 * </tr>
 * </table>
 */

public class JSONFormatter {

  public static final String JSON_CLASSNAME = "__GEMFIRE_JSON";

  /***
   * By setting "gemfire.sort-json-field-names" to true, enables serialization of JSON field in JSON
   * document to be sorted. That can help to reduce the number of pdx typeId generation if different
   * JSON documents have same fields in the different order.
   */
  public static final String SORT_JSON_FIELD_NAMES_PROPERTY =
      GeodeGlossary.GEMFIRE_PREFIX + "pdx.mapper.sort-json-field-names";

  enum states {
    NONE, OBJECT_START, FIELD_NAME, SCALAR_FOUND, LIST_FOUND, LIST_ENDS, OBJECT_ENDS
  }

  private RegionService regionService;

  public JSONFormatter() {}

  public JSONFormatter(RegionService regionService) {
    this.regionService = regionService;
  }

  public RegionService getRegionService() {
    return regionService;
  }

  /**
   * Converts a String JSON document into a PdxInstance
   *
   * @param jsonString The JSON String to convert to PDX
   * @throws JSONFormatterException if unable to create the PdxInstance
   * @return The PdxInstance
   */
  public static PdxInstance fromJSON(String jsonString) {
    return new JSONFormatter().toPdxInstance(jsonString);
  }

  /**
   * Converts a String JSON document into a PdxInstance
   *
   * @param jsonString The JSON String to convert to PDX
   * @param identityFields Any desired identity fields on the JSON object to be used for equals and
   *        hashCode computations
   * @throws JSONFormatterException if unable to create the PdxInstance
   * @return The PdxInstance
   */
  public static PdxInstance fromJSON(String jsonString, String... identityFields) {
    return new JSONFormatter().toPdxInstance(jsonString, identityFields);
  }

  /**
   * Converts a Byte Array JSON document into a PdxInstance
   *
   * @param jsonByteArray The JSON Object as a byte array to convert to PDX
   * @throws JSONFormatterException if unable to create the PdxInstance
   * @return The PdxInstance
   */
  public static PdxInstance fromJSON(byte[] jsonByteArray) {
    return new JSONFormatter().toPdxInstance(jsonByteArray);
  }

  /**
   * Converts a Byte Array JSON document into a PdxInstance
   *
   * @param jsonByteArray The JSON Object as a byte array to convert to PDX
   * @param identityFields Any desired identity fields on the JSON object to be used for equals and
   *        hashCode computations
   * @throws JSONFormatterException if unable to create the PdxInstance
   * @return The PdxInstance
   */
  public static PdxInstance fromJSON(byte[] jsonByteArray, String... identityFields) {
    return new JSONFormatter().toPdxInstance(jsonByteArray, identityFields);
  }

  /**
   * Converts a JSON document (String or Byte Array) into a PdxInstance
   *
   * @param json The JSON document (String or Byte Array) to convert to PDX
   * @param identityFields Any desired identity fields on the JSON object to be used for equals and
   *        hashCode computations
   * @return The PdxInstance
   * @throws JSONFormatterException if unable to create the PdxInstance
   */
  public PdxInstance toPdxInstance(Object json, String... identityFields) {
    if (regionService != null && regionService instanceof ProxyCache) {
      ProxyCache proxyCache = (ProxyCache) regionService;
      UserAttributes.userAttributes.set(proxyCache.getUserAttributes());
    }
    JsonParser jp = null;
    try {
      if (json instanceof String) {
        jp = new JsonFactory().createParser((String) json);

      } else if (json instanceof byte[]) {
        jp = new JsonFactory().createParser((byte[]) json);
      } else {
        throw new JSONFormatterException("Could not parse the " + json.getClass() + " type");
      }
      enableJSONParserFeature(jp);
      return getPdxInstance(jp, states.NONE, null, identityFields).getPdxInstance();
    } catch (JsonParseException jpe) {
      throw new JSONFormatterException("Could not parse JSON document ", jpe);
    } catch (IOException e) {
      throw new JSONFormatterException("Could not parse JSON document: " + jp.getCurrentLocation(),
          e);
    } catch (Exception e) {
      throw new JSONFormatterException("Could not parse JSON document: " + jp.getCurrentLocation(),
          e);
    } finally {
      UserAttributes.userAttributes.set(null);
    }
  }

  private void enableJSONParserFeature(JsonParser jp) {
    jp.enable(Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
    jp.enable(Feature.ALLOW_UNQUOTED_FIELD_NAMES);
  }

  /**
   * Converts a PdxInstance into a JSON document in String form
   *
   * @param pdxInstance the JSON string.
   * @return the JSON string
   *         JSONFormatterException if unable to create the JSON document
   */
  public static String toJSON(PdxInstance pdxInstance) {
    return new JSONFormatter().fromPdxInstance(pdxInstance);
  }

  /**
   * Converts a PdxInstance into a JSON document
   *
   * @param pdxInstance The PdxInstance to convert
   * @return the JSON string
   * @throws JSONFormatterException if unable to create the JSON document
   */
  public String fromPdxInstance(PdxInstance pdxInstance) {
    try {
      PdxToJSON pj = new PdxToJSON(pdxInstance);
      return pj.getJSON();
    } catch (Exception e) {
      throw new JSONFormatterException("Could not create JSON document from PdxInstance", e);
    }
  }

  /**
   * Converts a PdxInstance into a JSON document in byte-array form
   *
   * @param pdxInstance The PdxInstance to convert
   * @return the JSON byte array
   * @throws JSONFormatterException if unable to create the JSON document
   */
  public static byte[] toJSONByteArray(PdxInstance pdxInstance) {
    return new JSONFormatter().toJsonByteArrayFromPdxInstance(pdxInstance);
  }

  /**
   * Converts a PdxInstance into a JSON document in byte-array form
   *
   * @param pdxInstance The PdxInstance to convert
   * @return the JSON byte array
   * @throws JSONFormatterException if unable to create the JSON document
   */
  public byte[] toJsonByteArrayFromPdxInstance(PdxInstance pdxInstance) {
    try {
      PdxToJSON pj = new PdxToJSON(pdxInstance);
      return pj.getJSONByteArray();
    } catch (Exception e) {
      throw new JSONFormatterException("Could not create JSON document from PdxInstance", e);
    }
  }

  private JSONToPdxMapper createJSONToPdxMapper(String className, JSONToPdxMapper parent,
      String... identityFields) {
    if (Boolean.getBoolean(SORT_JSON_FIELD_NAMES_PROPERTY)) {
      return new PdxInstanceSortedHelper(className, parent, identityFields);
    } else {
      return new PdxInstanceHelper(className, parent, identityFields);
    }
  }

  private JSONToPdxMapper getPdxInstance(JsonParser jp, states currentState,
      JSONToPdxMapper currentPdxInstance, String... identityFields) throws IOException {
    String currentFieldName = null;
    if (currentState == states.OBJECT_START && currentPdxInstance == null) {
      currentPdxInstance = createJSONToPdxMapper(null, null, identityFields);// from getlist
    }
    while (true) {
      JsonToken nt = jp.nextToken();

      if (nt == null) {
        return currentPdxInstance;
      }
      switch (nt) {
        case START_OBJECT: {
          objectStarts(currentState);
          currentState = states.OBJECT_START;
          // need to create new PdxInstance
          // root object will not name, so create classname lazily from all members.
          // child object will have name; but create this as well lazily from all members
          JSONToPdxMapper tmp =
              createJSONToPdxMapper(currentFieldName, currentPdxInstance, identityFields);
          currentPdxInstance = tmp;
          break;
        }
        case END_OBJECT: {
          // pdxinstnce ends
          objectEnds(currentState);
          currentState = states.OBJECT_ENDS;
          currentPdxInstance.endObjectField("endobject");
          if (currentPdxInstance.getParent() == null) {
            return currentPdxInstance;// inner pdxinstance in list
          }
          JSONToPdxMapper tmp = currentPdxInstance;
          currentPdxInstance = currentPdxInstance.getParent();
          currentPdxInstance.addObjectField(tmp.getPdxFieldName(), tmp.getPdxInstance());
          break;
        }
        case FIELD_NAME: {
          fieldFound(currentState);
          // field name(object name, value may be object, string, array number etc)
          if (currentState == states.OBJECT_START) {
            currentPdxInstance.setPdxFieldName(currentFieldName);
          }

          currentFieldName = jp.getText();// not a object name
          currentState = states.FIELD_NAME;
          break;
        }
        case NOT_AVAILABLE: {
          throw new IllegalStateException("NOT_AVAILABLE token found");
          // break;
        }
        case START_ARRAY: {
          // need to create array; fieldname may be there; will it case it not there
          arrayStarts(currentState);
          currentState = states.LIST_FOUND;
          PdxListHelper list = getList(jp, currentState, null);
          currentPdxInstance.addListField(currentFieldName, list);
          currentState = states.LIST_ENDS;
          currentFieldName = null;
          break;
        }
        case END_ARRAY: {
          // array is end
          throw new IllegalStateException(
              "END_ARRAY token found in getPdxInstance while current state is " + currentState);
        }
        case VALUE_EMBEDDED_OBJECT: {
          throw new IllegalStateException(
              "VALUE_EMBEDDED_OBJECT token found in getPdxInstance while current state is "
                  + currentState);
        }
        case VALUE_FALSE: {
          // write boolen
          boolFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxInstance.addBooleanField(currentFieldName, jp.getValueAsBoolean());
          currentFieldName = null;
          break;
        }
        case VALUE_NULL: {
          // write null
          nullFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxInstance.addNullField(currentFieldName);
          currentFieldName = null;
          break;
        }
        case VALUE_NUMBER_FLOAT: {
          // write double/float
          doubleFound(currentState);
          currentState = states.SCALAR_FOUND;
          // currentPdxInstance.addDoubleField(currentFieldName, jp.getDoubleValue());
          setNumberField(jp, currentPdxInstance, currentFieldName);
          currentFieldName = null;
          break;
        }
        case VALUE_NUMBER_INT: {
          // write int
          intFound(currentState);
          currentState = states.SCALAR_FOUND;
          // currentPdxInstance.addIntField(currentFieldName, jp.getIntValue());
          setNumberField(jp, currentPdxInstance, currentFieldName);
          currentFieldName = null;
          break;
        }
        case VALUE_STRING: {
          // write string
          stringFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxInstance.addObjectField(currentFieldName, jp.getText());
          currentFieldName = null;
          break;
        }
        case VALUE_TRUE: {
          // write bool
          boolFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxInstance.addBooleanField(currentFieldName, jp.getValueAsBoolean());
          currentFieldName = null;
          break;
        }
        default: {
          throw new IllegalStateException("Token not handled " + nt);
        }
      }
    }
  }

  private void setNumberField(JsonParser jp, JSONToPdxMapper pih, String fieldName)
      throws IOException {
    NumberType nt = jp.getNumberType();

    switch (nt) {
      case BIG_DECIMAL:
        pih.addBigDecimalField(fieldName, jp.getDecimalValue());
        break;
      case BIG_INTEGER: {
        BigInteger bi = jp.getBigIntegerValue();
        pih.addBigIntegerField(fieldName, bi);
      }
        break;
      case DOUBLE:
        pih.addDoubleField(fieldName, jp.getDoubleValue());
        break;
      case FLOAT:
        pih.addFloatField(fieldName, jp.getFloatValue());
        break;
      case INT: {
        int val = jp.getIntValue();
        if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
          pih.addIntField(fieldName, val);
        } else if (val > Byte.MAX_VALUE || val < Byte.MIN_VALUE) {
          pih.addShortField(fieldName, (short) val);
        } else {
          pih.addByteField(fieldName, (byte) val);
        }
      }
        break;
      case LONG:
        pih.addLongField(fieldName, jp.getLongValue());
        break;
      default:
        throw new IllegalStateException("setNumberField:unknow number type " + nt);
    }
  }

  private void setNumberField(JsonParser jp, PdxListHelper pih) throws IOException {
    NumberType nt = jp.getNumberType();

    switch (nt) {
      case BIG_DECIMAL:
        pih.addBigDecimalField(jp.getDecimalValue());
        break;
      case BIG_INTEGER: {
        BigInteger bi = jp.getBigIntegerValue();
        pih.addBigIntegerField(bi);
      }
        break;
      case DOUBLE:
        pih.addDoubleField(jp.getDoubleValue());
        break;
      case FLOAT:
        pih.addFloatField(jp.getFloatValue());
        break;
      case INT: {
        int val = jp.getIntValue();
        if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
          pih.addIntField(val);
        } else if (val > Byte.MAX_VALUE || val < Byte.MIN_VALUE) {
          pih.addShortField((short) val);
        } else {
          pih.addByteField((byte) val);
        }
      }
        break;
      case LONG:
        pih.addLongField(jp.getLongValue());
        break;
      default:
        throw new IllegalStateException("setNumberField:unknow number type " + nt);
    }
  }

  private PdxListHelper getList(JsonParser jp, states currentState, PdxListHelper currentPdxList)
      throws IOException {
    String currentFieldName = null;
    currentPdxList = new PdxListHelper(currentPdxList, null);
    while (true) {
      JsonToken nt = jp.nextToken();

      if (nt == null) {
        return currentPdxList;
      }
      switch (nt) {
        case START_OBJECT: {
          objectStarts(currentState);
          currentState = states.OBJECT_START;
          // need to create new PdxInstance
          // root object will not name, so create classname lazily from all members.
          // child object will have name; but create this as well lazily from all members
          JSONToPdxMapper tmp = getPdxInstance(jp, currentState, null);
          currentPdxList.addObjectField(currentFieldName, tmp);
          currentState = states.OBJECT_ENDS;
          break;
        }
        case END_OBJECT: {
          // pdxinstnce ends
          throw new IllegalStateException(
              "getList got token END_OBJECT while current state is " + currentState);
        }
        case FIELD_NAME: {
          throw new IllegalStateException(
              "getList got token FIELD_NAME while current state is " + currentState);
        }
        case NOT_AVAILABLE: {
          throw new IllegalStateException(
              "NOT_AVAILABLE token found in getList current state is " + currentState);
          // break;
        }
        case START_ARRAY: {
          // need to create array; fieldname may be there; will it case it not there
          arrayStarts(currentState);
          PdxListHelper tmp = currentPdxList.addListField();
          currentPdxList = tmp;
          currentState = states.LIST_FOUND;
          break;
        }
        case END_ARRAY: {
          // array is end
          arrayEnds(currentState);
          currentState = states.LIST_ENDS;
          if (currentPdxList.getParent() == null) {
            return currentPdxList;
          }
          currentPdxList = currentPdxList.getParent();
          break;
        }
        case VALUE_EMBEDDED_OBJECT: {
          throw new IllegalStateException("VALUE_EMBEDDED_OBJECT token found");
        }
        case VALUE_FALSE: {
          // write boolen
          boolFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxList.addBooleanField(jp.getBooleanValue());
          break;
        }
        case VALUE_NULL: {
          // write null
          nullFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxList.addNullField(null);
          break;
        }
        case VALUE_NUMBER_FLOAT: {
          // write double/float
          doubleFound(currentState);
          currentState = states.SCALAR_FOUND;
          // currentPdxList.addDoubleField(jp.getDoubleValue());
          setNumberField(jp, currentPdxList);
          break;
        }
        case VALUE_NUMBER_INT: {
          // write int
          intFound(currentState);
          currentState = states.SCALAR_FOUND;
          // currentPdxList.addIntField(jp.getIntValue());
          setNumberField(jp, currentPdxList);
          break;
        }
        case VALUE_STRING: {
          // write string
          stringFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxList.addStringField(jp.getText());
          currentFieldName = null;
          break;
        }
        case VALUE_TRUE: {
          // write bool
          boolFound(currentState);
          currentState = states.SCALAR_FOUND;
          currentPdxList.addBooleanField(jp.getBooleanValue());
          break;
        }
        default: {
          throw new IllegalStateException("Token not handled in getlist" + nt);
        }
      }
    }
  }

  private boolean objectStarts(states currentState) {
    switch (currentState) {
      case NONE:
      case FIELD_NAME:
      case OBJECT_ENDS:// in list
      case SCALAR_FOUND:// inlist
      case LIST_FOUND:
      case LIST_ENDS:
        return true;
      default:
        throw new IllegalStateException("Object start called when state is " + currentState);

    }
  }

  private boolean objectEnds(states currentState) {
    switch (currentState) {
      case OBJECT_START: // when empty object on field
      case SCALAR_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:// inner object closes
        return true;
      default:
        throw new IllegalStateException("Object ends called when state is " + currentState);

    }
  }

  private boolean arrayStarts(states currentState) {
    switch (currentState) {
      case SCALAR_FOUND:
      case FIELD_NAME:
      case LIST_FOUND:
      case LIST_ENDS:
        return true;
      default:
        throw new IllegalStateException("Array start called when state is " + currentState);

    }
  }

  // enum states {NONE, OBJECT_START, FIELD_NAME, INNER_OBJECT_FOUND, SCALAR_FOUND, LIST_FOUND,
  // OBJECT_ENDS};
  private boolean arrayEnds(states currentState) {
    switch (currentState) {
      case FIELD_NAME:// when empty array
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("Array ends called when state is " + currentState);

    }
  }

  private boolean stringFound(states currentState) {
    switch (currentState) {
      case FIELD_NAME:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("stringFound called when state is " + currentState);

    }
  }

  private boolean intFound(states currentState) {
    switch (currentState) {
      case FIELD_NAME:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("intFound called when state is " + currentState);

    }
  }

  private boolean boolFound(states currentState) {
    switch (currentState) {
      case FIELD_NAME:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("boolFound called when state is " + currentState);

    }
  }

  private boolean doubleFound(states currentState) {
    switch (currentState) {
      case FIELD_NAME:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("doubleFound called when state is " + currentState);

    }
  }

  private boolean fieldFound(states currentState) {
    switch (currentState) {
      case OBJECT_START:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("fieldFound called when state is " + currentState);

    }
  }

  private boolean nullFound(states currentState) {
    switch (currentState) {
      case FIELD_NAME:
      case SCALAR_FOUND:
      case LIST_FOUND:
      case LIST_ENDS:
      case OBJECT_ENDS:
        return true;
      default:
        throw new IllegalStateException("nullFound called when state is " + currentState);

    }
  }
}
