/**
 * erlyberly, erlang trace debugger
 * Copyright (C) 2016 Andy Till
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package erlyberly;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangBinary;
import com.ericsson.otp.erlang.OtpErlangException;
import com.ericsson.otp.erlang.OtpErlangFun;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangMap;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangString;
import com.ericsson.otp.erlang.OtpErlangTuple;

import erlyberly.format.TermFormatter;
import erlyberly.node.OtpUtil;
import erlyberly.node.RecordManager;
import hextstar.HexstarView;
import javafx.event.ActionEvent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCombination;

@SuppressWarnings("rawtypes")
public class TermTreeView extends TreeView<TermTreeItem> {

    private static final OtpErlangAtom DICT_ATOM = OtpUtil.atom("dict");
    private static final OtpErlangAtom ERLYBERLY_RECORD_FIELD_ATOM = OtpUtil.atom("erlyberly_record_field");
    private OtpErlangAtom moduleName;

    public TermTreeView() {
        getStyleClass().add("term-tree");
        setRoot(new TreeItem<TermTreeItem>());

        MenuItem copyMenuItem = new MenuItem("Copy");
        copyMenuItem.setAccelerator(KeyCombination.keyCombination("shortcut+c"));
        copyMenuItem.setOnAction(this::onCopyCalls);

        MenuItem dictMenuItem = new MenuItem("Dict to List");
        dictMenuItem.setOnAction(this::onViewDict);

        MenuItem hexViewMenuItem = new MenuItem("Hex View");
        hexViewMenuItem.setOnAction(this::onHexView);

        MenuItem decompileFunContextMenu = new MenuItem("Decompile Fun");
        decompileFunContextMenu.setOnAction(this::onDecompileFun);

        setContextMenu(new ContextMenu(copyMenuItem, dictMenuItem, decompileFunContextMenu, hexViewMenuItem));
    }

    public void onHexView(ActionEvent e) {
        TreeItem<TermTreeItem> item = getSelectionModel().getSelectedItem();
        if(item != null && item.getValue().getObject() instanceof OtpErlangBinary) {
            OtpErlangBinary binary = (OtpErlangBinary) item.getValue().getObject();
            HexstarView hexstarView;
            hexstarView = new HexstarView();
            hexstarView.setBinary(binary);
            ErlyBerly.showPane("Hex View", ErlyBerly.wrapInPane(hexstarView));
        }
    }

    public void onDecompileFun(ActionEvent e) {
        TreeItem<TermTreeItem> item = getSelectionModel().getSelectedItem();
        if(item != null && item.getValue().getObject() instanceof OtpErlangFun) {
            OtpErlangFun fun = (OtpErlangFun)item.getValue().getObject();
            try {
                String funSource = ErlyBerly.nodeAPI().decompileFun(fun);
                ErlyBerly.showPane(fun + " Source Code", ErlyBerly.wrapInPane(new CodeView(funSource)));
            } catch (OtpErlangException e1) {
                e1.printStackTrace();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    public void onViewDict(ActionEvent e) {
        TreeItem<TermTreeItem> item = getSelectionModel().getSelectedItem();
        if(item == null)
            return;
        if(item.getValue() == null || !DICT_ATOM.equals(item.getValue().getObject()))
            return;
        if(item.getParent() == null || !(item.getParent().getValue().getObject() instanceof OtpErlangTuple))
            return;

        OtpErlangObject dict = item.getParent().getValue().getObject();

        try {
            OtpErlangList props = ErlyBerly.nodeAPI().dictToPropslist(dict);
            TermTreeView parentControl = new TermTreeView();
            parentControl.populateFromTerm(props);
            ErlyBerly.showPane("dict_to_list", ErlyBerly.wrapInPane(parentControl));
        }
        catch (Exception e1) {
            e1.printStackTrace();
        }
    }

    public void populateFromListContents(OtpErlangAtom moduleName, OtpErlangList list) {
        for (OtpErlangObject a : list) {
            populateFromTerm(moduleName, a);
        }
    }

    public void populateFromListContents(OtpErlangList list) {
        populateFromListContents(null, list);
    }

    public void populateFromTerm(OtpErlangObject obj) {
        populateFromTerm(null, obj);
    }


    public void populateFromTerm(OtpErlangAtom moduleName, OtpErlangObject obj) {
        this.moduleName = moduleName;
        setShowRoot(false);
        addToTreeItem(getRoot(), obj);
    }

    private void addToTreeItem(TreeItem<TermTreeItem> parent, OtpErlangObject obj) {
        TermFormatter f = ErlyBerly.getTermFormatter();

        if(obj instanceof OtpErlangBinary) {
            String termString = f.toString(obj);
            TreeItem<TermTreeItem> item = new TreeItem<>(new TermTreeItem(obj, termString));
            parent.getChildren().add(item);
        }
        else if(obj instanceof OtpErlangTuple) {
            OtpErlangObject[] elements = ((OtpErlangTuple) obj).elements();

            if(elements.length == 0) {
                parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.emptyTupleString())));
            }
            else {
                TreeItem<TermTreeItem> tupleItem;
                if(OtpUtil.isErlyberlyRecord(obj)) {
                    // this is the "old" way show tuples with record metadata, where the metadata is in the
                    // term itself, rather than stored separately in the RecordManager
                    String recordNameText = "#" + OtpUtil.tupleElement(1, obj) + " ";

                    tupleItem = new TreeItem<>(new TermTreeItem(obj, f.tupleLeftParen()));
                    tupleItem.setGraphic(recordLabel(recordNameText));
                    parent.getChildren().add(tupleItem);
                    tupleItem.setExpanded(true);
                    elements = OtpUtil.iterableElements(OtpUtil.tupleElement(2, obj));
                    for (OtpErlangObject e : elements) {
                        addToTreeItem(tupleItem, e);
                    }
                    parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.tupleRightParen())));
                }
                else if(isRecordField(obj)) {
                    tupleItem = new TreeItem<>(new TermTreeItem(obj, " "));
                    String recordField = "";
                    OtpErlangObject recordFieldNameObj = OtpUtil.tupleElement(1, obj);
                    // do not toString an OtpErlangString because that will add the quotes around it
                    if(recordFieldNameObj instanceof OtpErlangString) {
                        recordField = ((OtpErlangString)recordFieldNameObj).stringValue();
                    }
                    else {
                        recordField = recordFieldNameObj.toString();
                    }
                    tupleItem.setGraphic(recordLabel(recordField + " = "));
                    tupleItem.setExpanded(true);

                    parent.getChildren().add(tupleItem);

                    OtpErlangObject value = OtpUtil.tupleElement(2, obj);
                    if(OtpUtil.isLittleTerm(value))
                        tupleItem.setValue(new TermTreeItem(value, f.toString(obj)));
                    else
                        addToTreeItem(tupleItem, value);
                }
                else {
                    List<String> recordNames = null;
                    OtpErlangTuple objTuple = (OtpErlangTuple) obj;
                    if((recordNames = findRecordDef(objTuple)) != null) {
                        String recordNameText = "#" + OtpUtil.tupleElement(0, obj) + " ";
                        tupleItem = new TreeItem<>(new TermTreeItem(obj, f.tupleLeftParen()));
                        tupleItem.setGraphic(recordLabel(recordNameText));
                        parent.getChildren().add(tupleItem);
                        tupleItem.setExpanded(true);
                        elements = objTuple.elements();
                        for (int i = 1; i < elements.length; i++) {
                            addToTreeItem(tupleItem, OtpUtil.tuple(ERLYBERLY_RECORD_FIELD_ATOM, recordNames.get(i-1), elements[i]));
                        }
                        parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.tupleRightParen())));
                    }
                    else {
                        tupleItem = new TreeItem<>();
                        tupleItem.setExpanded(true);
                        if(OtpUtil.isLittleTerm(obj)) {
                            tupleItem.setValue(new TermTreeItem(obj, f.toString(obj)));
                            parent.getChildren().add(tupleItem);
                        }
                        else {
                            tupleItem.setValue(new TermTreeItem(obj, f.tupleLeftParen()));
                            for (OtpErlangObject e : elements) {
                                addToTreeItem(tupleItem, e);
                            }
                            parent.getChildren().add(tupleItem);
                            parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.tupleRightParen())));
                        }
                    }
                }
            }
        }
        else if(obj instanceof OtpErlangList) {
            OtpErlangObject[] elements = ((OtpErlangList) obj).elements();

            if(elements.length == 0) {
                parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.emptyListString())));
            }
            else {
                TreeItem<TermTreeItem> listItem;

                listItem = new TreeItem<>(new TermTreeItem(obj, f.listLeftParen()));
                listItem.setExpanded(true);


                if(OtpUtil.isLittleTerm(obj)) {
                    listItem.setValue(new TermTreeItem(obj, f.toString(obj)));
                    parent.getChildren().add(listItem);
                }
                else {
                    listItem.setValue(new TermTreeItem(obj, f.listLeftParen()));
                    for (OtpErlangObject e : elements) {
                        addToTreeItem(listItem, e);
                    }
                    if (!((OtpErlangList)obj).isProper()) {
                        listItem.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.cons())));
                        addToTreeItem(listItem, ((OtpErlangList)obj).getLastTail());
                    }
                    parent.getChildren().add(listItem);
                    parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.listRightParen())));
                }
            }
        }
        else if (obj instanceof OtpErlangMap){
            TreeItem<TermTreeItem> mapNode = new TreeItem<>(new TermTreeItem(obj, f.mapLeft(obj)));
            parent.getChildren().add(mapNode);
            for (Map.Entry<OtpErlangObject,OtpErlangObject> e : ((OtpErlangMap) obj).entrySet()) {
                if (f.isHiddenField(e.getKey()))
                    continue;
                String keyStr = f.mapKeyToString(e.getKey());
                String valStr = f.toString(e.getValue());
                // Inline short values that are not maps
                if (!((OtpErlangObject)e.getValue() instanceof OtpErlangMap) && (valStr.length() < 50 )) {
                    TreeItem<TermTreeItem> key = new TreeItem<>(new TermTreeItem(e.getKey(), valStr));
                    key.setGraphic(recordLabel(keyStr));
                    mapNode.getChildren().add(key);
                }
                else {
                    TreeItem<TermTreeItem> key = new TreeItem<>(new TermTreeItem(e.getKey(), ""));
                    key.setGraphic(recordLabel(keyStr));
                    addToTreeItem(key, e.getValue());
                    mapNode.getChildren().add(key);
                }
            }
            parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.mapRight())));
        }
        else {
            parent.getChildren().add(new TreeItem<>(new TermTreeItem(obj, f.toString(obj))));
        }
    }

    /**
     * return null if this tuple is not a known record, or a string list
     * of record field names if it is.
     */
    private List<String> findRecordDef(OtpErlangTuple obj) {
        if(obj.arity() == 0 || !(obj.elementAt(0) instanceof OtpErlangAtom))
            return null;
        OtpErlangAtom recordName = (OtpErlangAtom) obj.elementAt(0);
        return ErlyBerly.nodeAPI().getRecordManager().get(new RecordManager.RecordKey(moduleName, recordName));
    }

    private Label recordLabel(String recordNameText) {
        Label label;
        label = new Label(recordNameText);
        label.getStyleClass().add("record-label");
        return label;
    }

    private boolean isRecordField(OtpErlangObject obj) {
        return OtpUtil.isTupleTagged(ERLYBERLY_RECORD_FIELD_ATOM, obj);
    }

    private void onCopyCalls(ActionEvent e) {
        StringBuilder sbuilder = new StringBuilder();
        for (TreeItem item : getSelectionModel().getSelectedItems()) {
            copyTerms(item, sbuilder);
        }
    }

    private void copyTerms(TreeItem item, StringBuilder sbuilder) {
        for (Object obj : item.getChildren()) {
            copyTerms((TreeItem) obj, sbuilder);
        }
        copyToClipboard(sbuilder);
    }

    private void copyToClipboard(StringBuilder sbuilder) {
        final Clipboard clipboard = Clipboard.getSystemClipboard();
        final ClipboardContent content = new ClipboardContent();
        content.putString(sbuilder.toString());
        clipboard.setContent(content);
    }
}