package be.iminds.ilabt.jfed.ui.javafx.log_gui;

import be.iminds.ilabt.jfed.call_log_output.HtmlLogOutput;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import be.iminds.ilabt.jfed.ui.javafx.util.TimeUtils;
import be.iminds.ilabt.jfed.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public class LogPanel {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LogPanel.class);
    private static final GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");
    private final ObjectProperty<SerializableApiCallDetails> shownLog = new SimpleObjectProperty<>();
    @FXML
    private TextArea resultText;
    @FXML
    private TextArea resultErrorText;
    @FXML
    private TextField connectionTypeText;
    @FXML
    private TextField authorityNameText;
    @FXML
    private VBox resultTabContent;
    private boolean showingResultErrorText = true;
    private boolean showingResultText = true;
    @FXML
    private Parent resultTextGroup;
    @FXML
    private Parent resultErrorTextGroup;
    @FXML
    private Label resultCode;
    @FXML
    private Label resultLabel;
    @FXML
    private TextArea lowLevelGeniValueText;
    @FXML
    private TextArea lowLevelGeniCodeText;
    @FXML
    private TextArea lowLevelGeniOutputText;
    @FXML
    private TextField xmlRpcCommand;
    @FXML
    private TextArea xmlRpcSentText;
    @FXML
    private CheckBox xmlRpcSentTextShowCredentialsCheckBox;
    @FXML
    private CheckBox realJsonSentCheckBox;
    @FXML
    private TextArea xmlRpcReceivedText;
    @FXML
    private CheckBox xmlRpcReceivedTextShowCredentialsCheckBox;
    @FXML
    private CheckBox realJsonReceivedCheckBox;
    @FXML
    private StackPane xmlRpcSentStackPane;
    @FXML
    private StackPane xmlRpcReplyStackPane;
    @FXML
    private Button xmlRpcSentSwitchViewButton;
    @FXML
    private Button xmlRpcReplySwitchViewButton;
    @FXML
    private XmlRpcPane xmlRpcSentPane;
    @FXML
    private XmlRpcPane xmlRpcReceivedPane;
    @FXML
    private TextField baseServerUrlText;
    @FXML
    private TextField callServerUrlText;
    @FXML
    private TextField proxyText;
    @FXML
    private TextField connectionDebugIdText;
    @FXML
    private TextField connectionUserUrnText;
    @FXML
    private TextField connectionBasicHttpAuthUsernameText;
    @FXML
    private Label connectionBasicHttpAuthUsernameLabel;
    @FXML
    private TextField httpRequestLineText;
    @FXML
    private TextArea httpSentHeaders;
    @FXML
    private TextArea httpSentText;
    @FXML
    private TextField httpStatusLineText;
    @FXML
    private TextArea httpReceivedHeaders;
    @FXML
    private TextArea httpReceivedText;
    @FXML
    private TabPane logTabPane;
    @FXML
    private VBox logPanelContainer;
    @FXML
    private Tab resultTab;
    @FXML
    private Tab lowLevelValueTab;
    @FXML
    private Tab lowLevelOutputTab;
    //    @FXML private Tab XmlRpcTab;
