/*
 *  Copyright (C) 2010-2021 JPEXS
 * 
 *  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 com.jpexs.decompiler.flash.gui.action;

import com.jpexs.decompiler.flash.DisassemblyListener;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.action.Action;
import com.jpexs.decompiler.flash.action.ActionGraph;
import com.jpexs.decompiler.flash.action.ActionList;
import com.jpexs.decompiler.flash.action.ConstantPoolTooBigException;
import com.jpexs.decompiler.flash.action.deobfuscation.BrokenScriptDetector;
import com.jpexs.decompiler.flash.action.parser.ActionParseException;
import com.jpexs.decompiler.flash.action.parser.pcode.ASMParser;
import com.jpexs.decompiler.flash.action.parser.script.ActionScript2Parser;
import com.jpexs.decompiler.flash.action.parser.script.ActionScriptLexer;
import com.jpexs.decompiler.flash.action.parser.script.ParsedSymbol;
import com.jpexs.decompiler.flash.action.parser.script.SymbolType;
import com.jpexs.decompiler.flash.action.swf4.ActionPush;
import com.jpexs.decompiler.flash.action.swf4.ConstantIndex;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode;
import com.jpexs.decompiler.flash.gui.AppStrings;
import com.jpexs.decompiler.flash.gui.DebugPanel;
import com.jpexs.decompiler.flash.gui.DebuggerHandler;
import com.jpexs.decompiler.flash.gui.FasterScrollPane;
import com.jpexs.decompiler.flash.gui.GraphDialog;
import com.jpexs.decompiler.flash.gui.HeaderLabel;
import com.jpexs.decompiler.flash.gui.Main;
import com.jpexs.decompiler.flash.gui.MainPanel;
import com.jpexs.decompiler.flash.gui.SearchListener;
import com.jpexs.decompiler.flash.gui.SearchPanel;
import com.jpexs.decompiler.flash.gui.TagEditorPanel;
import com.jpexs.decompiler.flash.gui.View;
import com.jpexs.decompiler.flash.gui.ViewMessages;
import com.jpexs.decompiler.flash.gui.controls.JPersistentSplitPane;
import com.jpexs.decompiler.flash.gui.controls.NoneSelectedButtonGroup;
import com.jpexs.decompiler.flash.gui.editor.DebuggableEditorPane;
import com.jpexs.decompiler.flash.gui.editor.LinkHandler;
import com.jpexs.decompiler.flash.gui.tagtree.TagTreeModel;
import com.jpexs.decompiler.flash.helpers.HighlightedText;
import com.jpexs.decompiler.flash.helpers.HighlightedTextWriter;
import com.jpexs.decompiler.flash.helpers.hilight.HighlightData;
import com.jpexs.decompiler.flash.helpers.hilight.Highlighting;
import com.jpexs.decompiler.flash.search.ActionScriptSearch;
import com.jpexs.decompiler.flash.search.ActionSearchResult;
import com.jpexs.decompiler.flash.search.ScriptSearchListener;
import com.jpexs.decompiler.flash.search.ScriptSearchResult;
import com.jpexs.decompiler.flash.tags.DoInitActionTag;
import com.jpexs.decompiler.flash.tags.base.ASMSource;
import com.jpexs.decompiler.graph.CompilationException;
import com.jpexs.helpers.CancellableWorker;
import com.jpexs.helpers.Helper;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.border.BevelBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.tree.TreePath;
import jsyntaxpane.SyntaxDocument;
import jsyntaxpane.Token;
import jsyntaxpane.TokenType;
import jsyntaxpane.actions.ActionUtils;

/**
 *
 * @author JPEXS
 */
public class ActionPanel extends JPanel implements SearchListener<ScriptSearchResult>, TagEditorPanel {

    private static final Logger logger = Logger.getLogger(ActionPanel.class.getName());

    private JPanel brokenHintPanel;
    private MainPanel mainPanel;

    public DebuggableEditorPane editor;

    public DebuggableEditorPane decompiledEditor;

    public JPersistentSplitPane splitPane;

    public JButton saveButton = new JButton(AppStrings.translate("button.save"), View.getIcon("save16"));

    public JButton editButton = new JButton(AppStrings.translate("button.edit.script.disassembled"), View.getIcon("edit16"));

    public JButton cancelButton = new JButton(AppStrings.translate("button.cancel"), View.getIcon("cancel16"));

    public JButton editDecompiledButton = new JButton(AppStrings.translate("button.edit.script.decompiled"), View.getIcon("edit16"));

    public JButton saveDecompiledButton = new JButton(AppStrings.translate("button.save"), View.getIcon("save16"));

    public JButton cancelDecompiledButton = new JButton(AppStrings.translate("button.cancel"), View.getIcon("cancel16"));

    public JToggleButton hexButton;

    public JToggleButton hexOnlyButton;

    public JToggleButton constantsViewButton;

    public JToggleButton resolveConstantsButton;

    public JToggleButton showFileOffsetInPcodeHexButton;

    public JToggleButton showOriginalBytesInPcodeHexButton;

    public JLabel asmLabel = new HeaderLabel(AppStrings.translate("panel.disassembled"));

    public JLabel decLabel = new HeaderLabel(AppStrings.translate("panel.decompiled"));

