package org.acra.util;

import org.acra.ACRA;
import org.acra.ReportField;
import org.acra.collector.CollectorUtil;
import org.acra.collector.CrashReportData;
import org.acra.sender.ReportSenderException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

import static org.acra.ACRA.LOG_TAG;

public class JSONReportBuilder {
    /**
     * <p>
     * Create a JSONObject containing the whole report data with the most
     * detailed possible structure depth. Fields are not just converted to a
     * single key=value pair. If a value can be decomposed into subobjects, it
     * is done.
     * </p>
     * <p>
     * <p>
     * For example, a String containing:
     * <p>
     * <pre>
     * some.key.name1=value1
     * some.key.name2=value2
     * some.other=value3
     * any.other.key=value4
     * key.without.value5
     * </pre>
     * <p>
     * is converted to
     * <p>
     * <pre>
     * {
     *   some : {
     *     key : {
     *       name1 : "value1",
     *       name2 : "value2"
     *     },
     *     other : "value3"
     *   },
     *   any : {
     *     other : {
     *       key : "value4"
     *     }
     *   }
     *   key.without.value : true
     * }
     * </pre>
     * <p>
     * </p>
     *
     * @param errorContent The ACRA report data structure.
     * @return A JSONObject containing all fields from the report converted to
     * JSON.
     * @throws ReportSenderException
     * @throws JSONReportException
     */
    public static JSONObject buildJSONReport(CrashReportData errorContent) throws JSONReportException {
        JSONObject jsonReport = new JSONObject();
        BufferedReader reader = null;
        for (ReportField key : errorContent.keySet()) {
            try {
                // Each ReportField can be identified as a substructure and not
                // a simple String value.
                if (key.containsKeyValuePairs()) {
                    JSONObject subObject = new JSONObject();
                    String strContent = errorContent.getProperty(key);
                    reader = new BufferedReader(new StringReader(strContent), 1024);
                    String line = null;
                    try {
                        while ((line = reader.readLine()) != null) {
                            addJSONFromProperty(subObject, line);
                        }
                    } catch (IOException e) {
                        ACRA.log.e(LOG_TAG, "Error while converting " + key.name() + " to JSON.", e);
                    }
                    jsonReport.accumulate(key.name(), subObject);
                } else {
                    // This field is a simple String value, store it as it is
                    jsonReport.accumulate(key.name(), guessType(errorContent.getProperty(key)));
                }
            } catch (JSONException e) {
                throw new JSONReportException("Could not create JSON object for key " + key, e);
            } finally {
                CollectorUtil.safeClose(reader);
            }
        }
        return jsonReport;
    }

    /**
     * <p>
     * Given a String containing key=value pairs on each line, adds a detailed
     * JSON structure to an existing JSONObject, reusing intermediate subobjects
     * if available when keys are composed of a succession of subkeys delimited
     * by dots.
     * </p>
     * <p>
     * <p>
     * For example, adding the string "metrics.xdpi=160.0" to an object
     * containing
     * <p>
     * <pre>
     * {
     *   "metrics" : { "ydpi" : "160.0"},
     *   "width" : "320",
     *   "height" : "533"
     * }
     * </pre>
     * <p>
     * results in
     * <p>
     * <pre>
     * {
     *   "metrics" : { "ydpi" : "160.0", "xdpi" : "160.0"},
     *   "width" : "320",
     *   "height" : "533"
     * }
     * </pre>
     * <p>
     * </p>
     *
     * @param destination    The JSONObject where the data must be inserted.
     * @param propertyString A string containing "some.key.name=Any value"
     * @throws JSONException
     */
    private static void addJSONFromProperty(JSONObject destination, String propertyString) throws JSONException {
        int equalsIndex = propertyString.indexOf('=');
        if (equalsIndex > 0) {
            JSONObject finalObject = destination;
            String currentKey = propertyString.substring(0, equalsIndex).trim();
            String currentValue = propertyString.substring(equalsIndex + 1).trim();
            Object value = guessType(currentValue);
            if (value instanceof String) {
                value = ((String) value).replaceAll("\\\\n", "\n");
            }
            String[] splitKey = currentKey.split("\\.");
            if (splitKey.length > 1) {
                addJSONSubTree(finalObject, splitKey, value);
            } else {
                finalObject.accumulate(currentKey, value);
            }
        } else {
            destination.put(propertyString.trim(), true);
        }
    }

    private static Object guessType(String value) {
        if (value.equalsIgnoreCase("true"))
            return true;
        if (value.equalsIgnoreCase("false"))
            return false;

        if (value.matches("(?:^|\\s)([1-9](?:\\d*|(?:\\d{0,2})(?:,\\d{3})*)(?:\\.\\d*[1-9])?|0?\\.\\d*[1-9]|0)(?:\\s|$)")) {
            NumberFormat format = NumberFormat.getInstance(Locale.US);
            try {
                Number number = format.parse(value);
                return number;
            } catch (ParseException e) {
                // never mind
            }
        }
        return value;
    }

    /**
     * Deep insert a value inside a JSONObject, reusing existing subobjects when
     * available or creating them when necessary.
     *
     * @param destination The JSONObject which receives the additional subitem.
     * @param keys        An array containing the path keys leading to where the value
     *                    has to be inserted.
     * @param value       The value to be inserted.
     * @throws JSONException
     */
    private static void addJSONSubTree(JSONObject destination, String[] keys, Object value) throws JSONException {
        for (int i = 0; i < keys.length; i++) {
            String subKey = keys[i];
            if (i < keys.length - 1) {
                JSONObject intermediate = null;
                if (destination.isNull(subKey)) {
                    intermediate = new JSONObject();
                    destination.accumulate(subKey, intermediate);
                } else {
                    Object target = destination.get(subKey);
                    if (target instanceof JSONObject) {
                        intermediate = destination.getJSONObject(subKey);
                    } else if (target instanceof JSONArray) {
                        // Unexpected JSONArray, see issue #186
                        JSONArray wildCard = destination.getJSONArray(subKey);
                        for (int j = 0; j < wildCard.length(); j++) {
                            intermediate = wildCard.optJSONObject(j);
                            if (intermediate != null) {
                                // Found the original JSONObject we were looking for
                                break;
                            }
                        }
                    }

                    if (intermediate == null) {
                        ACRA.log.e(LOG_TAG, "Unknown json subtree type, see issue #186");
                        // We should never get here, but if we do, drop this value to still send the report
                        return;
                    }
                }
                destination = intermediate;
            } else {
                destination.accumulate(subKey, value);
            }
        }
    }

    public static class JSONReportException extends Exception {
        private static final long serialVersionUID = -694684023635442219L;

        public JSONReportException(String message, Throwable e) {
            super(message, e);
        }
    }

    ;
}