//    @FXML private Tab httpTab;
    @FXML
    private Tab XmlRpcRequestTab;
    @FXML
    private Tab XmlRpcReplyTab;
    @FXML
    private Tab connectionDetailsTab;
    @FXML
    private Tab httpRequestTab;
    @FXML
    private Tab httpReplyTab;
    //extra tabs
    @FXML
    private Tab stitchingOverviewTab;
    @FXML
    private Tab exceptionTab;
    @FXML
    private Tab xmlTab;
    @FXML
    private Tab rspecTab;
    @FXML
    private Tab spewLogTab;
    @FXML
    private Tab htmlErrorTab;
    @FXML
    private Label requestSizeLabel;
    @FXML
    private Label replySizeLabel;
    @FXML
    private Label requestSizeHeaderLabel;
    @FXML
    private Label replySizeHeaderLabel;
    @FXML
    private TextArea exceptionTextField;
    @FXML
    private TextArea stitchingOverviewTextArea;
    @FXML
    private TextField callStartTimeField;
    @FXML
    private Label callStartTimeRelativeLabel;
    @FXML
    private TextField callStopTimeField;
    @FXML
    private Label callStopTimeRelativeLabel;
    @FXML
    private TextArea xmlText;
    @FXML
    private TextArea rspecText;
    @FXML
    private HBox showRspecButtonBox;

    private String xmlToFormat;
    private final List<RspecTabContent> rspecTabContents = new ArrayList<>();
    private String shownExceptionText = null;
    @FXML
    private WebView htmlWebView;
    private SerializableApiCallDetails htmlApiCallDetails;
    @FXML
    private WebView spewWebView;
    @FXML
    private Label spewWebViewLabel;
    @FXML
    private TextField spewUrl;
    @FXML
    private TextField spewUrn;
    private final ObjectProperty<SerializableApiCallDetails> shownApiCallDetails = new SimpleObjectProperty<>(null);
    private final ChangeListener<SerializableApiCallDetails> shownLogChangeListener =
            (observableValue, oldApiCallDetails, newApiCallDetails) -> {
                try {
                    //no need to bind! ApiCallDetails never changes...
                    logTabPane.getTabs().removeAll(xmlTab, rspecTab, spewLogTab, htmlErrorTab, exceptionTab, stitchingOverviewTab);

                    hideRspec();
                    LOG.debug("hide Rspec tab");

                    shownApiCallDetails.set(newApiCallDetails);

                    if (newApiCallDetails == null) {
                        resultText.setText("");
                        lowLevelGeniValueText.setText("");
                        lowLevelGeniOutputText.setText("");
                        lowLevelGeniCodeText.setText("");
                        xmlRpcCommand.setText("");
                        xmlRpcSentText.setText("");
                        xmlRpcReceivedText.setText("");
                        xmlRpcSentPane.setObj(null);
                        xmlRpcReceivedPane.setObj(null);
                        connectionTypeText.setText("");
                        authorityNameText.setText("");
                        baseServerUrlText.setText("");
                        callServerUrlText.setText("");
                        proxyText.setText("");
                        connectionDebugIdText.setText("");
                        connectionUserUrnText.setText("");
                        callStartTimeField.setText("");
                        callStartTimeRelativeLabel.setText("");
                        callStopTimeField.setText("");
                        callStopTimeRelativeLabel.setText("");
                        requestSizeLabel.setVisible(false);
                        replySizeLabel.setVisible(false);
                        httpRequestLineText.setText("");
                        httpSentHeaders.setText("");
                        httpSentText.setText("");
                        httpStatusLineText.setText("");
                        httpReceivedHeaders.setText("");
                        httpReceivedText.setText("");
                        xmlText.setText("");

                        showResultErrorText(false);
                        showResultText(false);

                        resultTab.setDisable(true);
                        lowLevelValueTab.setDisable(true);
                        lowLevelOutputTab.setDisable(true);
                        XmlRpcRequestTab.setDisable(true);
                        XmlRpcReplyTab.setDisable(true);
                        connectionDetailsTab.setDisable(true);
                        httpRequestTab.setDisable(true);
                        httpReplyTab.setDisable(true);
                    } else {
                        resultTab.setDisable(false);
                        lowLevelValueTab.setDisable(false);
                        lowLevelOutputTab.setDisable(false);
                        XmlRpcRequestTab.setDisable(false);
                        XmlRpcReplyTab.setDisable(false);
                        connectionDetailsTab.setDisable(false);
                        httpRequestTab.setDisable(false);
                        httpReplyTab.setDisable(false);

                        if (newApiCallDetails.getConnectionType() != null) {
                            connectionTypeText.setText(newApiCallDetails.getConnectionType() +
                                    " proto=" + newApiCallDetails.getConnectionProtocol() +
                                    " auth=" + newApiCallDetails.getConnectionAuthenticationMethod());
                        } else {
                            connectionTypeText.setText("");
                        }

                        if (newApiCallDetails.getAuthorityUrn() != null)
                            authorityNameText.setText(newApiCallDetails.getAuthorityUrn());
                        else
                            authorityNameText.setText("");
                        baseServerUrlText.setText(newApiCallDetails.getBaseServerUrl());
                        callServerUrlText.setText(newApiCallDetails.getCallServerUrl());
                        proxyText.setText(newApiCallDetails.getProxyInfo() == null ? "no proxy used"
                                : newApiCallDetails.getProxyInfo().toString());
                        connectionUserUrnText.setText(newApiCallDetails.getConnectionSslAuthUserUrn());
                        if (newApiCallDetails.getConnectionBasicHttpAuthUsername() == null) {
                            connectionBasicHttpAuthUsernameText.setText("<not used>");
                            connectionBasicHttpAuthUsernameText.setDisable(true);
                            connectionBasicHttpAuthUsernameLabel.setDisable(true);
                        } else {
                            connectionBasicHttpAuthUsernameText.setText(newApiCallDetails.getConnectionBasicHttpAuthUsername());
                            connectionBasicHttpAuthUsernameText.setDisable(false);
                            connectionBasicHttpAuthUsernameLabel.setDisable(false);
                        }
                        connectionDebugIdText.setText(newApiCallDetails.getConnectionId());

                        Date startTime = newApiCallDetails.getStartTime();
                        Date stopTime = newApiCallDetails.getStopTime();
                        if (startTime == null) {
                            callStartTimeField.setText("");
                            callStartTimeRelativeLabel.setText("");
                        } else {
                            callStartTimeField.setText("" + startTime);
                            long millis = System.currentTimeMillis() - startTime.getTime();
                            String relString = TimeUtils.formatMillis(millis, TimeUnit.DAYS, TimeUnit.SECONDS);
                            callStartTimeRelativeLabel.setText("(" + relString + " ago)");
                        }
                        if (stopTime == null) {
                            callStopTimeField.setText("");
                            callStopTimeRelativeLabel.setText("");
                        } else {
                            callStopTimeField.setText("" + stopTime);
                            String res = "(";
                            long millis = System.currentTimeMillis() - stopTime.getTime();
                            res += TimeUtils.formatMillis(millis, TimeUnit.DAYS, TimeUnit.SECONDS) + " ago";
                            if (startTime != null) {
                                long millis2 = stopTime.getTime() - startTime.getTime();
                                res +=
                                        " = " + TimeUtils.formatMillis(millis2, TimeUnit.DAYS, TimeUnit.MILLISECONDS) + " after start";
                            }
                            res += ")";
                            callStopTimeRelativeLabel.setText(res);
                        }

                        httpRequestLineText.setText(
                                newApiCallDetails.getHttpRequestStatusLine() == null ? "<no data>"
                                        : newApiCallDetails.getHttpRequestStatusLine()
                        );
                        httpSentHeaders.setText(
                                newApiCallDetails.getHttpRequestHeaders() == null ? "<no data>"
                                        : newApiCallDetails.getHttpRequestHeaders()
                        );
                        httpSentText.setText(
                                newApiCallDetails.getHttpRequest() == null ? "<no data>"
                                        : newApiCallDetails.getHttpRequest()
                        );
                        httpStatusLineText.setText(
                                newApiCallDetails.getHttpResponseStatusLine() == null ? "<no data>"
                                        : newApiCallDetails.getHttpResponseStatusLine()
                        );
                        httpReceivedHeaders.setText(
                                newApiCallDetails.getHttpResponseHeaders() == null ? "<no data>"
                                        : newApiCallDetails.getHttpResponseHeaders()
                        );
                        httpReceivedText.setText(
                                newApiCallDetails.getHttpResponse() == null ? "<no data>"
                                        : newApiCallDetails.getHttpResponse()
                        );

                        requestSizeLabel.setVisible(true);
                        replySizeLabel.setVisible(true);
                        long requestSizeByte = 0;
                        if (newApiCallDetails.getHttpRequestStatusLine() != null)
                            requestSizeByte += newApiCallDetails.getHttpRequestStatusLine().length();
                        if (newApiCallDetails.getHttpRequestHeaders() != null)
                            requestSizeByte += newApiCallDetails.getHttpRequestHeaders().length();
                        if (newApiCallDetails.getHttpRequest() != null)
                            requestSizeByte += newApiCallDetails.getHttpRequest().length();
                        requestSizeLabel.setText(requestSizeByte + "");
                        long replySizeByte = 0;
                        if (newApiCallDetails.getHttpResponseStatusLine() != null)
                            replySizeByte += newApiCallDetails.getHttpResponseStatusLine().length();
                        if (newApiCallDetails.getHttpResponseHeaders() != null)
                            replySizeByte += newApiCallDetails.getHttpResponseHeaders().length();
                        if (newApiCallDetails.getHttpResponse() != null)
                            replySizeByte += newApiCallDetails.getHttpResponse().length();
                        replySizeLabel.setText(replySizeByte + "");

                        xmlRpcCommand.setText(newApiCallDetails.getGeniMethodName());


                        String xmlRpcSentTextString =
                                (!xmlRpcSentTextShowCredentialsCheckBox.isSelected()) || realJsonSentCheckBox.isSelected()
                                        ?
                                        newApiCallDetails.getXmlRpcRequestJsonString() :
                                        newApiCallDetails.getXmlRpcRequestHumanReadableString();
                        if (xmlRpcSentTextString == null)
                            xmlRpcSentTextString = "null";
                        if (!xmlRpcSentTextShowCredentialsCheckBox.isSelected())
                            xmlRpcSentTextString = removeCredentialsFromJson(xmlRpcSentTextString);
                        xmlRpcSentText.setText(xmlRpcSentTextString);


                        String xmlRpcReceivedTextString =
                                (!xmlRpcReceivedTextShowCredentialsCheckBox.isSelected()) || realJsonReceivedCheckBox.isSelected()
                                        ?
                                        newApiCallDetails.getXmlRpcResponseJsonString() :
                                        newApiCallDetails.getXmlRpcResponseHumanReadableString();
                        if (xmlRpcReceivedTextString == null)
                            xmlRpcReceivedTextString = "null";
                        if (!xmlRpcReceivedTextShowCredentialsCheckBox.isSelected())
                            xmlRpcReceivedTextString = removeCredentialsFromJson(xmlRpcReceivedTextString);
                        xmlRpcReceivedText.setText(xmlRpcReceivedTextString);


                        if (newApiCallDetails.getXmlRpcRequestJsonString() == null) {
                            xmlRpcSentPane.setObj(null);
                        } else {
                            if (newApiCallDetails.getXmlRpcRequestJsonString().length() < 100000)
                                xmlRpcSentPane.setObj(newApiCallDetails.getXmlRpcRequestJsonObject());
                            else
                                xmlRpcSentPane.setObj("output too long for this view");
                        }
                        if (newApiCallDetails.getXmlRpcResponseJsonString() != null) {
                            if (newApiCallDetails.getXmlRpcResponseJsonString().length() < 100000)
                                xmlRpcReceivedPane.setObj(newApiCallDetails.getXmlRpcResponseJsonObject());
                            else
                                xmlRpcReceivedPane.setObj("output too long for this view");
                        } else {
                            xmlRpcReceivedPane.setObj(null);
                        }

                        if (isHtml(newApiCallDetails.getHttpResponse()))
                            showHtml(newApiCallDetails);

                        //result code and error output (error output only hidden if there is none and the call was successful)
                        if (newApiCallDetails.getGeniResponseCode() != null) {
                            resultCode.setText("GeniResponseCode=" + newApiCallDetails.getGeniResponseCode() + " = " + newApiCallDetails.getGeniResponseCodeDescription());

                            assert (newApiCallDetails.getGeniResponseCodeIsSuccess() != null);
                            if (newApiCallDetails.getGeniResponseCodeIsSuccess() &&
                                    (newApiCallDetails.getGeniResponseOutput() == null ||
                                            newApiCallDetails.getGeniResponseOutput().isEmpty())) {
                                resultErrorText.setText("");
                                showResultErrorText(false);
                            } else {
                                resultErrorText.setText(newApiCallDetails.getGeniResponseOutput());
                                showResultErrorText(true);
                            }

                        } else {
                            resultCode.setText("ERROR: no GeniResponseCode");
                            showResultErrorText(true);
                        }


                        if (newApiCallDetails.getRawValue() != null) {
                            resultTab.setDisable(false);

                            showResultText(true);
                            resultText.setText(newApiCallDetails.getRawValue());
                            resultText.setDisable(false);

                            //very broad detection
                            if (newApiCallDetails.getRawValue().contains("stitching"))
                                showStitchingOverview(newApiCallDetails);


                            //determine if XML-tab should be shown
                            if (isXml(newApiCallDetails.getRawValue())) {
                                showXml(newApiCallDetails.getRawValue());
                                //show Xml by looking at low level reply, so even when reinterpreted
                                // (for example as AnyCredential). Does not show compressed Xml.
                            } else if (newApiCallDetails.getXmlRpcGeniResponseValue() != null
                                    && isXml(newApiCallDetails.getXmlRpcGeniResponseValue())) {
                                showXml(newApiCallDetails.getXmlRpcGeniResponseValue());
                            }

                            lowLevelValueTab.setDisable(false);
                            lowLevelOutputTab.setDisable(false);

                            if (newApiCallDetails.getXmlRpcGeniResponseValue() != null) {
                                lowLevelGeniValueText.setDisable(false);
                                lowLevelGeniValueText.setText(newApiCallDetails.getXmlRpcGeniResponseValue());
                            } else {
                                lowLevelGeniValueText.setDisable(true);
                                lowLevelGeniValueText.setText("ERROR: raw result is a Map without \"value\" field:\n\n"
                                        + newApiCallDetails.getRawValue());
                            }

                            if (newApiCallDetails.getXmlRpcGeniResponseCode() != null) {
                                lowLevelGeniCodeText.setDisable(false);
                                lowLevelGeniCodeText.setText(newApiCallDetails.getXmlRpcGeniResponseCode());
                            } else {
                                lowLevelGeniCodeText.setDisable(true);
                                lowLevelGeniOutputText.setText("no \"code\" field in result Map");
                            }

                            if (newApiCallDetails.getXmlRpcGeniResponseOutput() != null) {
                                lowLevelGeniOutputText.setDisable(false);
                                lowLevelGeniOutputText.setText(newApiCallDetails.getXmlRpcGeniResponseOutput());
                            } else {
                                lowLevelGeniOutputText.setDisable(true);
                                lowLevelGeniOutputText.setText("no \"output\" field in result Map");
                            }

                        } else {

                            //very broad detection
                            if (newApiCallDetails.getHttpRequest() != null
                                    && newApiCallDetails.getHttpRequest().contains("stitching")
                                    || newApiCallDetails.getHttpResponse() != null
                                    && newApiCallDetails.getHttpResponse().contains("stitching"))
                                showStitchingOverview(newApiCallDetails);

                            resultCode.setText("ERROR: no reply");
                            showResultErrorText(false);

                            resultText.setText("<null>");
                            showResultText(false);

                            resultTab.setDisable(true);
                            lowLevelValueTab.setDisable(true);
                            lowLevelOutputTab.setDisable(true);
                            lowLevelGeniValueText.setText("ERROR: no reply");
                            lowLevelGeniOutputText.setText("");
                            lowLevelGeniCodeText.setText("");
                            lowLevelGeniValueText.setDisable(true);
                            lowLevelGeniOutputText.setDisable(true);
                            lowLevelGeniCodeText.setDisable(true);
                        }

                        //determine if RSpec-tab should be shown
                        if (newApiCallDetails.getRawValue() != null) {
                            //search for RSpec in reply and request, by checking all strings in the objects
                            if (isRspec(newApiCallDetails.getRawValue())) {
                                showRspec("Reply", newApiCallDetails.getRawValue());
                            } else {
                                if (newApiCallDetails.getXmlRpcGeniResponseValue() != null
                                        && isRspec(newApiCallDetails.getXmlRpcGeniResponseValue())) {
                                    showRspec("Reply", newApiCallDetails.getXmlRpcGeniResponseValue());
                                } else {
                                    if (newApiCallDetails.getXmlRpcResponseJsonString() != null) {
                                        findRspecs(newApiCallDetails.getXmlRpcResponseJsonObject(), (rspec) -> showRspec("Reply", rspec));
                                    }
                                }
                            }
                        }

                        if (newApiCallDetails.getXmlRpcRequestJsonString() != null)
                            findRspecs(newApiCallDetails.getXmlRpcRequestJsonObject(), (rspec) -> showRspec("Request", rspec));

                        if (newApiCallDetails.getProtogeniSpewLogUrl() != null && !newApiCallDetails.getProtogeniSpewLogUrl().trim().isEmpty())
                            showSpewLogFile(newApiCallDetails.getProtogeniSpewLogUrl());


                        if (newApiCallDetails.getExceptionString() != null)
                            showExceptionString(newApiCallDetails.getExceptionString());
                    }
                } catch (Exception | AssertionError e) {
                    LOG.error("Exception while handling log panel change", e);
                    //ignore
                }
            };


    public ObjectProperty<SerializableApiCallDetails> shownLogProperty() {
        return shownLog;
    }

    public SerializableApiCallDetails getShownLog() {
        return shownLog.get();
    }

    public void setShownLog(SerializableApiCallDetails details) {
        shownLog.set(details);
    }

    @FXML
    private void initialize() {
        shownLog.addListener(shownLogChangeListener);

//        rspecShowPanel.editableProperty().set(false);
        logTabPane.getTabs().removeAll(stitchingOverviewTab, xmlTab, rspecTab, spewLogTab, htmlErrorTab, exceptionTab);
        hideRspec();
        LOG.debug("hide Rspec tab");
        logTabPane.getSelectionModel().select(resultTab);

        requestSizeLabel.managedProperty().bind(requestSizeLabel.visibleProperty());
        replySizeLabel.managedProperty().bind(replySizeLabel.visibleProperty());
        requestSizeHeaderLabel.managedProperty().bind(requestSizeHeaderLabel.visibleProperty());
        replySizeHeaderLabel.managedProperty().bind(replySizeHeaderLabel.visibleProperty());
        requestSizeHeaderLabel.visibleProperty().bind(requestSizeLabel.visibleProperty());
        replySizeHeaderLabel.visibleProperty().bind(replySizeLabel.visibleProperty());

        xmlRpcReplySwitchViewButton.setGraphic(fontAwesome.create(FontAwesome.Glyph.EYE).color(Color.BLUE));
        for (javafx.scene.Node node : xmlRpcReplyStackPane.getChildren()) {
            node.managedProperty().bind(node.visibleProperty());
        }
        xmlRpcSentSwitchViewButton.setGraphic(fontAwesome.create(FontAwesome.Glyph.EYE).color(Color.BLUE));
        for (javafx.scene.Node node : xmlRpcSentStackPane.getChildren()) {
            node.managedProperty().bind(node.visibleProperty());
        }
    }

    public void refreshShowCredentials() {
        shownLogChangeListener.changed(shownLog, shownLog.getValue(), shownLog.getValue());
    }

    private static final int MAX_REMOVE_CRED_RESUCRSE_DEPTH = 10;

    @SuppressWarnings("unchecked")
    private static Map removeCredentialsFromJsonHt(Map jsonHt, int recurseDepth) {
        if (recurseDepth >= MAX_REMOVE_CRED_RESUCRSE_DEPTH)
            return jsonHt;
        Map res = new HashMap();
        Set<Map.Entry> entrySet = jsonHt.entrySet();
        for (Map.Entry e : entrySet) {
            Object value = e.getValue();
            Object key = e.getKey();
            if (value instanceof Map) {
                Map h = (Map) value;
                res.put(key, removeCredentialsFromJsonHt(h, recurseDepth + 1));
            } else {
                if (value instanceof List) {
                    List v = (List) value;
                    res.put(key, removeCredentialsFromJsonV(v, recurseDepth + 1));
                } else
                    res.put(key, removeCredentialsFromJsonOtherO(value));
            }
        }
        return res;
    }

    @SuppressWarnings("unchecked")
    private static List removeCredentialsFromJsonV(List jsonV, int recurseDepth) {
        if (recurseDepth >= MAX_REMOVE_CRED_RESUCRSE_DEPTH)
            return jsonV;
        List res = new ArrayList();

        for (Object value : jsonV) {
            if (value instanceof Map) {
                Map h = (Map) value;
                res.add(removeCredentialsFromJsonHt(h, recurseDepth + 1));
            } else {
                if (value instanceof List) {
                    List v = (List) value;
                    res.add(removeCredentialsFromJsonV(v, recurseDepth + 1));
                } else
                    res.add(removeCredentialsFromJsonOtherO(value));
            }
        }

        return res;
    }

    /**
     * Will return true for almost all credentials.
     * Will return false for almost all non-credentials.
     * <p>
     * Is fast.
     */
    private static boolean isCredential(String text) {
        if (text == null) return false;
        String trimmedText = text.trim();
        if (!XmlUtil.couldBeXmlVeryQuickTest(trimmedText)) return false;

        String head = trimmedText;
        if (head.length() > 100)
            head = head.substring(0, 100);
        if (!head.toLowerCase().contains("signed-credential")) return false;

        return XmlUtil.isXmlLikeQuickTest(trimmedText);
    }

    private static Object removeCredentialsFromJsonOtherO(Object jsonO) {
        if (jsonO instanceof String && isCredential((String) jsonO)) {
            return "<!-- credential removed for this view. You can show them using the checkbox below. --->";
        }
        if (jsonO instanceof AnyCredential) {
            return "<!-- credential removed for this view. You can show them using the checkbox below. --->";
        }

        return jsonO;
    }

    public static String removeCredentialsFromJson(String jsonString) {
        if (jsonString == null)
            return "null";
        try {
            Object json = JSonHelper.jsonStringToXmlRpcLikeObject(jsonString);
            if (json != null) {
                return removeCredentialsFromJson(json);
            }
        } catch (Exception e) {
            LOG.debug("Error when processing JSon for log panel view, will ignore.");
        }
        //fallback
        return jsonString;
    }

    public static String removeCredentialsFromJson(Object json) {
        if (json == null)
            return "null";
        try {
            //remove credentials

            Object newjson;
//
            if (json instanceof Map) {
                Map h = (Map) json;
                newjson = removeCredentialsFromJsonHt(h, 0);
            } else {
                if (json instanceof List) {
                    List v = (List) json;
                    newjson = removeCredentialsFromJsonV(v, 0);
                } else
                    newjson = removeCredentialsFromJsonOtherO(json);
            }

            return XmlRpcPrintUtil.xmlRpcObjectToString(newjson);
        } catch (Exception e) {
            LOG.debug("Error when removing credentials from JSon for log panel view, will ignore.");
        } catch (StackOverflowError e) {
            //handle too much recursion by not ignoring credentials
            LOG.debug("StackOverflowError when processing JSon for log panel view, will ignore.");
        }
        //fallback
        return XmlRpcPrintUtil.xmlRpcObjectToString(json);
    }

    public void onXmpRpcSentSwitchView() {
        switchStackPaneVisibleItem(xmlRpcSentStackPane);
    }

    public void onXmpRpcReplySwitchView() {
        switchStackPaneVisibleItem(xmlRpcReplyStackPane);
    }

    public void switchStackPaneVisibleItem(StackPane sp) {
        int nextVisible = 0;
        List<javafx.scene.Node> nodes = sp.getChildren();
        for (int i = 0; i < nodes.size(); i++) {
            javafx.scene.Node node = nodes.get(i);
            if (node.isVisible()) {
                nextVisible = i + 1;
                node.setVisible(false);
            }
        }
        if (nextVisible >= nodes.size())
            nextVisible = 0;
        if (nextVisible < nodes.size())
            nodes.get(nextVisible).setVisible(true);
    }

    private void showResultText(boolean show) {
        if (showingResultText == show) return;
        showingResultText = show;
        if (!show)
            resultTabContent.getChildren().remove(resultTextGroup);
        else
            resultTabContent.getChildren().add(resultTextGroup);
    }

    private void showResultErrorText(boolean show) {
        if (showingResultErrorText == show) return;
        showingResultErrorText = show;
        if (!show)
            resultTabContent.getChildren().remove(resultErrorTextGroup);
        else
            resultTabContent.getChildren().add(resultErrorTextGroup);
    }

    public boolean isHtml(String text) {
        if (text == null) return false;
        String head;
        if (text.length() > 100)
            head = text.substring(0, 100);
        else
            head = text;
        return head.contains("<html") || head.contains("<HTML") || head.contains("<!DOCTYPE HTML");
    }

    /**
     * This test will not work for all cases, but it's good enough for usage in this class
     *
     */
    private static boolean isXml(String text) {
        return text != null && XmlUtil.isXmlLikeQuickTest(text.trim());
    }

    /**
     * Not a solid text, but good enough to exclude most non-RSpecs quickly
     *
     */
    private static boolean isRspec(String text) {
        LOG.trace("isRspec(" + TextUtil.abbreviate(text, 20) + ")");

        if (text == null) return false;
        String trimmedText = text.trim();
        if (!XmlUtil.couldBeXmlVeryQuickTest(trimmedText)) return false;

        String head = trimmedText;
        if (head.length() > 100)
            head = head.substring(0, 100);
        if (!head.contains("rspec")) return false;

        return XmlUtil.isXmlLikeQuickTest(trimmedText);
    }

    @SuppressWarnings("unchecked")
    private static void findRspecs(Object base, Consumer<String> rspecFoundHandler) {
        if (base == null)
            return;

        int ocount = 0;
        int rspecCount = 0;
        LinkedList<Object> objectsToCheck = new LinkedList<>();
        objectsToCheck.add(base);

        while (!objectsToCheck.isEmpty()) {
            Object o = objectsToCheck.removeLast(); //depth first
            //Object o = objectsToCheck.removeFirst(); //breadth first
            ocount++;

            if (o instanceof Map) {
                Map h = (Map) o;
                Set<Map.Entry> entrySet = h.entrySet();
                for (Map.Entry e : entrySet) {
                    Object key = e.getKey();
                    if (key instanceof String && ((String) key).toLowerCase().endsWith("rspec") && e.getValue() instanceof String) {
                        String value = (String) e.getValue();
                        if (isRspec(value)) {
                            rspecCount++;
                            rspecFoundHandler.accept(value);
                        } else {
                            String decodedValue;
                            try {
                                decodedValue = DataConversionUtils.decompressFromBase64(value);
                            } catch (Exception ex) {
                                LOG.debug("Ignoring exception when trying to decompress Rspec reply", ex);
                                decodedValue = null;
                            }
                            if (decodedValue != null && isRspec(decodedValue)) {
                                rspecCount++;
                                rspecFoundHandler.accept(decodedValue);
                            }
                        }
                    } else {
                        objectsToCheck.add(e.getValue());
                    }
                }
            }
            if (o instanceof List) {
                for (Object val : (List) o)
                    objectsToCheck.add(val);
            }
            if (o instanceof String) {
                if (isRspec((String) o)) {
                    rspecCount++;
                    rspecFoundHandler.accept((String) o);
                }
            }
        }
        LOG.debug("Rspec find loop checked " + ocount + " objects and found " + rspecCount + " rspecs");
    }

    public void showXml(String text) {
        this.xmlToFormat = text;
        xmlText.setText("");

        assert Platform.isFxApplicationThread();
        if (!logTabPane.getTabs().contains(xmlTab))
            logTabPane.getTabs().add(xmlTab);
    }

    public void hideRspec() {
        assert Platform.isFxApplicationThread();
        logTabPane.getTabs().removeAll(rspecTab);
        this.rspecTabContents.clear();
        showRspecButtonBox.getChildren().clear();
        rspecText.setText("");
    }

    public void showRspec(String location, String text) {
        LOG.trace("showRspec(" + TextUtil.abbreviate(text, 20) + ")");
        RspecTabContent rspecTabContent = new RspecTabContent(location, text, this.rspecTabContents.size());
        this.rspecTabContents.add(rspecTabContent);
        showRspecButtonBox.getChildren().add(rspecTabContent.button);
        rspecText.setText("");
        rspecTabContent.button.setOnAction(event -> {
            try {
                rspecText.setText(rspecTabContent.getFormattedRspec());
            } catch (Exception e) {
                LOG.warn("Exception while formatting RSpec XML, original xml: \"" + rspecTabContent.rawRspec + "\"", e);
                rspecText.setText("Exception while formatting RSpec XML:\n\n" + TextUtil.exceptionToString(e) + "\n\nOriginal RSpec:\n" + rspecTabContent.rawRspec + "\n");
            }
        });

        assert Platform.isFxApplicationThread();
        if (logTabPane.getTabs().contains(xmlTab))
            logTabPane.getTabs().remove(xmlTab);
        if (!logTabPane.getTabs().contains(rspecTab))
            logTabPane.getTabs().add(rspecTab);
    }

    @FXML
    private void showXmlTextLog() {
        if (xmlToFormat != null) {
            try {
                String formattedXml = XmlUtil.formatXmlFromString(xmlToFormat);
                xmlText.setText(formattedXml);
                xmlToFormat = null;
            } catch (Exception e) {
                LOG.warn("Exception while formatting XML, original xml: \"" + xmlToFormat + "\"", e);
                xmlText.setText("Exception while formatting XML:\n\n" + TextUtil.exceptionToString(e) + "\n\nOriginal xml:\n" + xmlToFormat + "\n");
            }
        }
    }


    public void showStitchingOverview(SerializableApiCallDetails callDetails) {
        if (callDetails != null) {
            assert Platform.isFxApplicationThread();
            if (!logTabPane.getTabs().contains(stitchingOverviewTab))
                logTabPane.getTabs().add(stitchingOverviewTab);

            String stitchingOverviewText = HtmlLogOutput.makeStitchingOverview(callDetails);

            assert stitchingOverviewTextArea != null;
            stitchingOverviewTextArea.setText(stitchingOverviewText + "");
        }
    }

    public void showExceptionString(String exceptionStackTrace) {
        if (exceptionStackTrace != null) {
            if (!logTabPane.getTabs().contains(exceptionTab))
                logTabPane.getTabs().add(exceptionTab);

            String exceptionText = exceptionStackTrace + "\n";
            shownExceptionText = exceptionText;

            assert exceptionTextField != null;
            exceptionTextField.setText(exceptionText);
        }
    }

    @FXML
    private void printException() {
        if (shownExceptionText != null)
            System.err.println(shownExceptionText);
    }

    public void showHtml(SerializableApiCallDetails apiCallDetails) {
        this.htmlApiCallDetails = apiCallDetails;
        if (!logTabPane.getTabs().contains(htmlErrorTab))
            logTabPane.getTabs().add(htmlErrorTab);
    }

    @FXML
    private void showHtmlErrorLog() {
        if (htmlApiCallDetails != null) {
            final WebEngine webEngine = htmlWebView.getEngine();
            webEngine.loadContent(htmlApiCallDetails.getHttpResponse());
            htmlApiCallDetails = null;
        }
    }

    public void showSpewLogFile(String url) {
        spewUrl.setText(url);
        assert Platform.isFxApplicationThread();

        if (!logTabPane.getTabs().contains(spewLogTab))
            logTabPane.getTabs().add(spewLogTab);

        showSpewLog();
    }

    public void showSpewLog() {
        final WebEngine webEngine = spewWebView.getEngine();
        LOG.trace("Loading URL into spewWebView: \"" + spewUrl.getText() + "\"");

        webEngine.getLoadWorker().stateProperty().addListener(
                (ov, oldState, newState) -> {
                    spewWebViewLabel.setText("ProtoGeni Error Log (HTML loading status: " + newState + "):");
                    if (newState == Worker.State.FAILED) {
                        if (spewUrl.getText().startsWith("https"))
                            spewWebViewLabel.setText("ProtoGeni Error Log Failed to load. You are using SSL, perhaps the certificate is self-signed? (This is not yet supported when showing logs)");
                        else
                            spewWebViewLabel.setText("ProtoGeni Error Log Failed to load.");
                    }
                }
        );
        webEngine.setOnAlert(
                msg -> LOG.trace("Spew Log onAlert: " + msg)
        );
        webEngine.setOnStatusChanged(
                msg -> LOG.trace("Spew Log onStatusChanged: " + msg)
        );

        webEngine.load(spewUrl.getText());
    }

    private String getSpew() {
        final WebEngine webEngine = spewWebView.getEngine();
        Document doc = webEngine.getDocument();
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

            StringWriter sw = new StringWriter();
            transformer.transform(new DOMSource(doc), new StreamResult(sw));
            return sw.getBuffer().toString();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @FXML
    private void saveSpew() {
        askSave(getSpew(), "txt");
    }

    @FXML
    private void saveXml() {
        askSave(xmlText.getText(), "xml");
    }

    @FXML
    private void saveRspec() {
        askSave(rspecText.getText(), "xml");
    }

    @FXML
    private void saveResultText() {
        askSave(resultText.getText(), "txt");
    }

    @FXML
    private void saveResultError() {
        askSave(resultErrorText.getText(), "txt");
    }

    @FXML
    private void saveLowLevel() {
        askSave(lowLevelGeniValueText.getText(), "txt");
    }

    @FXML
    private void saveXmlRpcSent() {
        askSave(xmlRpcSentText.getText(), "txt");
    }

    @FXML
    private void saveXmlRpcRecv() {
        askSave(xmlRpcReceivedText.getText(), "txt");
    }

    @FXML
    private void saveHttpSent() {
        askSave(httpSentText.getText(), "txt");
    }

    @FXML
    private void saveHttpRecv() {
        askSave(httpReceivedText.getText(), "txt");
    }

    private final static ObjectMapper MAPPER = new ObjectMapper();

    @FXML
    private void formatHttpReplyJson() {
        try {
            String json = httpReceivedText.getText();
            Object jsonO = MAPPER.readValue(json, Object.class);
            String formattedJson = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(jsonO);
            httpReceivedText.setText(formattedJson);
        } catch (IOException e) {
            LOG.warn("Error formatting received JSON for LogPanel. Will ignore. " + e.getMessage());
        }
    }

    @FXML
    private void formatHttpRequestJson() {
        try {
            String json = httpSentText.getText();
            Object jsonO = MAPPER.readValue(json, Object.class);
            String formattedJson = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(jsonO);
            httpSentText.setText(formattedJson);
        } catch (IOException e) {
            LOG.warn("Error formatting request JSON for LogPanel. Will ignore. " + e.getMessage());
        }
    }

    @FXML
    private void saveAll() {
        SerializableApiCallDetails apiCallDetails = shownApiCallDetails.get();
        String text =
                "Call \"" + apiCallDetails.getGeniMethodName() + "\" in API \"" + apiCallDetails.getApiName() + "\"\n";
        text +=
                "Authority: \"" + apiCallDetails.getAuthorityName() + "\" urn=" + apiCallDetails.getAuthorityUrn() + "\n";
        text += "Base Server URL: \"" + apiCallDetails.getBaseServerUrl() + "\"\n\n";
        text += "Server URL: \"" + apiCallDetails.getCallServerUrl() + "\"\n\n";
        text += "Proxy: \"" + apiCallDetails.getProxyInfo() + "\"\n\n";
        text += "Start time: \"" + apiCallDetails.getStartTime() + "\"\n\n";
        text += "Stop time: \"" + apiCallDetails.getStopTime() + "\"\n\n";
        text += "HTTP data sent:\n" + apiCallDetails.getHttpRequest() + "\n\n";
        text += "HTTP data recv:\n" + apiCallDetails.getHttpResponse() + "\n\n";
        text += "XmlRpc data sent:\n" + apiCallDetails.getXmlRpcRequestJsonString() + "\n\n";
        text += "XmlRpc data recv:\n" + apiCallDetails.getXmlRpcResponseJsonString() + "\n\n";

        if (apiCallDetails.getXmlRpcGeniResponseValue() != null) {
            text += "Reply recv:\n" + apiCallDetails.getXmlRpcGeniResponseValue() + "\n\n";
        }

        if (apiCallDetails.getProtogeniSpewLogUrl() != null)
            text += "Spew log:\n" + getSpew() + "\n\n";

        askSave(text, "txt");

    }

    @FXML
    private void saveAllAsXml() {
        SerializableApiCallDetails apiCallDetails = shownApiCallDetails.get();

        StringWriter sw = new StringWriter();
        try {
            Marshaller m = JAXBContext.newInstance(SerializableApiCallDetails.class).createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            m.marshal(apiCallDetails, sw);
        } catch (JAXBException e) {
            e.printStackTrace();
        }

        String xml = sw.getBuffer().toString();

        askSave(xml, "xml");

    }

    private void askSave(String text, String ext) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Save " + ext);
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter(ext + " Files", "*." + ext),
                new FileChooser.ExtensionFilter("All Files", "*.*")
        );
        File file = fileChooser.showSaveDialog(/*stage*/null);
        if (file != null) {
            IOUtils.stringToFile(file, text);
        }
    }

    private class RspecTabContent {
        public final String location;
        public final String rawRspec;
        public String formattedRspec;
        public final Button button;
        public final int index;

        public RspecTabContent(String location, String rawRspec, int index) {
            this.location = location;
            this.rawRspec = rawRspec;
            this.index = index;
            this.button = new Button(toString());
        }

        public String getFormattedRspec() {
            if (formattedRspec == null) {
                formattedRspec = XmlUtil.formatXmlFromString(rawRspec);
            }
            return formattedRspec;
        }

        @Override
        public String toString() {
            return location + " Rspec " + index;
        }
    }
}
