package com.sedmelluq.discord.lavaplayer.tools;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Allows to easily navigate in decoded JSON data
 */
public class JsonBrowser {
  public static final JsonBrowser NULL_BROWSER = new JsonBrowser(null);

  private static final ObjectMapper mapper = setupMapper();

  private final JsonNode node;

  private JsonBrowser(JsonNode node) {
    this.node = node;
  }

  /**
   * @return True if the value represents a list.
   */
  public boolean isList() {
    return node instanceof ArrayNode;
  }

  /**
   * @return True if the value represents a map.
   */
  public boolean isMap() {
    return node instanceof ObjectNode;
  }

  /**
   * Get an element at an index for a list value
   * @param index List index
   * @return JsonBrowser instance which wraps the value at the specified index
   */
  public JsonBrowser index(int index) {
    if (isList()) {
      return create(node.get(index));
    } else {
      return NULL_BROWSER;
    }
  }

  /**
   * Get an element by key from a map value
   * @param key Map key
   * @return JsonBrowser instance which wraps the value with the specified key
   */
  public JsonBrowser get(String key) {
    if (isMap()) {
      return create(node.get(key));
    } else {
      return NULL_BROWSER;
    }
  }

  /**
   * Put a value into the map if this instance contains a map.
   * @param key The map entry key
   * @param item The map entry value
   */
  public void put(String key, Object item) {
    if (node instanceof ObjectNode) {
      ((ObjectNode) node).set(key, mapper.valueToTree(item));
    } else {
      throw new IllegalStateException("Put only works on a map");
    }
  }

  /**
   * Returns a list of all the values in this element
   * @return The list of values as JsonBrowser elements
   */
  public List<JsonBrowser> values() {
    List<JsonBrowser> values = new ArrayList<>();

    if (node != null) {
      node.elements().forEachRemaining(child -> values.add(new JsonBrowser(child)));
    }

    return values;
  }

  /**
   * Attempt to retrieve the value in the specified format
   * @param klass The class to retrieve the value as
   * @return The value as an instance of the specified class
   * @throws IllegalArgumentException If conversion is impossible
   */
  public <T> T as(Class<T> klass) {
    try {
      return mapper.treeToValue(node, klass);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public <T> T as(TypeReference<T> type) {
    try {
      return mapper.readValue(mapper.treeAsTokens(node), type);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * @return The value of the element as text
   */
  public String text() {
    if (node != null) {
      if (node.isNull()) {
        return null;
      } else if (node.isTextual()) {
        return node.textValue();
      } else if (node.isIntegralNumber()) {
        return String.valueOf(node.longValue());
      } else if (node.isNumber()) {
        return node.numberValue().toString();
      } else if (node.isBoolean()) {
        return String.valueOf(node.booleanValue());
      } else {
        return node.toString();
      }
    }

    return null;
  }

  public boolean asBoolean(boolean defaultValue) {
    if (node != null && node.isBoolean()) {
      return node.booleanValue();
    } else {
      return defaultValue;
    }
  }

  public long asLong(long defaultValue) {
    if (node != null && node.isNumber()) {
      return node.numberValue().longValue();
    } else {
      return defaultValue;
    }
  }

  public String safeText() {
    String text = text();
    return text != null ? text : "";
  }

  public String format() {
    try {
      return node != null ? mapper.writeValueAsString(node) : null;
    } catch (Exception e) {
      return null;
    }
  }

  /**
   * @return The value of the element as text
   */
  public boolean isNull() {
    return node == null || node.isNull();
  }

  /**
   * Parse from string.
   * @param json The JSON object as a string
   * @return JsonBrowser instance for navigating in the result
   * @throws IOException When parsing the JSON failed
   */
  public static JsonBrowser parse(String json) throws IOException {
    return create(mapper.readTree(json));
  }

  /**
   * Parse from string.
   * @param stream The JSON object as a stream
   * @return JsonBrowser instance for navigating in the result
   * @throws IOException When parsing the JSON failed
   */
  public static JsonBrowser parse(InputStream stream) throws IOException {
    return create(mapper.readTree(stream));
  }

  private static ObjectMapper setupMapper() {
    JsonFactory jsonFactory = new JsonFactory();
    jsonFactory.enable(JsonParser.Feature.ALLOW_COMMENTS);
    jsonFactory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
    return new ObjectMapper(jsonFactory);
  }

  private static JsonBrowser create(JsonNode node) {
    return node != null ? new JsonBrowser(node) : NULL_BROWSER;
  }
}

@@ -3694,28 +3694,38 @@
 node != null
- && 
+) %7B%0A      if (
 node.isBoole
@@ -3730,32 +3730,34 @@
 lean()) %7B%0A      
+  
 return node.bool
@@ -3776,49 +3776,239 @@
     
+  
 %7D else 
-%7B%0A      return defaultValue;%0A    %7D
+if (node.isTextual()) %7B%0A        if (%22true%22.equals(node.textValue())) %7B%0A          return true;%0A        %7D else if (%22false%22.equals(node.textValue())) %7B%0A          return false;%0A        %7D%0A      %7D%0A    %7D%0A%0A    return defaultValue;
 %0A  %7D
@@ -4075,12 +4075,22 @@
 null
- && 
+) %7B%0A      if (
 node
@@ -4100,24 +4100,26 @@
 Number()) %7B%0A
+  
       return
@@ -4159,37 +4159,192 @@
     
+  
 %7D else 
-%7B%0A      return
+if (node.isTextual()) %7B%0A        try %7B%0A          return Long.parseLong(node.textValue());%0A        %7D catch (NumberFormatException ignored) %7B%0A          // Fall through to
  default
 Valu
@@ -4343,20 +4343,65 @@
 ault
-V
+ v
 alue
-;%0A    %7D
+.%0A        %7D%0A      %7D%0A    %7D%0A%0A    return defaultValue;
 %0A  %7D

Fix breaking content length parsing in previous commit.