    private boolean ignoreCarret = false;

    private boolean editMode = false;

    private boolean editDecompiledMode = false;

    private ActionList lastCode;

    private ASMSource src;

    public JPanel topButtonsPan;

    private HighlightedText srcWithHex;

    private HighlightedText srcNoHex;

    private HighlightedText srcHexOnly;

    private HighlightedText srcConstants;

    private HighlightedText disassembledText = HighlightedText.EMPTY;

    private HighlightedText lastDecompiled = HighlightedText.EMPTY;

    private ASMSource lastASM;

    public SearchPanel<ScriptSearchResult> searchPanel;

    private CancellableWorker setSourceWorker;

    private List<Runnable> scriptListeners = new ArrayList<>();

    public void addScriptListener(Runnable listener) {
        scriptListeners.add(listener);
    }

    public void removeScriptListener(Runnable listener) {
        scriptListeners.remove(listener);
    }

    private void fireScript() {
        List<Runnable> list = new ArrayList<>(scriptListeners);
        for (Runnable r : list) {
            r.run();
        }
    }

    public void clearSource() {
        View.checkAccess();

        lastCode = null;
        lastASM = null;
        lastDecompiled = HighlightedText.EMPTY;
        searchPanel.clear();
        src = null;
        srcWithHex = null;
        srcNoHex = null;
        srcHexOnly = null;
        srcConstants = null;
    }

    public String getStringUnderCursor() {
        View.checkAccess();
        int pos = decompiledEditor.getCaretPosition();
        return getStringUnderPosition(pos, decompiledEditor);
    }

    public String getStringUnderPosition(int pos, JTextComponent component) {
        SyntaxDocument sDoc = ActionUtils.getSyntaxDocument(component);
        if (sDoc != null) {
            Token t = sDoc.getTokenAt(pos + 1);
            String ident = null;
            //It should be identifier or obfuscated identifier
            if (t != null && (t.type == TokenType.IDENTIFIER || t.type == TokenType.REGEX)) {
                CharSequence tData = t.getText(sDoc);
                ident = tData.toString();
                //We need to get unescaped identifier, so we use our Lexer
                ActionScriptLexer lex = new ActionScriptLexer(new StringReader(ident));
                try {
                    ParsedSymbol symb = lex.lex();
                    if (symb.type == SymbolType.IDENTIFIER) {
                        ident = (String) symb.value;
                    } else {
                        ident = null;
                    }
                } catch (IOException | ActionParseException ex) {
                    ident = null;
                }
            }
            if (ident == null) {
                Highlighting h = Highlighting.searchPos(lastDecompiled.getInstructionHighlights(), pos);
                if (h != null) {
                    List<Action> list = lastCode;
                    Action lastIns = null;
                    int inspos = 0;
                    Action selIns = null;
                    for (Action ins : list) {
                        if (h.getProperties().offset == ins.getAddress()) {
                            selIns = ins;
                            break;
                        }
                        if (ins.getAddress() > h.getProperties().offset && lastIns != null) {
                            inspos = (int) (h.getProperties().offset - lastIns.getAddress());
                            selIns = lastIns;
                            break;
                        }
                        lastIns = ins;
                    }
                    if (selIns != null) {
                        if (selIns instanceof ActionPush) {
                            ActionPush ap = (ActionPush) selIns;
                            Object var = ap.values.get(inspos - 1);
                            String identifier = null;
                            if (var instanceof String) {
                                identifier = (String) var;
                            }
                            if (var instanceof ConstantIndex) {
                                identifier = ap.constantPool.get(((ConstantIndex) var).index);
                            }
                            return identifier;
                        }
                    }

                }
            } else {
                return ident;
            }
        }
        return null;
    }

    public List<ActionSearchResult> search(SWF swf, final String txt, boolean ignoreCase, boolean regexp, boolean pcode, CancellableWorker<Void> worker, Map<String, ASMSource> scope) {
        if (txt != null && !txt.isEmpty()) {
            searchPanel.setOptions(ignoreCase, regexp);

            String workText = AppStrings.translate("work.searching");
            String decAdd = AppStrings.translate("work.decompiling");
            return new ActionScriptSearch().searchAs2(swf, txt, ignoreCase, regexp, pcode, new ScriptSearchListener() {
                @Override
                public void onDecompile(int pos, int total, String name) {
                    Main.startWork(workText + " \"" + txt + "\", " + decAdd + " - (" + pos + "/" + total + ") " + name + "... ", worker);
                }

                @Override
                public void onSearch(int pos, int total, String name) {
                    Main.startWork(workText + " \"" + txt + "\" - (" + pos + "/" + total + ") " + name + "... ", worker);
                }
            }, scope);
        }

        return null;
    }

    private void setDecompiledText(final String scriptName, final String text) {
        View.checkAccess();

        ignoreCarret = true;
        decompiledEditor.setScriptName(scriptName);
        decompiledEditor.setText(text);
        BrokenScriptDetector det = new BrokenScriptDetector();
        if (det.codeIsBroken(text)) {
            brokenHintPanel.setVisible(true);
        } else {
            brokenHintPanel.setVisible(false);
        }

        ignoreCarret = false;
    }

