package org.ggp.base.util.crypto;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;

import external.JSON.JSONArray;
import external.JSON.JSONException;
import external.JSON.JSONObject;
import external.JSON.JSONString;

public class CanonicalJSON {
    /* Right now we only support one canonicalization strategy, which is
     * the SIMPLE approach. In the future, we may need to make breaking changes
     * to the canonicalization strategy, to support unforeseen situations (e.g.
     * edge cases the current canonicalization strategy doesn't handle properly).
     * However, we'd still like to be able to canonicalize data using the older
     * strategies so that we can e.g. still verify signatures created using the
     * older canonicalization strategy. So this class is designed to be able to
     * support multiple canonicalization strategies, and the user chooses which
     * strategy is used. */
    static enum CanonicalizationStrategy {
        SIMPLE,
    };

    /* Helper function to generate canonical strings for JSON strings */
    static String getCanonicalForm(String x, CanonicalizationStrategy s) {
        try {
            return getCanonicalForm(new JSONObject(x), s);
        } catch (JSONException e) {
            return null;
        }
    }

    /* Main function to generate canonical strings for JSON objects */
    static String getCanonicalForm(JSONObject x, CanonicalizationStrategy s) {
        if (s == CanonicalizationStrategy.SIMPLE) {
            return renderSimpleCanonicalJSON(x);
        } else {
            throw new RuntimeException("Canonicalization strategy not recognized.");
        }
    }

    /* This should be identical to the standard code to render the JSON object,
     * except it forces the keys for maps to be listed in sorted order. */
    static String renderSimpleCanonicalJSON(Object x) {
        try {
            if (x instanceof JSONObject) {
                JSONObject theObject = (JSONObject)x;

                // Sort the keys
                TreeSet<String> t = new TreeSet<String>();
                Iterator<?> i = theObject.keys();
                while (i.hasNext()) t.add(i.next().toString());
                Iterator<String> keys = t.iterator();

                StringBuilder sb = new StringBuilder("{");
                while (keys.hasNext()) {
                    if (sb.length() > 1) {
                        sb.append(',');
                    }
                    Object o = keys.next();
                    sb.append(JSONObject.quote(o.toString()));
                    sb.append(':');
                    sb.append(renderSimpleCanonicalJSON(theObject.get(o.toString())));
                }
                sb.append('}');
                return sb.toString();
            } else if (x instanceof JSONArray) {
                JSONArray theArray = (JSONArray)x;
                StringBuilder sb = new StringBuilder();
                sb.append("[");
                int len = theArray.length();
                for (int i = 0; i < len; i += 1) {
                    if (i > 0) {
                        sb.append(",");
                    }
                    sb.append(renderSimpleCanonicalJSON(theArray.get(i)));
                }
                sb.append("]");
                return sb.toString();
            } else {
                if (x == null || x.equals(null)) {
                    return "null";
                }
                if (x instanceof JSONString) {
                    Object object;
                    try {
                        object = ((JSONString)x).toJSONString();
                    } catch (Exception e) {
                        throw new JSONException(e);
                    }
                    if (object instanceof String) {
                        return (String)object;
                    }
                    throw new JSONException("Bad value from toJSONString: " + object);
                }
                if (x instanceof Number) {
                    return JSONObject.numberToString((Number)x);
                }
                if (x instanceof Boolean || x instanceof JSONObject ||
                        x instanceof JSONArray) {
                    return x.toString();
                }
                if (x instanceof Map) {
                    return renderSimpleCanonicalJSON(new JSONObject((Map<?,?>)x)).toString();
                }
                if (x instanceof Collection) {
                    return renderSimpleCanonicalJSON(new JSONArray((Collection<?>)x)).toString();
                }
                if (x.getClass().isArray()) {
                    return renderSimpleCanonicalJSON(new JSONArray(x)).toString();
                }
                return JSONObject.quote(x.toString());
            }
        } catch (Exception e) {
            return null;
        }
    }
}