package be.iminds.ilabt.jfed.util;


import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * This helper class "stringifies" XMLRPC output (a structure made out of: List, Map, String, Integer, Double, ...)
 */
public class XmlRpcPrintUtil {
    private static final Logger LOG = LoggerFactory.getLogger(XmlRpcPrintUtil.class);

    private static String prependToEachNextLine(String in, String texttoprepend) {
        StringBuilder res = new StringBuilder(in.length() + 100);
        for (int i = 0; i < in.length(); i++) {
            char c = in.charAt(i);
            if (c == '\n' && i != in.length() - 1) {
                res.append(c).append(texttoprepend);
            } else
                res.append(c);
        }
        return res.toString();
    }

    public static String xmlRpcObjectToString(Object o) {
        return printXmlRpcResultObject(o, "", true /*warnForUnsupported*/);
    }

    /** try to convert any object ot a json string. Might not always work nicely */
    public static String anyObjectToJsonString(Object o) {
        return printXmlRpcResultObject(o, "", false /*warnForUnsupported*/);
    }

    /**
     * uses a JSON syntax   (not JSON-like (which the old version of this method did), but actual JSON compatible syntax, including character escapes)
     */
    @SuppressWarnings("unchecked")
    private static String printXmlRpcResultObject(Object o, String texttoprepend, boolean warnForUnsupported) {
        if (o == null) return "null";
        StringBuilder res = new StringBuilder(100);
        boolean knownClass = false;
        if (o == null || JSONObject.NULL.equals(o)) {
            res.append(prependToEachNextLine("null", texttoprepend));
            return res.toString();
        }
        if (o instanceof Map) {
            Map h= (Map) o;
            if (h.size() == 0)
                res.append("{ }");
            else {
                res.append("{\n");
                int j = 0;
                for (Object key : h.keySet()) {
                    Object val = h.get(key);
                    String preptext = texttoprepend;
                    int shiftLen = 6 + key.toString().length() + 3;
                    if (shiftLen > 6 + 6 + 3) shiftLen = 4 + 3;
                    for (int i = 0; i < shiftLen; i++)
                        preptext += " ";
                    if (key instanceof String)
                        res.append(texttoprepend).append("\"").append(key).append("\": ").append(printXmlRpcResultObject(val, preptext, warnForUnsupported));
                    else
                        res.append(texttoprepend).append("").append(key).append(": ").append(printXmlRpcResultObject(val, preptext, warnForUnsupported));
                    if (j++ < h.keySet().size() - 1)
                        res.append(",");
                    res.append("\n");
                }
                res.append(texttoprepend).append("}");
            }
            knownClass = true;
        }
        if (o instanceof List) {
            List v= (List) o;
            if (v.size() == 0)
                res.append("[ ]");
            else {
                res.append("[\n");
                int i = 0;
                for (Object e : v) {
                    res.append(texttoprepend).append(printXmlRpcResultObject(e, texttoprepend + "   ", warnForUnsupported));
                    if (i++ < v.size() - 1)
                        res.append(",");
                    res.append("\n");
                }
                res.append(texttoprepend).append("]");
            }
            knownClass = true;
        }
        if (o instanceof Object[]) {
            Object[] v= (Object[]) o;
            if (v.length == 0)
                res.append("[ ]");
            else {
                res.append("[\n");
                int i = 0;
                for (Object e : v) {
                    res.append(texttoprepend).append(printXmlRpcResultObject(e, texttoprepend + "   ", warnForUnsupported));
                    if (i++ < v.length - 1)
                        res.append(",");
                    res.append("\n");
                }
                res.append(texttoprepend).append("]");
            }
            knownClass = true;
        }
        if (Objects.equals(o.getClass(), String.class) || Objects.equals(o.getClass(), GeniUrn.class) || o instanceof AnyCredential) {
            String str;
            if (o instanceof AnyCredential) {
                str = ((AnyCredential)o).getCredentialXml();
            } else {
                if (o instanceof String)
                    str = (String) o;
                else
                    str = o.toString();
            }

            res.append(encodeStringToJsonValue(str, texttoprepend));
            knownClass = true;
        }
        if (o instanceof Number) {
            res.append(prependToEachNextLine(o.toString(), texttoprepend));
            knownClass = true;
        }
        if (o instanceof Boolean) {
            res.append(prependToEachNextLine(o.toString(), texttoprepend));
            knownClass = true;
        }
        if (!knownClass) {
            if (warnForUnsupported) {
                if (o instanceof Collection) {
                    //            throw new IllegalArgumentException("Unsupported collection class in input object: "+o.getClass().getName());
                    LOG.warn("printXmlRpcResultObject got an input object containing an unsupported collection class: " + o.getClass().getName() + ". " +
                            "This is not correct, and will be corrected automatically by converting it to a String. " +
                            "Fix the bug in the code!");
                } else {
//            throw new IllegalArgumentException("Unsupported class in input object: "+o.getClass().getName());
                    LOG.warn("printXmlRpcResultObject got an input object containing a " + o.getClass().getName() + ". " +
                            "This is not correct, and will be corrected automatically by converting it to a String. " +
                            "Fix the bug in the code!");

                }
            }
//            res.append(prependToEachNextLine(o.toString(), texttoprepend)); //not best fallback
            //safest fallback: convert to string and encode as string
            //res.append(o.toString());

            res.append(encodeStringToJsonValue(o.toString(), texttoprepend));
        }
        return res.toString();
    }

    /**
     * @return the string encoded as json value, inclusing start and end quotation marks
     * */
    private static String encodeStringToJsonValue(String str, String texttoprepend) {
        StringBuilder s = new StringBuilder();

        //escape what needs to be escaped or is nice to escape:
        //from JSON RFC:
        //    characters that must be escaped:
        //   quotation mark, reverse solidus, and the control characters (U+0000
        //   through U+001F).
        //
        //       char = unescaped /
        //              escape (
        //                  %x22 /          ; "    quotation mark  U+0022
        //                  %x5C /          ; \    reverse solidus U+005C
        //                  %x2F /          ; /    solidus         U+002F
        //                  %x62 /          ; b    backspace       U+0008
        //                  %x66 /          ; f    form feed       U+000C
        //                  %x6E /          ; n    line feed       U+000A
        //                  %x72 /          ; r    carriage return U+000D
        //                  %x74 /          ; t    tab             U+0009
        //                  %x75 4HEXDIG )  ; uXXXX                U+XXXX

        //note: we don't escape a slash (solidus), as it is not needed and more readable when not escaped.
        for (int i = 0 ; i < str.length(); i++) {
            char c = str.charAt(i);
            switch (c) {
                case '"' : { s.append("\\\""); break;  }
                case '\\' : { s.append("\\\\"); break; }
                case '\b' : { s.append("\\b"); break;  }
                case '\f' : { s.append("\\f"); break;  }
                case '\n' : { s.append("\\n"); break;  } //must be escaped: is \u000A
                case '\r' : { s.append("\\r"); break;  }
                case '\t' : { s.append("\\t"); break;  }
                default: {
                    long l = c | 0x10000;
                    if (l >= 0 && l <= 0x001F)
                        s.append("\\u").append(Integer.toHexString(c | 0x10000).substring(1));
                    else
                        s.append(c);
                    break;
                }
            }
        }

        if (s.length() > 10 * 1024 * 1024) //limit to 10MB
            return "\"" + prependToEachNextLine("=== WARNING: String to long (" + s.length() + " chars) to process! ===", texttoprepend)+"\"";
        else
            return "\""+prependToEachNextLine(s.toString(), texttoprepend)+"\"";
    }
}