    private void setEditorText(final String scriptName, final String text, final String contentType) {
        View.checkAccess();

        ignoreCarret = true;
        editor.setScriptName("#PCODE " + scriptName);
        editor.changeContentType(contentType);
        editor.setText(text);
        ignoreCarret = false;
    }

    private void setText(final HighlightedText text, final String contentType, final String scriptName) {
        View.checkAccess();

        int pos = editor.getCaretPosition();
        Highlighting lastH = null;
        for (Highlighting h : disassembledText.getInstructionHighlights()) {
            if (pos < h.startPos) {
                break;
            }
            lastH = h;
        }
        Long offset = lastH == null ? 0 : lastH.getProperties().offset;
        disassembledText = text;
        setEditorText(scriptName, text.text, contentType);
        Highlighting h = Highlighting.searchOffset(disassembledText.getInstructionHighlights(), offset);
        if (h != null) {
            if (h.startPos <= editor.getDocument().getLength()) {
                editor.setCaretPosition(h.startPos);
            }
        }
    }

    private HighlightedText getHighlightedText(ScriptExportMode exportMode, ActionList actions) {
        if (actions == null) {
            logger.log(Level.WARNING, "Action list is null");
            return HighlightedText.EMPTY;
        }

        ASMSource asm = (ASMSource) src;
        DisassemblyListener listener = getDisassemblyListener();
        asm.addDisassemblyListener(listener);
        HighlightedTextWriter writer = new HighlightedTextWriter(Configuration.getCodeFormatting(), true);
        try {
            asm.getASMSource(exportMode, writer, actions);
        } catch (InterruptedException ex) {
            logger.log(Level.SEVERE, null, ex);
        }

        asm.removeDisassemblyListener(listener);
        return new HighlightedText(writer);
    }

    private void updateHexButtons(ScriptExportMode exportMode) {
        View.checkAccess();

        showFileOffsetInPcodeHexButton.setVisible(exportMode == ScriptExportMode.PCODE_HEX);
        showOriginalBytesInPcodeHexButton.setVisible(exportMode == ScriptExportMode.PCODE_HEX);
        resolveConstantsButton.setVisible(exportMode != ScriptExportMode.CONSTANTS && exportMode != ScriptExportMode.HEX);
    }

    private void setHex(ScriptExportMode exportMode, String scriptName, ActionList actions) {
        View.checkAccess();
        updateHexButtons(exportMode);

        switch (exportMode) {
            case PCODE:
                if (srcNoHex == null) {
                    srcNoHex = getHighlightedText(exportMode, actions);
                }

                setText(srcNoHex, "text/flasm", scriptName);
                break;
            case PCODE_HEX:
                if (srcWithHex == null) {
                    srcWithHex = getHighlightedText(exportMode, actions);
                }

                setText(srcWithHex, "text/flasm", scriptName);
                break;
            case HEX:
                if (srcHexOnly == null) {
                    HighlightedTextWriter writer = new HighlightedTextWriter(Configuration.getCodeFormatting(), true);
                    Helper.byteArrayToHexWithHeader(writer, src.getActionBytes().getRangeData());
                    srcHexOnly = new HighlightedText(writer);
                }

                setText(srcHexOnly, "text/plain", scriptName);
                break;
            case CONSTANTS:
                if (srcConstants == null) {
                    srcConstants = getHighlightedText(exportMode, actions);
                }

                setText(srcConstants, "text/plain", scriptName);
                break;
            default:
                throw new Error("Export mode not supported: " + exportMode);
        }
    }

    private DisassemblyListener getDisassemblyListener() {
        DisassemblyListener listener = new DisassemblyListener() {
            int percent = 0;

            String phase = "";

            private void progress(String phase, long pos, long total) {
                if (total < 1) {
                    return;
                }
                int newpercent = (int) (pos * 100 / total);
                if (((newpercent > percent) || (!this.phase.equals(phase))) && newpercent <= 100) {
                    percent = newpercent;
                    this.phase = phase;

                    // todo: honfika: it is very slow to show every percent
                    View.execInEventDispatch(() -> {
                        setEditorText("-", "; " + AppStrings.translate("work.disassembling") + " - " + phase + " " + percent + "%...", "text/flasm");
                    });
                }
            }

            @Override
            public void progressReading(long pos, long total) {
                progress(AppStrings.translate("disassemblingProgress.reading"), pos, total);
            }

            @Override
            public void progressToString(long pos, long total) {
                progress(AppStrings.translate("disassemblingProgress.toString"), pos, total);
            }

            @Override
            public void progressDeobfuscating(long pos, long total) {
                progress(AppStrings.translate("disassemblingProgress.deobfuscating"), pos, total);
            }
        };
        return listener;
    }

    public void setSource(final ASMSource src, final boolean useCache) {
        View.checkAccess();

        if (setSourceWorker != null) {
            setSourceWorker.cancel(true);
            setSourceWorker = null;
        }

        clearSource();
        this.src = src;
        final ASMSource asm = (ASMSource) src;

        if (!useCache) {
            SWF.uncache(asm);
        }

        boolean decompile = Configuration.decompile.get();
        HighlightedText decompiledText;
        if (!decompile) {
            decompiledText = new HighlightedText(Helper.getDecompilationSkippedComment());
        } else {
            decompiledText = SWF.getFromCache(asm);
        }

        setDecompiledEditMode(false);
        setEditMode(false);

        ActionList actions = SWF.getActionListFromCache(asm);

        boolean disassemblingNeeded = actions == null;
        boolean decompileNeeded = decompiledText == null;

        if (disassemblingNeeded || decompileNeeded) {
            CancellableWorker worker = new CancellableWorker() {
                @Override
                protected Void doInBackground() throws Exception {

                    ActionList innerActions = actions;
                    if (disassemblingNeeded) {
                        View.execInEventDispatch(() -> {
                            setEditorText(asm.getScriptName(), "; " + AppStrings.translate("work.disassembling") + "...", "text/flasm");
                            if (decompileNeeded) {
                                setDecompiledText("-", "// " + AppStrings.translate("work.waitingfordissasembly") + "...");
                            }
                        });

                        DisassemblyListener listener = getDisassemblyListener();
                        asm.addDisassemblyListener(listener);
                        innerActions = asm.getActions();
                        asm.removeDisassemblyListener(listener);
                    }

                    if (decompileNeeded) {
                        View.execInEventDispatch(() -> {
                            setDecompiledText("-", "// " + AppStrings.translate("work.decompiling") + "...");
                        });

                        HighlightedText htext = SWF.getCached(asm, innerActions);
                        ActionList finalActions = innerActions;
                        View.execInEventDispatch(() -> {
                            setSourceCompleted(asm, htext, finalActions);
                        });
                    }

                    return null;
                }

                @Override
                protected void done() {
                    View.execInEventDispatch(() -> {
                        setSourceWorker = null;
                        if (!Main.isDebugging()) {
                            Main.stopWork();
                        }

                        try {
                            get();
                        } catch (CancellationException ex) {
                            setEditorText("-", "; " + AppStrings.translate("work.canceled"), "text/flasm");
                        } catch (Exception ex) {
                            logger.log(Level.SEVERE, "Error", ex);
                            setDecompiledText("-", "// " + AppStrings.translate("decompilationError") + ": " + ex);
                        }
                    });
                }
            };

            worker.execute();
            setSourceWorker = worker;
            if (!Main.isDebugging()) {
                Main.startWork(AppStrings.translate("work.decompiling") + "...", worker);
            }
        } else {
            setSourceCompleted(asm, decompiledText, actions);
        }
    }

    private void setSourceCompleted(ASMSource asm, HighlightedText decompiledText, ActionList actions) {
        View.checkAccess();

        if (decompiledText == null) {
            decompiledText = HighlightedText.EMPTY;
        }

        lastASM = asm;
        lastCode = actions;
        lastDecompiled = decompiledText;

        setHex(getExportMode(), asm.getScriptName(), actions);
        setDecompiledText(asm.getScriptName(), decompiledText.text);
        fireScript();
    }

    public void hilightOffset(long offset) {
        View.checkAccess();
    }

    public int getLocalDeclarationOfPos(int pos) {
        View.checkAccess();

        Highlighting sh = Highlighting.searchPos(lastDecompiled.getSpecialHighlights(), pos);
        Highlighting h = Highlighting.searchPos(lastDecompiled.getInstructionHighlights(), pos);

        if (h == null) {
            return -1;
        }

        List<Highlighting> tms = Highlighting.searchAllPos(lastDecompiled.getMethodHighlights(), pos);
        if (tms.isEmpty()) {
            return -1;
        }
        for (Highlighting tm : tms) {

            List<Highlighting> tm_tms = Highlighting.searchAllLocalNames(lastDecompiled.getMethodHighlights(), tm.getProperties().localName);
            //is it already declaration?
            if (h.getProperties().declaration || (sh != null && sh.getProperties().declaration)) {
                return -1; //no jump
            }

            String lname = h.getProperties().localName;
            if ("this".equals(lname)) {
                Highlighting ch = Highlighting.searchPos(lastDecompiled.getClassHighlights(), pos);
                //    int cindex = (int) ch.getProperties().index;
                return ch.startPos;
            }

            HighlightData hData = h.getProperties();
            HighlightData search = new HighlightData();
            search.declaration = hData.declaration;
            //search.declaredType = hData.declaredType;
            search.localName = hData.localName;
            //search.specialValue = hData.specialValue;
            if (search.isEmpty()) {
                return -1;
            }
            search.declaration = true;

            for (Highlighting tm1 : tm_tms) {
                Highlighting rh = Highlighting.search(lastDecompiled.getInstructionHighlights(), search, tm1.startPos, tm1.startPos + tm1.len);
                if (rh != null) {
                    return rh.startPos;
                }
            }
        }

        return -1;
    }

    public ActionPanel(MainPanel mainPanel) {
        this.mainPanel = mainPanel;
        editor = new DebuggableEditorPane();
        editor.setEditable(false);
        decompiledEditor = new DebuggableEditorPane();
        decompiledEditor.setEditable(false);
        decompiledEditor.setLinkHandler(new LinkHandler() {
            @Override
            public boolean isLink(Token token) {
                int pos = token.start;
                Highlighting h = Highlighting.searchPos(lastDecompiled.getInstructionHighlights(), pos);
                if (h != null) {
                    if (h.getProperties().localName != null && !h.getProperties().declaration) {
                        return getLocalDeclarationOfPos(pos) != -1;
                    }
                }
                return false;
            }

            @Override
            public void handleLink(Token token) {
                int pos = token.start;
                int tpos = getLocalDeclarationOfPos(pos);
                if (tpos > -1) {
                    //System.err.println("goto " + tpos);
                    decompiledEditor.setCaretPosition(tpos);
                } else {
                    //System.err.println("cannot handle");
                }
            }

            @Override
            public Highlighter.HighlightPainter linkPainter() {
                return decompiledEditor.linkPainter();
            }
        });

        searchPanel = new SearchPanel<>(new FlowLayout(), this);

        JButton graphButton = new JButton(View.getIcon("graph16"));
        graphButton.addActionListener(this::graphButtonActionPerformed);
        graphButton.setToolTipText(AppStrings.translate("button.viewgraph"));
        graphButton.setMargin(new Insets(3, 3, 3, 3));

        hexButton = new JToggleButton(View.getIcon("hexas16"));
        hexButton.addActionListener(this::hexButtonActionPerformed);
        hexButton.setToolTipText(AppStrings.translate("button.viewhexpcode"));
        hexButton.setMargin(new Insets(3, 3, 3, 3));

        hexOnlyButton = new JToggleButton(View.getIcon("hex16"));
        hexOnlyButton.addActionListener(this::hexOnlyButtonActionPerformed);
        hexOnlyButton.setToolTipText(AppStrings.translate("button.viewhex"));
        hexOnlyButton.setMargin(new Insets(3, 3, 3, 3));

        constantsViewButton = new JToggleButton(View.getIcon("constantpool16"));
        constantsViewButton.addActionListener(this::constantsViewButtonActionPerformed);
        constantsViewButton.setToolTipText(AppStrings.translate("button.viewConstants"));
        constantsViewButton.setMargin(new Insets(3, 3, 3, 3));

        NoneSelectedButtonGroup exportModeButtonGroup = new NoneSelectedButtonGroup();
        exportModeButtonGroup.add(hexButton);
        exportModeButtonGroup.add(hexOnlyButton);
        exportModeButtonGroup.add(constantsViewButton);

        resolveConstantsButton = new JToggleButton(View.getIcon("resolveconst16"));
        resolveConstantsButton.addActionListener(this::resolveConstantsButtonActionPerformed);
        resolveConstantsButton.setToolTipText(AppStrings.translate("button.resolveConstants"));
        resolveConstantsButton.setMargin(new Insets(3, 3, 3, 3));
        resolveConstantsButton.setSelected(Configuration.resolveConstants.get());

        showFileOffsetInPcodeHexButton = new JToggleButton(View.getIcon("fileoffset16"));
        showFileOffsetInPcodeHexButton.addActionListener(this::showFileOffsetInPcodeHexButtonActionPerformed);
        showFileOffsetInPcodeHexButton.setToolTipText(AppStrings.translate("button.showFileOffsetInPcodeHex"));
        showFileOffsetInPcodeHexButton.setMargin(new Insets(3, 3, 3, 3));
        showFileOffsetInPcodeHexButton.setSelected(Configuration.showFileOffsetInPcodeHex.get());

        showOriginalBytesInPcodeHexButton = new JToggleButton(View.getIcon("originalbytes16"));
        showOriginalBytesInPcodeHexButton.addActionListener(this::showOriginalBytesInPcodeHexButtonActionPerformed);
        showOriginalBytesInPcodeHexButton.setToolTipText(AppStrings.translate("button.showOriginalBytesInPcodeHex"));
        showOriginalBytesInPcodeHexButton.setMargin(new Insets(3, 3, 3, 3));
        showOriginalBytesInPcodeHexButton.setSelected(Configuration.showOriginalBytesInPcodeHex.get());

        topButtonsPan = new JPanel();
        topButtonsPan.setLayout(new BoxLayout(topButtonsPan, BoxLayout.X_AXIS));
        topButtonsPan.add(graphButton);
        topButtonsPan.add(Box.createRigidArea(new Dimension(10, 0)));
        topButtonsPan.add(hexButton);
        topButtonsPan.add(hexOnlyButton);
        topButtonsPan.add(constantsViewButton);
        topButtonsPan.add(Box.createRigidArea(new Dimension(10, 0)));
        topButtonsPan.add(resolveConstantsButton);
        topButtonsPan.add(Box.createRigidArea(new Dimension(10, 0)));
        topButtonsPan.add(showFileOffsetInPcodeHexButton);
        topButtonsPan.add(showOriginalBytesInPcodeHexButton);

        if (hexOnlyButton.isSelected()) {
            updateHexButtons(ScriptExportMode.HEX);
        } else if (constantsViewButton.isSelected()) {
            updateHexButtons(ScriptExportMode.CONSTANTS);
        } else {
            updateHexButtons(ScriptExportMode.PCODE);
        }

        JPanel panCode = new JPanel(new BorderLayout());
        panCode.add(new FasterScrollPane(editor), BorderLayout.CENTER);
        panCode.add(topButtonsPan, BorderLayout.NORTH);

        JPanel panB = new JPanel();
        panB.setLayout(new BorderLayout());
        asmLabel.setHorizontalAlignment(SwingConstants.CENTER);
        //asmLabel.setBorder(new BevelBorder(BevelBorder.RAISED));
        panB.add(asmLabel, BorderLayout.NORTH);
        panB.add(panCode, BorderLayout.CENTER);

        JPanel buttonsPan = new JPanel();
        buttonsPan.setLayout(new FlowLayout());
        buttonsPan.add(editButton);
        buttonsPan.add(saveButton);
        buttonsPan.add(cancelButton);

        editButton.setMargin(new Insets(3, 3, 3, 10));
        saveButton.setMargin(new Insets(3, 3, 3, 10));
        cancelButton.setMargin(new Insets(3, 3, 3, 10));

        JPanel decButtonsPan = new JPanel(new FlowLayout());
        decButtonsPan.add(editDecompiledButton);
        decButtonsPan.add(saveDecompiledButton);
        decButtonsPan.add(cancelDecompiledButton);

        editDecompiledButton.setMargin(new Insets(3, 3, 3, 10));
        saveDecompiledButton.setMargin(new Insets(3, 3, 3, 10));
        cancelDecompiledButton.setMargin(new Insets(3, 3, 3, 10));

        //buttonsPan.add(saveHexButton);
        //buttonsPan.add(loadHexButton);
        panB.add(buttonsPan, BorderLayout.SOUTH);

        saveButton.addActionListener(this::saveActionButtonActionPerformed);
        editButton.addActionListener(this::editActionButtonActionPerformed);
        cancelButton.addActionListener(this::cancelActionButtonActionPerformed);
        saveButton.setVisible(false);
        cancelButton.setVisible(false);

        saveDecompiledButton.addActionListener(this::saveDecompiledButtonActionPerformed);
        editDecompiledButton.addActionListener(this::editDecompiledButtonActionPerformed);
        cancelDecompiledButton.addActionListener(this::cancelDecompiledButtonActionPerformed);
        saveDecompiledButton.setVisible(false);
        cancelDecompiledButton.setVisible(false);

        JPanel panA = new JPanel(new BorderLayout());

        panA.add(decLabel, BorderLayout.NORTH);

        DebugPanel debugPanel = new DebugPanel();

        JPanel panelWithHint = new JPanel(new BorderLayout());
        brokenHintPanel = new JPanel(new BorderLayout(10, 10));
        brokenHintPanel.add(new JLabel("<html>" + AppStrings.translate("script.seemsBroken") + "</html>"), BorderLayout.CENTER);
        brokenHintPanel.setBackground(new Color(253, 205, 137));
        brokenHintPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED), new EmptyBorder(5, 5, 5, 5)));

        panelWithHint.add(brokenHintPanel, BorderLayout.NORTH);
        panelWithHint.add(new FasterScrollPane(decompiledEditor), BorderLayout.CENTER);

        brokenHintPanel.setVisible(false);

        panA.add(new JPersistentSplitPane(JSplitPane.VERTICAL_SPLIT, panelWithHint, debugPanel, Configuration.guiActionVarsSplitPaneDividerLocationPercent), BorderLayout.CENTER);
        panA.add(decButtonsPan, BorderLayout.SOUTH);

        //decPanel.add(searchPanel, BorderLayout.NORTH);
        Main.getDebugHandler().addConnectionListener(new DebuggerHandler.ConnectionListener() {
            @Override
            public void connected() {
                decButtonsPan.setVisible(false);
            }

            @Override
            public void disconnected() {
                decButtonsPan.setVisible(true);
            }
        });

        //new JSplitPane(JSplitPane.VERTICAL_SPLIT, decompiledEditor, debugPanel)
        //decPanel.add(decButtonsPan, BorderLayout.SOUTH);
        //JPanel panBot = new JPanel(new BorderLayout());
        //panBot.add(decButtonsPan, BorderLayout.NORTH);
        //panBot.add(debugPanel, BorderLayout.CENTER);
        //panA.add(decButtonsPan, BorderLayout.SOUTH);
        debugPanel.setVisible(false);

        decLabel.setHorizontalAlignment(SwingConstants.CENTER);
        //decLabel.setBorder(new BevelBorder(BevelBorder.RAISED));

        setLayout(new BorderLayout());
        add(splitPane = new JPersistentSplitPane(JSplitPane.HORIZONTAL_SPLIT, panA, panB, Configuration.guiActionSplitPaneDividerLocationPercent), BorderLayout.CENTER);

        editor.setFont(Configuration.getSourceFont());
        decompiledEditor.setFont(Configuration.getSourceFont());
        decompiledEditor.changeContentType("text/actionscript");

        editor.addCaretListener(new CaretListener() {
            @Override
            public void caretUpdate(CaretEvent e) {
                if (ignoreCarret) {
                    return;
                }
                if (editMode || editDecompiledMode) {
                    return;
                }
                editor.getCaret().setVisible(true);
                int pos = editor.getCaretPosition();
                Highlighting lastH = null;
                for (Highlighting h : disassembledText.getInstructionHighlights()) {
                    if (pos < h.startPos) {
                        break;
                    }
                    lastH = h;
                }
                Long ofs = lastH == null ? 0 : lastH.getProperties().offset;
                Highlighting h2 = Highlighting.searchOffset(lastDecompiled.getInstructionHighlights(), ofs);
                if (h2 != null) {
                    ignoreCarret = true;
                    if (h2.startPos <= decompiledEditor.getDocument().getLength()) {
                        decompiledEditor.setCaretPosition(h2.startPos);
                    }
                    decompiledEditor.getCaret().setVisible(true);
                    ignoreCarret = false;

                }
            }
        });

        decompiledEditor.addCaretListener(new CaretListener() {
            @Override
            public void caretUpdate(CaretEvent e) {
                if (ignoreCarret) {
                    return;
                }
                if (editMode || editDecompiledMode) {
                    return;
                }
                decompiledEditor.getCaret().setVisible(true);
                int pos = decompiledEditor.getCaretPosition();
                Highlighting h = Highlighting.searchPos(lastDecompiled.getInstructionHighlights(), pos);
                if (h != null) {
                    Highlighting h2 = Highlighting.searchOffset(disassembledText.getInstructionHighlights(), h.getProperties().offset);
                    if (h2 != null) {
                        ignoreCarret = true;
                        if (h2.startPos > 0 && h2.startPos < editor.getText().length()) {
                            editor.setCaretPosition(h2.startPos);
                        }
                        editor.getCaret().setVisible(true);
                        ignoreCarret = false;
                    }
                }
            }
        });

        editor.addTextChangedListener(this::editorTextChanged);
        decompiledEditor.addTextChangedListener(this::decompiledEditorTextChanged);
    }

    private void editorTextChanged() {
        setModified(true);
    }

    private void decompiledEditorTextChanged() {
        setDecompiledModified(true);
    }

    private boolean isModified() {
        View.checkAccess();

        return saveButton.isVisible() && saveButton.isEnabled();
    }

    private void setModified(boolean value) {
        View.checkAccess();

        saveButton.setEnabled(value);
        cancelButton.setEnabled(value);
    }

    private boolean isDecompiledModified() {
        View.checkAccess();

        return saveDecompiledButton.isVisible() && saveDecompiledButton.isEnabled();
    }

    private void setDecompiledModified(boolean value) {
        View.checkAccess();

        saveDecompiledButton.setEnabled(value);
        cancelDecompiledButton.setEnabled(value);
    }

    public void setEditMode(boolean val) {
        View.checkAccess();

        if (val) {
            if (hexOnlyButton.isSelected()) {
                setHex(ScriptExportMode.HEX, src.getScriptName(), lastCode);
            } else if (constantsViewButton.isSelected()) {
                setHex(ScriptExportMode.CONSTANTS, src.getScriptName(), lastCode);
            } else {
                setHex(ScriptExportMode.PCODE, src.getScriptName(), lastCode);
            }
        }

        editor.setEditable(val);
        saveButton.setVisible(val);
        saveButton.setEnabled(false);
        editButton.setVisible(!val);
        cancelButton.setVisible(val);

        editor.getCaret().setVisible(true);
        asmLabel.setIcon(val ? View.getIcon("editing16") : null); // this line is not working
        topButtonsPan.setVisible(!val);
        editMode = val;
        if (val) {
            editor.requestFocusInWindow();
        }
    }

    public void setDecompiledEditMode(boolean val) {
        View.checkAccess();

        if (src != null) {
            if (val) {
                setDecompiledText(src.getScriptName(), lastDecompiled.text);
            } else {
                setDecompiledText(src.getScriptName(), lastDecompiled.text);
            }
        }

        decompiledEditor.setEditable(val);
        saveDecompiledButton.setVisible(val);
        saveDecompiledButton.setEnabled(false);
        editDecompiledButton.setVisible(!val);
        cancelDecompiledButton.setVisible(val);

        decompiledEditor.getCaret().setVisible(true);
        decLabel.setIcon(val ? View.getIcon("editing16") : null);
        editDecompiledMode = val;
        if (val) {
            decompiledEditor.requestFocusInWindow();
        }
    }

    private void graphButtonActionPerformed(ActionEvent evt) {
        if (lastCode != null) {
            try {
                boolean insideDoInitAction = (this.src instanceof DoInitActionTag);
                GraphDialog gf = new GraphDialog(mainPanel.getMainFrame().getWindow(), new ActionGraph(this.src.getScriptName(), insideDoInitAction, false, lastCode, new HashMap<>(), new HashMap<>(), new HashMap<>(), SWF.DEFAULT_VERSION), "");
                gf.setVisible(true);
            } catch (InterruptedException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        }
    }

    private void editActionButtonActionPerformed(ActionEvent evt) {
        setEditMode(true);
    }

    private void hexButtonActionPerformed(ActionEvent evt) {
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void hexOnlyButtonActionPerformed(ActionEvent evt) {
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void constantsViewButtonActionPerformed(ActionEvent evt) {
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void resolveConstantsButtonActionPerformed(ActionEvent evt) {
        boolean resolve = resolveConstantsButton.isSelected();
        Configuration.resolveConstants.set(resolve);

        srcWithHex = null;
        srcNoHex = null;
        // srcHexOnly = null; is not needed since it does not contains the resolved constant names
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void showFileOffsetInPcodeHexButtonActionPerformed(ActionEvent evt) {
        boolean resolve = showFileOffsetInPcodeHexButton.isSelected();
        Configuration.showFileOffsetInPcodeHex.set(resolve);

        srcWithHex = null;
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void showOriginalBytesInPcodeHexButtonActionPerformed(ActionEvent evt) {
        boolean resolve = showOriginalBytesInPcodeHexButton.isSelected();
        Configuration.showOriginalBytesInPcodeHex.set(resolve);

        srcWithHex = null;
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void cancelActionButtonActionPerformed(ActionEvent evt) {
        setEditMode(false);
        setHex(getExportMode(), src.getScriptName(), lastCode);
    }

    private void saveActionButtonActionPerformed(ActionEvent evt) {
        try {
            String text = editor.getText();
            String trimmed = text.trim();
            if (trimmed.startsWith(Helper.hexData)) {
                src.setActionBytes(Helper.getBytesFromHexaText(text));
            } else if (trimmed.startsWith(Helper.constants)) {
                List<List<String>> constantPools = Helper.getConstantPoolsFromText(text);
                try {
                    Action.setConstantPools(src, constantPools, true);
                } catch (ConstantPoolTooBigException ex) {
                    ViewMessages.showMessageDialog(this, AppStrings.translate("error.constantPoolTooBig").replace("%index%", Integer.toString(ex.index)).replace("%size%", Integer.toString(ex.size)), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
                }
            } else {
                src.setActions(ASMParser.parse(0, true, text, src.getSwf().version, false));
            }

            SWF.uncache(src);
            src.setModified();
            setSource(this.src, false);
            ViewMessages.showMessageDialog(this, AppStrings.translate("message.action.saved"), AppStrings.translate("dialog.message.title"), JOptionPane.INFORMATION_MESSAGE, Configuration.showCodeSavedMessage);
            saveButton.setVisible(false);
            cancelButton.setVisible(false);
            editButton.setVisible(true);
            editor.setEditable(false);
            editMode = false;
            mainPanel.refreshTree(src.getSwf());
        } catch (IOException ex) {
        } catch (ActionParseException ex) {
            editor.gotoLine((int) ex.line);
            editor.markError();
            ViewMessages.showMessageDialog(this, AppStrings.translate("error.action.save").replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
        } catch (Throwable ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

    private void editDecompiledButtonActionPerformed(ActionEvent evt) {
        setDecompiledEditMode(true);
    }

    private void cancelDecompiledButtonActionPerformed(ActionEvent evt) {
        setDecompiledEditMode(false);
    }

    private void saveDecompiledButtonActionPerformed(ActionEvent evt) {
        try {
            ActionScript2Parser par = new ActionScript2Parser(mainPanel.getCurrentSwf(), src);
            src.setActions(par.actionsFromString(decompiledEditor.getText()));
            SWF.uncache(src);
            src.setModified();
            setSource(src, false);

            ViewMessages.showMessageDialog(this, AppStrings.translate("message.action.saved"), AppStrings.translate("dialog.message.title"), JOptionPane.INFORMATION_MESSAGE, Configuration.showCodeSavedMessage);
            setDecompiledEditMode(false);
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "IOException during action compiling", ex);
        } catch (ActionParseException ex) {
            decompiledEditor.gotoLine((int) ex.line);
            decompiledEditor.markError();
            ViewMessages.showMessageDialog(this, AppStrings.translate("error.action.save").replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
        } catch (CompilationException ex) {
            decompiledEditor.gotoLine((int) ex.line);
            decompiledEditor.markError();
            ViewMessages.showMessageDialog(this, AppStrings.translate("error.action.save").replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
        } catch (Throwable ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

    private ScriptExportMode getExportMode() {
        ScriptExportMode exportMode = hexOnlyButton.isSelected() ? ScriptExportMode.HEX
                : hexButton.isSelected() ? ScriptExportMode.PCODE_HEX
                        : constantsViewButton.isSelected() ? ScriptExportMode.CONSTANTS
                                : ScriptExportMode.PCODE;
        return exportMode;
    }

    @Override
    public void updateSearchPos(String searchedText, boolean ignoreCase, boolean regExp, ScriptSearchResult item) {
        View.checkAccess();

        if (!(item instanceof ActionSearchResult)) {
            return;
        }
        ActionSearchResult result = (ActionSearchResult) item;

        searchPanel.setOptions(ignoreCase, regExp);
        searchPanel.setSearchText(searchedText);
        Runnable onScriptComplete = new Runnable() {
            @Override
            public void run() {
                decompiledEditor.setCaretPosition(0);

                if (result.isPcode()) {
                    searchPanel.showQuickFindDialog(editor);
                } else {
                    searchPanel.showQuickFindDialog(decompiledEditor);
                }
                removeScriptListener(this);
            }
        };

        addScriptListener(onScriptComplete);

        TagTreeModel ttm = (TagTreeModel) mainPanel.tagTree.getModel();
        TreePath tp = ttm.getTreePath(result.getSrc());
        mainPanel.tagTree.setSelectionPath(tp);
        mainPanel.tagTree.scrollPathToVisible(tp);

    }

    @Override
    public boolean tryAutoSave() {
        View.checkAccess();

        // todo: implement
        return false;
    }

    @Override
    public boolean isEditing() {
        View.checkAccess();

        return (saveButton.isVisible() && saveButton.isEnabled())
                || (saveDecompiledButton.isVisible() && saveDecompiledButton.isEnabled());
    }
}
