/*
 * DBeaver - Universal Database Manager
 * Copyright (C) 2010-2023 DBeaver Corp and others
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jkiss.dbeaver.ui.editors.binary;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.themes.ITheme;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ui.UIFonts;
import org.jkiss.dbeaver.ui.UIUtils;
import org.jkiss.dbeaver.ui.editors.binary.pref.HexPreferencesPage;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.CommonUtils;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.List;


/**
 * A binary file editor, composed of two synchronized displays: an hexadecimal and a basic ascii char
 * display. The file size has no effect on the memory footprint of the editor. It has binary, ascii
 * and unicode find functionality. Use addListener(SWT.Modify, Listener) to listen to changes of the
 * 'dirty', 'overwrite/insert', 'selection' and 'canUndo/canRedo' status.
 *
 * @author Jordi
 */
public class HexEditControl extends Composite {

    private static final Log log = Log.getLog(HexEditControl.class);

    public static final String CONTROL_ID = "org.jkiss.dbeaver.ui.hexEditor";
    public static final String DEFAULT_FONT_NAME = "Courier New"; //$NON-NLS-1$"
    public static final FontData DEFAULT_FONT_DATA = new FontData(DEFAULT_FONT_NAME, 10, SWT.NORMAL);

    static final Color COLOR_BLUE = Display.getCurrent().getSystemColor(SWT.COLOR_BLUE);
    static final Color COLOR_LIGHT_SHADOW = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW);
    static final Color COLOR_NORMAL_SHADOW = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);

    /**
     * Map of displayed chars. Chars that cannot be displayed correctly are changed for a '.' char.
     * There are differences on which chars can correctly be displayed in each operating system,
     * charset encoding, or font system.
     */
    static String headerRow;
    static final byte[] hexToNibble = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1,
        10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15};
    static final int maxScreenResolution = 1920;
    static final int minCharSize = 5;
    static final int SET_TEXT = 0;
    static final int SHIFT_FORWARD = 1;  // frame
    static final int SHIFT_BACKWARD = 2;

    static {
        // Compose header row
        StringBuilder rowChars = new StringBuilder();
        for (int i = 0; i < maxScreenResolution / minCharSize / 3; ++i)
            rowChars.append(GeneralUtils.byteToHex[i & 0x0ff]).append(' ');
        headerRow = rowChars.toString().toUpperCase();
    }

    public final char[] byteToChar = new char[256];

    private boolean readOnly;
    private int charsForFileSizeAddress = 0;
    private String charset = null;
    private boolean delayedInQueue = false;
    private Runnable delayedWaiting = null;
    private boolean dragging = false;
    private int fontCharWidth = -1;
    private List<Integer> highlightRangesInScreen;
    private List<Long> mergeChangeRanges = null;
    private List<Integer> mergeHighlightRanges = null;
    private int mergeIndexChange = -2;
    private int mergeIndexHighlight = -2;
    private boolean mergeRangesIsBlue = false;
    private boolean mergeRangesIsHighlight = false;
    private int mergeRangesPosition = -1;
    private final int charsForAddress;  // Files up to 16 Ters: 11 binary digits + ':'
    private int bytesPerLine;
    private boolean caretStickToStart = false;  // stick to end
    private BinaryClipboard myClipboard;
    private BinaryContent content = null;
    private long endPosition = 0L;
    private BinaryTextFinder finder = null;
    private boolean isInserting = true;
    private KeyListener keyAdapter = new ControlKeyAdapter();
    private int lastFocusedTextArea;  // 1 or 2;
    private long lastLocationPosition = -1L;
    private List<SelectionListener> longSelectionListeners;
    private long previousFindEnd = -1;
    private boolean previousFindIgnoredCase = false;
    private String previousFindString = null;
    private boolean previousFindStringWasHex = false;
    private int previousLine;
    private long previousRedrawStart = -1;
    private long startPosition = 0L;
    private long textAreasStart = -1L;
    private int upANibble = 0;  // always 0 or 1
    private int numberOfLines = 16;
    private int numberOfLines_1 = numberOfLines - 1;
    private boolean stopSearching = false;
    private byte[] tmpRawBuffer = new byte[maxScreenResolution / minCharSize / 3 * maxScreenResolution / minCharSize];
    private int verticalBarFactor = 0;

    // visual components
    private Font fontCurrent = null;  // disposed externally
    private Font fontDefault = null;  // disposed internally
    private GridData textGridData = null;
    private GridData previewGridData = null;
    private Text linesTextSeparator = null;
    private StyledText linesText = null;

    private StyledText hexHeaderText = null;
    private StyledText hexText = null;

    private Text previewTextSeparator = null;
    private StyledText previewText = null;

    private int defWidth = Integer.valueOf(HexPreferencesPage.getDefaultWidth());

    private Color colorText;
    private Color colorCaretLine = null;
    private Color colorHighlightText = null;

    public HexEditControl(final Composite parent, int style)
    {
        this(parent, style, 12, 16);
    }

    /**
     * Create a binary text editor
     *
     * @param parent parent in the widget hierarchy
     * @param style  not used for the moment
     */
    public HexEditControl(final Composite parent, int style, int charsForAddress, int bytesPerLine)
    {
        super(parent, style | SWT.V_SCROLL);

        loadSettings();

        this.readOnly = (style & SWT.READ_ONLY) != 0;
        this.charsForAddress = charsForAddress;
        this.bytesPerLine = bytesPerLine;

        this.highlightRangesInScreen = new ArrayList<>();

        this.myClipboard = new BinaryClipboard(parent.getDisplay());
        this.longSelectionListeners = new ArrayList<>();
        addDisposeListener(e -> {
            try {
                myClipboard.dispose();
            } catch (IOException ex) {
                log.warn("Can't cleanup clipboard temporary data");
            }
        });
        initialize();
        this.lastFocusedTextArea = 1;
        this.previousLine = -1;

        UIUtils.addFocusTracker(UIUtils.getActiveWorkbenchWindow(), CONTROL_ID, this);
    }

    private void loadSettings() {
        ITheme currentTheme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme();
        this.colorCaretLine = currentTheme.getColorRegistry().get("org.jkiss.dbeaver.hex.editor.color.caret");
        this.colorText = currentTheme.getColorRegistry().get("org.jkiss.dbeaver.hex.editor.color.text");
        this.colorHighlightText = UIUtils.getSharedColor(UIUtils.blend(this.colorText.getRGB(), this.colorCaretLine.getRGB(), 50));
        this.fontDefault = currentTheme.getFontRegistry().get(UIFonts.DBEAVER_FONTS_MONOSPACE);
    }

    /**
     * Get long selection start and end points. Helper method for long selection listeners.
     * The start point is formed by event.width as the most significant int and event.x as the least
     * significant int. The end point is similarly formed by event.height and event.y
     */
    public static long[] getLongSelection(SelectionEvent event)
    {
        return new long[]{((long) event.width) << 32 | (event.x & 0x0ffffffffL),
            ((long) event.height) << 32 | (event.y & 0x0ffffffffL)};
    }

    /**
     * Converts a hex String to byte[]. Will convert full bytes only, odd number of hex characters will
     * have a leading '0' added. Big endian.
     *
     * @param hexString an hex string (ie. "0fdA1").
     * @return the byte[] value of the hex string
     */
    public static byte[] hexStringToByte(String hexString)
    {
        if ((hexString.length() & 1) == 1)  // nibbles promote to a full byte
            hexString = '0' + hexString;
        byte[] tmp = new byte[hexString.length() / 2];
        for (int i = 0; i < tmp.length; ++i) {
            String hexByte = hexString.substring(i * 2, i * 2 + 2);
            tmp[i] = (byte) Integer.parseInt(hexByte, 16);
        }

        return tmp;
    }

/*
    private class EditableStyledText extends StyledText {

        EditableStyledText(Composite parent, int style) {
            super(parent, style);
        }

        @Override
        public void copy() {
            HexEditControl.this.copy();
        }

        @Override
        public void paste() {
            HexEditControl.this.paste();
        }
    }
*/

    private class ControlKeyAdapter extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e)
        {
            switch (e.keyCode) {
                case SWT.ARROW_UP:
                case SWT.ARROW_DOWN:
                case SWT.ARROW_LEFT:
                case SWT.ARROW_RIGHT:
                case SWT.END:
                case SWT.HOME:
                case SWT.PAGE_UP:
                case SWT.PAGE_DOWN:
                    //boolean selection = startPosition != endPosition;
                    boolean ctrlKey = (e.stateMask & SWT.CONTROL) != 0;
                    if ((e.stateMask & SWT.SHIFT) != 0) {  // shift mod2
                        long newPos = doNavigateKeyPressed(ctrlKey, e.keyCode, getCaretPos(), false);
                        shiftStartAndEnd(newPos);
                    } else {  // if no modifier or control or alt
                        endPosition = startPosition = doNavigateKeyPressed(
                            ctrlKey,
                            e.keyCode,
                            getCaretPos(),
                            e.widget == hexText && !isInserting);
                        caretStickToStart = false;
                    }
                    ensureCaretIsVisible();
                    Runnable delayed = () -> {
                        redrawTextAreas(false);
                        runnableEnd();
                    };
                    runnableAdd(delayed);
                    notifyLongSelectionListeners();
//                    if (selection != (startPosition != endPosition))
//                        notifyListeners(SWT.Modify, null);
                    e.doit = false;
                    break;
                case SWT.INSERT:
                    if ((e.stateMask & SWT.MODIFIER_MASK) == 0) {
                        redrawCaret(true);
                    } else if (!readOnly && e.stateMask == SWT.SHIFT) {
                        paste();
                    } else if (e.stateMask == SWT.CONTROL) {
                        copy();
                    }
                    break;
                case 'a':
                    if (e.stateMask == SWT.CONTROL)  // control mod1
                        selectAll();
                    break;
                case 'c':
                    if (e.stateMask == SWT.CONTROL)  // control mod1
                        copy();
                    break;
                case 'v':
                    if (!readOnly && e.stateMask == SWT.CONTROL)  // control mod1
                        paste();
                    break;
                case 'x':
                    if (!readOnly && e.stateMask == SWT.CONTROL)  // control mod1
                        cut();
                    break;
                case 'y':
                    if (!readOnly && e.stateMask == SWT.CONTROL)  // control mod1
                        redo();
                    break;
                case 'z':
                    if (!readOnly && e.stateMask == SWT.CONTROL)  // control mod1
                        undo();
                    break;
                default:
                    break;
            }
        }
    }

    private class ControlMouseAdapter extends MouseAdapter {
        int charLen;

        public ControlMouseAdapter(boolean hexContent)
        {
            charLen = 1;
            if (hexContent) charLen = 3;
        }

        @Override
        public void mouseDown(MouseEvent e)
        {
            if (e.button == 1)
                dragging = true;
            int textOffset;
            try {
                textOffset = ((StyledText) e.widget).getOffsetAtLocation(new Point(e.x, e.y));
            } catch (IllegalArgumentException ex) {
                textOffset = ((StyledText) e.widget).getCharCount();
            }
            int byteOffset = textOffset / charLen;
            ((StyledText) e.widget).setTopIndex(0);
            if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK & ~SWT.SHIFT) == 0) {// no modif or shift
                if ((e.stateMask & SWT.MODIFIER_MASK) == 0) {
                    caretStickToStart = false;
                    startPosition = endPosition = textAreasStart + byteOffset;
                } else {  // shift
                    shiftStartAndEnd(textAreasStart + byteOffset);
                }
                refreshCaretsPosition();
                setFocus();
                refreshSelections();
                //notifyListeners(SWT.Modify, null);
                notifyLongSelectionListeners();
            }
        }

        @Override
        public void mouseUp(MouseEvent e)
        {
            if (e.button == 1)
                dragging = false;
        }
    }

    private class ControlPaintAdapter implements PaintListener {
        boolean hexContent;

        ControlPaintAdapter(boolean isHexText)
        {
            hexContent = isHexText;
        }

        @Override
        public void paintControl(PaintEvent event)
        {
            event.gc.setForeground(COLOR_LIGHT_SHADOW);
            int lineWidth = 1;
            int charLen = 1;
            int rightHalfWidth = 0;  // is 1, but better to tread on leftmost char pixel than rightmost one
            if (hexContent) {
                lineWidth = fontCharWidth;
                charLen = 3;
                rightHalfWidth = (lineWidth + 1) / 2;  // line spans to both sides of its position
            }
            event.gc.setLineWidth(lineWidth);
            for (int block = defWidth; block <= bytesPerLine; block += defWidth) {
                int xPos = (charLen * block) * fontCharWidth - rightHalfWidth;
                event.gc.drawLine(xPos, event.y, xPos, event.y + event.height);
            }
        }
    }

    private class ControlSelectionAdapter extends SelectionAdapter implements SelectionListener {
        int charLen;

        public ControlSelectionAdapter(boolean hexContent)
        {
            charLen = 1;
            if (hexContent) charLen = 3;
        }

        @Override
        public void widgetSelected(SelectionEvent e)
        {
            if (!dragging)
                return;

            //boolean selection = startPosition != endPosition;
            int lower = e.x / charLen;
            int higher = e.y / charLen;
            int caretPos = ((StyledText) e.widget).getCaretOffset() / charLen;
            caretStickToStart = caretPos < higher || caretPos < lower;
            if (lower > higher) {
                lower = higher;
                higher = e.x / charLen;
            }

            select(textAreasStart + lower, textAreasStart + higher);
//            if (selection != (startPosition != endPosition))
//                notifyListeners(SWT.Modify, null);

            redrawTextAreas(false);
        }
    }


    private class ControlTraverseAdapter implements TraverseListener {
        @Override
        public void keyTraversed(TraverseEvent e)
        {
            if (e.detail == SWT.TRAVERSE_TAB_NEXT)
                e.doit = true;
        }
    }

    private class ControlVerifyKeyAdapter implements VerifyKeyListener {
        @Override
        public void verifyKey(VerifyEvent e)
        {
            if (readOnly) {
                return;
            }
            if ((e.character == SWT.DEL || e.character == SWT.BS) && isInserting) {
                if (!deleteSelected()) {
                    if (e.character == SWT.BS) {
                        startPosition += upANibble;
                        if (startPosition > 0L) {
                            content.delete(startPosition - 1L, 1L);
                            endPosition = --startPosition;
                        }
                    } else {  // e.character == SWT.DEL
                        content.delete(startPosition, 1L);
                    }
                    ensureWholeScreenIsVisible();
                    ensureCaretIsVisible();
                    Runnable delayed = () -> {
                        redrawTextAreas(true);
                        runnableEnd();
                    };
                    runnableAdd(delayed);
                    updateScrollBar();

                    notifyListeners(SWT.Modify, null);
                    notifyLongSelectionListeners();
                }
                upANibble = 0;
            } else {
                doModifyKeyPressed(e);
            }

            e.doit = false;
        }
    }

    public BinaryTextFinder getFinder()
    {
        return finder;
    }

    /**
     * compose byte-to-char map
     */
    private void composeByteToCharMap()
    {
        if (charset == null || previewText == null) return;

        CharsetDecoder decoder = Charset.forName(charset).newDecoder().
            onMalformedInput(CodingErrorAction.REPLACE).
            onUnmappableCharacter(CodingErrorAction.REPLACE).
            replaceWith(".");
        ByteBuffer bb = ByteBuffer.allocate(1);
        CharBuffer cb = CharBuffer.allocate(1);
        for (int i = 0; i < 256; ++i) {
            if (i < 0x20 || i == 0x7f) {
                byteToChar[i] = (char) (160 + i);
            } else {
                bb.clear();
                bb.put((byte) i);
                bb.rewind();
                cb.clear();
                decoder.reset();
                decoder.decode(bb, cb, true);
                decoder.flush(cb);
                cb.rewind();
                char decoded = cb.get();
                // neither font metrics nor graphic context work for charset 8859-1 chars between 128 and
                // 159
                // It works too slow. Dumn with it.
                byteToChar[i] = decoded;
            }
        }
    }

    public void setCharset(String name)
    {
        if (CommonUtils.isEmpty(name)) {
            name = GeneralUtils.getDefaultFileEncoding();
        }
        charset = name;
        composeByteToCharMap();
    }

    public StyledText getHexText()
    {
        return hexText;
    }

    public StyledText getPreviewText()
    {
        return previewText;
    }

    /**
     * redraw the caret with respect of Inserting/Overwriting mode
     */
    public void redrawCaret(boolean focus)
    {
        drawUnfocusedCaret(false);
        setCaretsSize(focus ? (!isInserting) : isInserting);
        if (isInserting && upANibble != 0) {
            upANibble = 0;
            refreshCaretsPosition();
            if (focus) setFocus();
        } else {
            drawUnfocusedCaret(true);
        }
//        if (focus) notifyListeners(SWT.Modify, null);
    }

    /**
     * Adds a long selection listener. Events sent to the listener have long start and end points.
     * The start point is formed by event.width as the most significant int and event.x as the least
     * significant int. The end point is similarly formed by event.height and event.y
     * A listener can obtain the long selection with this code: getLongSelection(SelectionEvent)
     * long start = ((long)event.width) << 32 | (event.x & 0x0ffffffffL)
     * Similarly for the end point:
     * long end = ((long)event.height) << 32 | (event.y & 0x0ffffffffL)
     *
     * @param listener the listener
     * @see StyledText#addSelectionListener(org.eclipse.swt.events.SelectionListener)
     */
    public void addLongSelectionListener(SelectionListener listener)
    {
        if (listener == null)
            throw new IllegalArgumentException();

        if (!longSelectionListeners.contains(listener))
            longSelectionListeners.add(listener);
    }

    /**
     * This method initializes composite
     */
    private void initialize() {
        GridLayout gridLayout1 = new GridLayout();
        gridLayout1.numColumns = 3;
        gridLayout1.marginHeight = 0;
        gridLayout1.verticalSpacing = 0;
        gridLayout1.horizontalSpacing = 0;
        gridLayout1.marginWidth = 0;
        setLayout(gridLayout1);

        FocusListener myFocusAdapter = new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                drawUnfocusedCaret(false);
                lastFocusedTextArea = 1;
                if (e.widget == previewText)
                    lastFocusedTextArea = 2;
                UIUtils.asyncExec(() -> drawUnfocusedCaret(true));
            }
        };
        Caret defaultCaret;
        Caret nonDefaultCaret;

        {
            // Lines
            Composite linesColumn = new Composite(this, SWT.NONE);
            GridLayout columnLayout = new GridLayout();
            columnLayout.marginHeight = 0;
            columnLayout.verticalSpacing = 1;
            columnLayout.horizontalSpacing = 0;
            columnLayout.marginWidth = 0;
            linesColumn.setLayout(columnLayout);
            //linesColumn.setBackground(COLOR_LIGHT_SHADOW);
            GridData gridDataColumn = new GridData(SWT.BEGINNING, SWT.FILL, false, true);
            linesColumn.setLayoutData(gridDataColumn);

            GridData gridDataTextSeparator = new GridData(SWT.FILL, SWT.BEGINNING, true, false);
            gridDataTextSeparator.widthHint = 10;
            linesTextSeparator = new Text(linesColumn, SWT.SEPARATOR);
            linesTextSeparator.setEnabled(false);
            linesTextSeparator.setBackground(COLOR_LIGHT_SHADOW);
            linesTextSeparator.setLayoutData(gridDataTextSeparator);

            linesText = new StyledText(linesColumn, SWT.MULTI | SWT.READ_ONLY);
            linesText.setEditable(false);
            linesText.setEnabled(false);
            //linesText.setBackground(COLOR_LIGHT_SHADOW);
            //linesText.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
//            fontDefault = new Font(Display.getCurrent(), DEFAULT_FONT_DATA);
            fontCurrent = fontDefault;
            linesText.setFont(fontCurrent);
            GC styledTextGC = new GC(linesText);
            fontCharWidth = styledTextGC.getFontMetrics().getAverageCharWidth();
            styledTextGC.dispose();
            GridData gridDataAddresses = new GridData(SWT.BEGINNING, SWT.FILL, false, true);
            gridDataAddresses.heightHint = numberOfLines * linesText.getLineHeight();
            linesText.setLayoutData(gridDataAddresses);
            setAddressesGridDataWidthHint();
            linesText.setContent(new DisplayedContent(charsForAddress, numberOfLines));
        }

        {
            // Hex
            Composite hexColumn = new Composite(this, SWT.NONE);
            GridLayout column1Layout = new GridLayout();
            column1Layout.marginHeight = 0;
            column1Layout.verticalSpacing = 1;
            column1Layout.horizontalSpacing = 0;
            column1Layout.marginWidth = 0;
            hexColumn.setLayout(column1Layout);
            //hexColumn.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
            GridData gridDataColumn1 = new GridData(SWT.BEGINNING, SWT.FILL, false, true);
            hexColumn.setLayoutData(gridDataColumn1);

            Composite hexHeaderGroup = new Composite(hexColumn, SWT.NONE);
            //hexHeaderGroup.setBackground(COLOR_LIGHT_SHADOW);
            GridLayout column1HeaderLayout = new GridLayout();
            column1HeaderLayout.marginHeight = 0;
            column1HeaderLayout.marginWidth = 0;
            hexHeaderGroup.setLayout(column1HeaderLayout);
            GridData gridDataColumn1Header = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false);
            hexHeaderGroup.setLayoutData(gridDataColumn1Header);

            GridData gridData = new GridData();
            gridData.horizontalIndent = 1;
            hexHeaderText = new StyledText(hexHeaderGroup, SWT.SINGLE | SWT.READ_ONLY);
            hexHeaderText.setEditable(false);
            hexHeaderText.setEnabled(false);
            hexHeaderText.setBackground(COLOR_LIGHT_SHADOW);
            //hexHeaderText.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
            hexHeaderText.setLayoutData(gridData);
            hexHeaderText.setFont(fontCurrent);
            refreshHeader();

            hexText = new StyledText(hexColumn, SWT.MULTI);
            hexText.setFont(fontCurrent);
            if (readOnly) {
                hexText.setEditable(false);
            }
            int width = bytesPerLine * 3 * fontCharWidth;
            textGridData = new GridData();
            textGridData.horizontalIndent = 1;
            textGridData.verticalAlignment = SWT.FILL;
            textGridData.widthHint = hexText.computeTrim(0, 0, width, 0).width;
            textGridData.grabExcessVerticalSpace = true;
            hexText.setLayoutData(textGridData);
            hexText.addKeyListener(keyAdapter);
            hexText.addFocusListener(myFocusAdapter);
            hexText.addMouseListener(new ControlMouseAdapter(true));
            hexText.addPaintListener(new ControlPaintAdapter(true));
            hexText.addTraverseListener(new ControlTraverseAdapter());
            hexText.addVerifyKeyListener(new ControlVerifyKeyAdapter());
            hexText.setContent(new DisplayedContent(bytesPerLine * 3, numberOfLines));
            hexText.setDoubleClickEnabled(false);
            hexText.addSelectionListener(new ControlSelectionAdapter(true));
            // StyledText.setCaretOffset() version 3.448 bug resets the caret size if using the default one,
            // so we use not the default one.
            defaultCaret = hexText.getCaret();
            nonDefaultCaret = new Caret(defaultCaret.getParent(), defaultCaret.getStyle());
            nonDefaultCaret.setBounds(defaultCaret.getBounds());
            hexText.setCaret(nonDefaultCaret);
            UIUtils.addFocusTracker(UIUtils.getActiveWorkbenchWindow(), CONTROL_ID, hexText);
        }

        {
            // Preview
            Composite previewColumn = new Composite(this, SWT.NONE);
            GridLayout column2Layout = new GridLayout();
            column2Layout.marginHeight = 0;
            column2Layout.verticalSpacing = 1;
            column2Layout.horizontalSpacing = 0;
            column2Layout.marginWidth = 0;
            previewColumn.setLayout(column2Layout);
            //previewColumn.setBackground(hexText.getBackground());
            GridData gridDataColumn2 = new GridData(SWT.FILL, SWT.FILL, true, true);
            previewColumn.setLayoutData(gridDataColumn2);

            GridData gridDataTextSeparator2 = new GridData();
            gridDataTextSeparator2.horizontalAlignment = SWT.FILL;
            gridDataTextSeparator2.verticalAlignment = SWT.FILL;
            gridDataTextSeparator2.grabExcessHorizontalSpace = true;
            previewTextSeparator = new Text(previewColumn, SWT.SEPARATOR);
            previewTextSeparator.setEnabled(false);
            previewTextSeparator.setBackground(COLOR_LIGHT_SHADOW);
            previewTextSeparator.setLayoutData(gridDataTextSeparator2);
            makeFirstRowSameHeight();

            previewText = new StyledText(previewColumn, SWT.MULTI);
            previewText.setFont(fontCurrent);
            if (readOnly) {
                previewText.setEditable(false);
            }
            int width = bytesPerLine * fontCharWidth + 1;  // one pixel for caret in last linesColumn
            previewGridData = new GridData();
            previewGridData.verticalAlignment = SWT.FILL;
            previewGridData.widthHint = previewText.computeTrim(0, 0, width, 0).width;
            previewGridData.grabExcessVerticalSpace = true;
            previewText.setLayoutData(previewGridData);
            previewText.addKeyListener(keyAdapter);
            previewText.addFocusListener(myFocusAdapter);
            previewText.addMouseListener(new ControlMouseAdapter(false));
            previewText.addPaintListener(new ControlPaintAdapter(false));
            previewText.addTraverseListener(new ControlTraverseAdapter());
            previewText.addVerifyKeyListener(new ControlVerifyKeyAdapter());
            previewText.setContent(new DisplayedContent(bytesPerLine, numberOfLines));
            previewText.setDoubleClickEnabled(false);
            previewText.addSelectionListener(new ControlSelectionAdapter(false));
            // StyledText.setCaretOffset() version 3.448 bug resets the caret size if using the default one,
            // so we use not the default one.
            defaultCaret = previewText.getCaret();
            nonDefaultCaret = new Caret(defaultCaret.getParent(), defaultCaret.getStyle());
            nonDefaultCaret.setBounds(defaultCaret.getBounds());
            previewText.setCaret(nonDefaultCaret);
            setCharset(null);

            UIUtils.addFocusTracker(UIUtils.getActiveWorkbenchWindow(), CONTROL_ID, previewText);
        }

        super.setFont(fontCurrent);
        ScrollBar vertical = getVerticalBar();
        vertical.setSelection(0);
        vertical.setMinimum(0);
        vertical.setIncrement(1);
        vertical.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e)
            {
                e.doit = false;
                long previousStart = textAreasStart;
                textAreasStart =
                    (((long) getVerticalBar().getSelection()) << verticalBarFactor) * (long) bytesPerLine;
                if (previousStart == textAreasStart) return;

                Runnable delayed = () -> {
                    redrawTextAreas(false);
                    setFocus();
                    runnableEnd();
                };
                runnableAdd(delayed);
            }
        });
        updateScrollBar();
        addMouseListener(new org.eclipse.swt.events.MouseAdapter() {
            @Override
            public void mouseDown(org.eclipse.swt.events.MouseEvent e)
            {
                setFocus();
            }
        });
        addControlListener(new org.eclipse.swt.events.ControlAdapter() {
            @Override
            public void controlResized(org.eclipse.swt.events.ControlEvent e)
            {
                updateTextsMetrics();
            }
        });
        addDisposeListener(e -> {
            if (content != null)
                content.dispose();
        });
    }


    /**
     * Tells whether the last action can be redone
     *
     * @return true: an action ca be redone
     */
    public boolean canRedo()
    {
        return content != null && content.canRedo();
    }


    /**
     * Tells whether the last action can be undone
     *
     * @return true: an action ca be undone
     */
    public boolean canUndo()
    {
        return content != null && content.canUndo();
    }


    /**
     * Copies the selection into the clipboard. If nothing is selected leaves the clipboard with its
     * current contents. The clipboard will hold text data (for pasting into a text editor) and binary
     * data (internal for HexText). Text data is limited to 4Mbytes, binary data is limited by disk space.
     */
    public void copy()
    {
        if (startPosition >= endPosition) return;

        myClipboard.setContents(content, startPosition, endPosition - startPosition);
    }


    StringBuilder cookAddresses(long address, int limit)
    {
        StringBuilder theText = new StringBuilder();
        for (int i = 0; i < limit; i += bytesPerLine, address += bytesPerLine) {
            boolean indenting = true;
            for (int j = (charsForAddress - 2) * 4; j > 0; j -= 4) {
                int nibble = ((int) (address >>> j)) & 0x0f;
                if (nibble != 0)
                    indenting = false;
                if (indenting) {
                    if (j >= (charsForFileSizeAddress * 4))
                        theText.append(' ');
                    else
                        theText.append('0');
                } else {
                    theText.append(GeneralUtils.nibbleToHex[nibble]);
                }
            }
            theText.append(GeneralUtils.nibbleToHex[((int) address) & 0x0f]).append(':');
        }

        return theText;
    }


    StringBuilder cookTexts(boolean isHexOutput, int length)
    {
        if (length > tmpRawBuffer.length) length = tmpRawBuffer.length;
        StringBuilder result;

        if (isHexOutput) {
            result = new StringBuilder(length * 3);
            for (int i = 0; i < length; ++i) {
                result.append(GeneralUtils.byteToHex[tmpRawBuffer[i] & 0x0ff]).append(' ');
            }
        } else {
            result = new StringBuilder(length);
            for (int i = 0; i < length; ++i) {
                result.append(byteToChar[tmpRawBuffer[i] & 0x0ff]);
            }
        }

        return result;
    }


    /**
     * Calls copy();deleteSelected();
     */
    public void cut()
    {
        copy();
        deleteSelected();
    }


    /**
     * While in insert mode, deletes the selection
     *
     * @return did delete something
     */
    public boolean deleteSelected()
    {
        if (!handleSelectedPreModify()) {
            return false;
        }
        upANibble = 0;
        ensureWholeScreenIsVisible();
        restoreStateAfterModify();

        return true;
    }


    void doModifyKeyPressed(KeyEvent event)
    {
        char aChar = event.character;
        if (aChar == '\0' || aChar == '\b' || aChar == '\u007f' || event.stateMask == SWT.CTRL ||
            event.widget == hexText && ((event.stateMask & SWT.MODIFIER_MASK) != 0 ||
                aChar < '0' || aChar > '9' && aChar < 'A' || aChar > 'F' && aChar < 'a' || aChar > 'f')) {
            return;
        }
        boolean origInserting = isInserting;
        if (getCaretPos() == content.length() && !isInserting) {
//            ensureCaretIsVisible();
//            redrawTextAreas(false);
//            return;
            isInserting = true;
        }
        try {
            handleSelectedPreModify();
            try {
                if (isInserting) {
                    if (event.widget == previewText) {
                        content.insert((byte) aChar, getCaretPos());
                    } else if (upANibble == 0) {
                        content.insert((byte) (hexToNibble[aChar - '0'] << 4), getCaretPos());
                    } else {
                        content.overwrite(hexToNibble[aChar - '0'], 4, 4, getCaretPos());
                    }
                } else {
                    if (event.widget == previewText) {
                        content.overwrite((byte) aChar, getCaretPos());
                    } else {
                        content.overwrite(hexToNibble[aChar - '0'], upANibble * 4, 4, getCaretPos());
                    }
                    content.get(ByteBuffer.wrap(tmpRawBuffer, 0, 1), null, getCaretPos());
                    int offset = (int) (getCaretPos() - textAreasStart);
                    hexText.replaceTextRange(offset * 3, 2, GeneralUtils.byteToHex[tmpRawBuffer[0] & 0x0ff]);
                    hexText.setStyleRange(new StyleRange(offset * 3, 2, colorHighlightText, null));
                    previewText.replaceTextRange(
                        offset,
                        1,
                        Character.toString(byteToChar[tmpRawBuffer[0] & 0x0ff]));
                    previewText.setStyleRange(new StyleRange(offset, 1, colorHighlightText, null));
                }
            } catch (IOException e) {
                log.warn(e);
            }
            startPosition = endPosition = incrementPosWithinLimits(getCaretPos(), event.widget == hexText);
            Runnable delayed = () -> {
                ensureCaretIsVisible();
                redrawTextAreas(false);
                if (isInserting) {
                    updateScrollBar();
                    redrawTextAreas(true);
                }
                refreshSelections();
                runnableEnd();
            };
            runnableAdd(delayed);
            notifyListeners(SWT.Modify, null);
            notifyLongSelectionListeners();
        } finally {
            isInserting = origInserting;
        }
    }


    private long doNavigateKeyPressed(boolean ctrlKey, int keyCode, long oldPos, boolean countNibbles)
    {
        if (!countNibbles)
            upANibble = 0;
        switch (keyCode) {
            case SWT.ARROW_UP:
                if (oldPos >= bytesPerLine) oldPos -= bytesPerLine;
                break;

            case SWT.ARROW_DOWN:
                if (oldPos <= content.length() - bytesPerLine) oldPos += bytesPerLine;
                if (countNibbles && oldPos == content.length()) upANibble = 0;
                break;

            case SWT.ARROW_LEFT:
                if (countNibbles && (oldPos > 0 || oldPos == 0 && upANibble > 0)) {
                    if (upANibble == 0) --oldPos;
                    upANibble ^= 1;  // 1->0, 0->1
                }
                if (!countNibbles && oldPos > 0)
                    --oldPos;
                break;

            case SWT.ARROW_RIGHT:
                oldPos = incrementPosWithinLimits(oldPos, countNibbles);
                break;

            case SWT.END:
                if (ctrlKey) {
                    oldPos = content.length();
                } else {
                    oldPos = oldPos - oldPos % bytesPerLine + bytesPerLine - 1L;
                    if (oldPos >= content.length()) oldPos = content.length();
                }
                upANibble = 0;
                if (countNibbles && oldPos < content.length()) upANibble = 1;
                break;

            case SWT.HOME:
                if (ctrlKey) {
                    oldPos = 0;
                } else {
                    oldPos = oldPos - oldPos % bytesPerLine;
                }
                upANibble = 0;
                break;

            case SWT.PAGE_UP:
                if (oldPos >= bytesPerLine) {
                    oldPos = oldPos - bytesPerLine * numberOfLines_1;
                    if (oldPos < 0L)
                        oldPos = (oldPos + bytesPerLine * numberOfLines_1) % bytesPerLine;
                }
                break;

            case SWT.PAGE_DOWN:
                if (oldPos <= content.length() - bytesPerLine) {
                    oldPos = oldPos + bytesPerLine * numberOfLines_1;
                    if (oldPos > content.length())
                        oldPos = oldPos -
                            ((oldPos - 1 - content.length()) / bytesPerLine + 1) * bytesPerLine;
                }
                if (countNibbles && oldPos == content.length()) upANibble = 0;
                break;
        }

        return oldPos;
    }


    void drawUnfocusedCaret(boolean visible) {
        if (hexText.isDisposed()) return;

        GC unfocusedGC = null;
        Caret unfocusedCaret;
        int chars = 0;
        int shift = 0;
        try {
            if (lastFocusedTextArea == 1) {
                unfocusedCaret = previewText.getCaret();
                unfocusedGC = new GC(previewText);
            } else {
                unfocusedCaret = hexText.getCaret();
                unfocusedGC = new GC(hexText);
                chars = 1;
                if (hexText.getCaretOffset() % 3 == 1) shift = -1;
            }
            if (unfocusedCaret.getVisible()) {
                Rectangle unfocused = unfocusedCaret.getBounds();
                unfocusedGC.setForeground(visible ? COLOR_NORMAL_SHADOW : colorCaretLine);
                unfocusedGC.drawRectangle(unfocused.x + shift * unfocused.width,
                    unfocused.y,
                    unfocused.width << chars,
                    unfocused.height - 1);
            }
        } finally {
            if (unfocusedGC != null) {
                unfocusedGC.dispose();
            }
        }
    }


    void ensureCaretIsVisible()
    {
        long caretPos = getCaretPos();
        long posInLine = caretPos % bytesPerLine;

        if (textAreasStart > caretPos) {
            textAreasStart = caretPos - posInLine;
        } else if (textAreasStart + bytesPerLine * numberOfLines < caretPos ||
            textAreasStart + bytesPerLine * numberOfLines == caretPos &&
                caretPos != content.length()) {
            textAreasStart = caretPos - posInLine - bytesPerLine * numberOfLines_1;
            if (caretPos == content.length() && posInLine == 0)
                textAreasStart = caretPos - bytesPerLine * numberOfLines;
            if (textAreasStart < 0L) textAreasStart = 0L;
        } else {

            return;
        }
        getVerticalBar().setSelection((int) ((textAreasStart / bytesPerLine) >>> verticalBarFactor));
    }


    private void ensureWholeScreenIsVisible()
    {
        if (textAreasStart + bytesPerLine * numberOfLines > content.length())
            textAreasStart = content.length() - (content.length() - 1L) % bytesPerLine - 1L -
                bytesPerLine * numberOfLines_1;

        if (textAreasStart < 0L)
            textAreasStart = 0L;
    }


    /**
     * Performs a find on the text and sets the selection accordingly.
     * The find starts at the current caret position.
     *
     * @param findString    the literal to find
     * @param isHexString   consider the literal as an hex string (ie. "0fdA1"). Used for binary finds.
     *                      Will search full bytes only, odd number of hex characters will have a leading '0' added.
     * @param searchForward look for matches after current position
     * @param ignoreCase    match upper case with lower case characters
     * @return whether a match was found
     */
    public boolean findAndSelect(String findString, boolean isHexString, boolean searchForward,
                                 boolean ignoreCase)
        throws IOException
    {
        return findAndSelectInternal(findString, isHexString, searchForward, ignoreCase, true);
    }


    private boolean findAndSelectInternal(String findString, boolean isHexString, boolean searchForward,
                                          boolean ignoreCase, boolean updateGui)
        throws IOException
    {
        if (findString == null) return true;

        initFinder(findString, isHexString, searchForward, ignoreCase);
        final Object[] result = new Object[2];
        HexManager.blockUntilFinished(() -> {
            try {
                result[0] = finder.getNextMatch();
            } catch (IOException e) {
                result[1] = e;
            }
        });
        if (result[1] != null) {
            throw (IOException) result[1];
        }
        Object[] vector = (Object[]) result[0];
        if (vector != null && vector.length > 1 && vector[0] != null && vector[1] != null) {
            startPosition = (Long) vector[0];
            caretStickToStart = false;
            if (updateGui) {
                setSelection(startPosition, startPosition + (Integer) vector[1]);
            } else {
                select(startPosition, startPosition + (Integer) vector[1]);
            }
            previousFindEnd = getCaretPos();

            return true;
        }

        return false;
    }


    /**
     * Get caret position in file, which can be out of view
     *
     * @return the current caret position
     */
    public long getCaretPos()
    {
        if (caretStickToStart)
            return startPosition;
        else
            return endPosition;
    }

    public byte getActualValue()
    {
        return getValue(getCaretPos());
    }

    public byte getValue(long pos)
    {
        try {
            content.get(ByteBuffer.wrap(tmpRawBuffer, 0, 1), null, pos);
        } catch (IOException e) {
            log.warn(e);
        }
        return tmpRawBuffer[0];
    }

    /**
     * Get the binary content
     *
     * @return the content being edited
     */
    public BinaryContent getContent()
    {
        return content;
    }


    private void getHighlightRangesInScreen(long start, int length)
    {
        highlightRangesInScreen.clear();
        if (lastLocationPosition >= start && lastLocationPosition < start + length) {
            highlightRangesInScreen.add((int) (lastLocationPosition - textAreasStart));
            highlightRangesInScreen.add(1);
        }
    }


    /**
     * Gets the selection start and end points as long values
     *
     * @return 2 elements long array, first one the start point (inclusive), second one the end point
     *         (exclusive)
     */
    public long[] getSelection()
    {
        return new long[]{startPosition, endPosition};
    }

    public boolean isSelected()
    {
        return (startPosition != endPosition);
    }

    boolean handleSelectedPreModify()
    {
        if (startPosition == endPosition || !isInserting) return false;

        content.delete(startPosition, endPosition - startPosition);
        endPosition = startPosition;

        return true;
    }


    long incrementPosWithinLimits(long oldPos, boolean countNibbles)
    {
        if (oldPos < content.length())
            if (countNibbles) {
                if (upANibble > 0) ++oldPos;
                upANibble ^= 1;  // 1->0, 0->1
            } else {
                ++oldPos;
            }

        return oldPos;
    }


    private void initFinder(String findString, boolean isHexString, boolean searchForward,
                            boolean ignoreCase)
    {
        if (!searchForward)
            caretStickToStart = true;
        if (finder == null || !findString.equals(previousFindString) ||
            isHexString != previousFindStringWasHex || ignoreCase != previousFindIgnoredCase) {
            previousFindString = findString;
            previousFindStringWasHex = isHexString;
            previousFindIgnoredCase = ignoreCase;

            if (isHexString) {
                finder = new BinaryTextFinder(hexStringToByte(findString), content);
            } else {
                finder = new BinaryTextFinder(findString, content);
                if (ignoreCase)
                    finder.setCaseSensitive(false);
            }
            finder.setNewStart(getCaretPos());
        }
        if (previousFindEnd != getCaretPos()) {
            finder.setNewStart(getCaretPos());
        }
        finder.setDirectionForward(searchForward);
    }


    /**
     * Tells whether the input is in overwrite or insert mode
     *
     * @return true: overwriting, false: inserting
     */
    public boolean isOverwriteMode()
    {
        return !isInserting;
    }


    void makeFirstRowSameHeight()
    {
        ((GridData) linesTextSeparator.getLayoutData()).heightHint =
            hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
        ((GridData) previewTextSeparator.getLayoutData()).heightHint =
            hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
    }


    /**
     * Merge ranges of changes in file with ranges of highlighted elements.
     * Finds lowest range border, finds next lowest range border. That's the first result. Keeps going
     * until last range border.
     *
     * @return list of StyleRanges, each with a style of type 'changed', 'highlighted', or both.
     */
    List<StyleRange> mergeRanges(List<Long> changeRanges, List<Integer> highlightRanges)
    {
        if (!mergerInit(changeRanges, highlightRanges)) {
            return null;
        }
        List<StyleRange> result = new ArrayList<>();
        mergerNext();
        int start = mergeRangesPosition;
        boolean blue = mergeRangesIsBlue;
        boolean highlight = mergeRangesIsHighlight;
        while (mergerNext()) {
            if (blue || highlight) {
                result.add(new StyleRange(start, mergeRangesPosition - start, blue ? colorText : null,
                    highlight ? colorHighlightText : null));
            }
            start = mergeRangesPosition;
            blue = mergeRangesIsBlue;
            highlight = mergeRangesIsHighlight;
        }

        return result;
    }


    boolean mergerCatchUps()
    {
        boolean withinRange = false;
        if (mergeChangeRanges != null && mergeChangeRanges.size() > mergeIndexChange) {
            withinRange = true;
            if (mergerPosition(true) < mergeRangesPosition) {
                ++mergeIndexChange;
            }
        }
        if (mergeHighlightRanges != null && mergeHighlightRanges.size() > mergeIndexHighlight) {
            withinRange = true;
            if (mergerPosition(false) < mergeRangesPosition) {
                ++mergeIndexHighlight;
            }
        }

        return withinRange;
    }


    /**
     * Initialise merger variables
     *
     * @return whether the parameters hold any data
     */
    boolean mergerInit(List<Long> changeRanges, List<Integer> highlightRanges)
    {
        if ((changeRanges == null || changeRanges.size() < 2) &&
            (highlightRanges == null || highlightRanges.size() < 2)) {
            return false;
        }
        this.mergeChangeRanges = changeRanges;
        this.mergeHighlightRanges = highlightRanges;
        mergeRangesIsBlue = false;
        mergeRangesIsHighlight = false;
        mergeRangesPosition = -1;
        mergeIndexChange = 0;
        mergeIndexHighlight = 0;

        return true;
    }


    int mergerMinimumInChangesHighlights()
    {
        int change = Integer.MAX_VALUE;
        if (mergeChangeRanges != null && mergeChangeRanges.size() > mergeIndexChange) {
            change = mergerPosition(true);
        }
        int highlight = Integer.MAX_VALUE;
        if (mergeHighlightRanges != null && mergeHighlightRanges.size() > mergeIndexHighlight) {
            highlight = mergerPosition(false);
        }
        int result = Math.min(change, highlight);
        if (change == result) {
            mergeRangesIsBlue = (mergeIndexChange & 1) == 0;
        }
        if (highlight == result) {
            mergeRangesIsHighlight = (mergeIndexHighlight & 1) == 0;
        }

        return result;
    }


    boolean mergerNext()
    {
        ++mergeRangesPosition;
        if (!mergerCatchUps()) {
            return false;
        }
        mergeRangesPosition = mergerMinimumInChangesHighlights();

        return true;
    }


    int mergerPosition(boolean changesNotHighlights)
    {
        int result;
        if (changesNotHighlights) {
            result = (int) (mergeChangeRanges.get(mergeIndexChange & 0xfffffffe) -
                textAreasStart);
            if ((mergeIndexChange & 1) == 1) {
                result = (int) Math.min(bytesPerLine * numberOfLines,
                    result + mergeChangeRanges.get(mergeIndexChange));
            }
        } else {
            result = mergeHighlightRanges.get(mergeIndexHighlight & 0xfffffffe);
            if ((mergeIndexHighlight & 1) == 1) {
                result += mergeHighlightRanges.get(mergeIndexHighlight);
            }
        }

        return result;
    }


    void notifyLongSelectionListeners()
    {
        if (longSelectionListeners.isEmpty()) return;

        Event basicEvent = new Event();
        basicEvent.widget = this;
        SelectionEvent anEvent = new SelectionEvent(basicEvent);
        anEvent.width = (int) (startPosition >>> 32);
        anEvent.x = (int) startPosition;
        anEvent.height = (int) (endPosition >>> 32);
        anEvent.y = (int) endPosition;

        for (SelectionListener aListener : longSelectionListeners) {
            aListener.widgetSelected(anEvent);
        }
    }


    /**
     * Pastes the clipboard content. The result depends on which insertion mode is currently active:
     * Insert mode replaces the selection with the DND.CLIPBOARD clipboard contents or, if there is no
     * selection, inserts at the current caret offset.
     * Overwrite mode replaces contents at the current caret offset, unless pasting would overflow the
     * content length, in which case does nothing.
     */
    public void paste()
    {
        if (!myClipboard.hasContents() || this.isReadOnly()) {
            return;
        }

        handleSelectedPreModify();
        long caretPos = getCaretPos();
        long total = myClipboard.getContents(content, caretPos, isInserting);
        startPosition = caretPos;
        endPosition = caretPos + total;
        caretStickToStart = false;
        redrawTextAreas(true);
        restoreStateAfterModify();
    }


    /**
     * Redoes the last undone action
     */
    public void redo()
    {
        undo(false);
    }


    void redrawTextAreas(int mode, StringBuilder newText, StringBuilder resultHex, StringBuilder resultChar,
                         List<StyleRange> viewRanges)
    {
        hexText.getCaret().setVisible(false);
        previewText.getCaret().setVisible(false);
        if (mode == SET_TEXT) {
            linesText.getContent().setText(newText.toString());
            hexText.getContent().setText(resultHex.toString());
            previewText.getContent().setText(resultChar.toString());
            previousLine = -1;
        } else {
            boolean forward = mode == SHIFT_FORWARD;
            linesText.setRedraw(false);
            hexText.setRedraw(false);
            previewText.setRedraw(false);
            ((DisplayedContent) linesText.getContent()).shiftLines(newText.toString(), forward);
            ((DisplayedContent) hexText.getContent()).shiftLines(resultHex.toString(), forward);
            ((DisplayedContent) previewText.getContent()).shiftLines(resultChar.toString(), forward);
            linesText.setRedraw(true);
            hexText.setRedraw(true);
            previewText.setRedraw(true);
            if (previousLine >= 0 && previousLine < numberOfLines)
                previousLine += newText.length() / charsForAddress * (forward ? 1 : -1);
            if (previousLine < -1 || previousLine >= numberOfLines)
                previousLine = -1;
        }
        if (viewRanges != null) {
            for (StyleRange styleRange : viewRanges) {
                previewText.setStyleRange(styleRange);
                styleRange = (StyleRange) styleRange.clone();
                styleRange.start *= 3;
                styleRange.length *= 3;
                hexText.setStyleRange(styleRange);
            }
        }
    }


    void redrawTextAreas(boolean fromScratch)
    {
        if (content == null || hexText.isDisposed()) return;

        long newLinesStart = textAreasStart;
        int linesShifted = numberOfLines;
        int mode = SET_TEXT;
        if (!fromScratch && previousRedrawStart >= 0L) {
            long lines = (textAreasStart - previousRedrawStart) / bytesPerLine;
            if (Math.abs(lines) < numberOfLines) {
                mode = lines > 0L ? SHIFT_BACKWARD : SHIFT_FORWARD;
                linesShifted = Math.abs((int) lines);
                if (linesShifted < 1) {
                    refreshSelections();
                    refreshCaretsPosition();

                    return;
                }
                if (mode == SHIFT_BACKWARD)
                    newLinesStart = textAreasStart + (numberOfLines - (int) lines) * bytesPerLine;
            }
        }
        previousRedrawStart = textAreasStart;

        StringBuilder newText = cookAddresses(newLinesStart, linesShifted * bytesPerLine);

        List<Long> changeRanges = new ArrayList<>();
        int actuallyRead;
        try {
            actuallyRead = content.get(ByteBuffer.wrap(tmpRawBuffer, 0, Math.min(tmpRawBuffer.length, linesShifted * bytesPerLine)),
                changeRanges, newLinesStart);
        } catch (IOException e) {
            actuallyRead = 0;
        }
        StringBuilder resultHex = cookTexts(true, actuallyRead);
        StringBuilder resultChar = cookTexts(false, actuallyRead);
        getHighlightRangesInScreen(newLinesStart, linesShifted * bytesPerLine);
        List<StyleRange> viewRanges = mergeRanges(changeRanges, highlightRangesInScreen);
        redrawTextAreas(mode, newText, resultHex, resultChar, viewRanges);
        refreshSelections();
        refreshCaretsPosition();
    }


    private void refreshCaretsPosition()
    {
        drawUnfocusedCaret(false);
        long caretLocation = getCaretPos() - textAreasStart;
        if (caretLocation >= 0L && caretLocation < bytesPerLine * numberOfLines ||
            getCaretPos() == content.length() && caretLocation == bytesPerLine * numberOfLines) {
            int tmp = (int) caretLocation;
            if (tmp == bytesPerLine * numberOfLines) {
                hexText.setCaretOffset(tmp * 3 - 1);
                previewText.setCaretOffset(tmp);
            } else {
                hexText.setCaretOffset(tmp * 3 + upANibble);
                previewText.setCaretOffset(tmp);
            }
            int line = hexText.getLineAtOffset(hexText.getCaretOffset());
            if (line != previousLine) {
                if (previousLine >= 0 && previousLine < numberOfLines) {
                    hexText.setLineBackground(previousLine, 1, null);
                    previewText.setLineBackground(previousLine, 1, null);
                }
                hexText.setLineBackground(line, 1, colorCaretLine);
                previewText.setLineBackground(line, 1, colorCaretLine);
                previousLine = line;
            }
            hexText.getCaret().setVisible(true);
            previewText.getCaret().setVisible(true);
            UIUtils.asyncExec(() -> drawUnfocusedCaret(true));
        } else {
            hexText.getCaret().setVisible(false);
            previewText.getCaret().setVisible(false);
        }
    }


    void refreshHeader()
    {
        hexHeaderText.setText(headerRow.substring(0, Math.min(bytesPerLine * 3, headerRow.length())));
    }


    void refreshSelections()
    {
        if (startPosition >= endPosition ||
            startPosition > textAreasStart + bytesPerLine * numberOfLines ||
            endPosition <= textAreasStart)
            return;

        long startLocation = startPosition - textAreasStart;
        if (startLocation < 0L) startLocation = 0L;
        int intStart = (int) startLocation;

        long endLocation = endPosition - textAreasStart;
        if (endLocation > bytesPerLine * numberOfLines)
            endLocation = bytesPerLine * numberOfLines;
        int intEnd = (int) endLocation;

        if (caretStickToStart) {
            int tmp = intStart;
            intStart = intEnd;
            intEnd = tmp;
        }

        hexText.setSelection(intStart * 3, intEnd * 3);
        hexText.setTopIndex(0);
        previewText.setSelection(intStart, intEnd);
        previewText.setTopIndex(0);
    }


    /**
     * Replaces the selection. The result depends on which insertion mode is currently active:
     * Insert mode replaces the selection with the replaceString or, if there is no selection, inserts
     * at the current caret offset.
     * Overwrite mode replaces contents at the current selection start.
     *
     * @param replaceString the new string
     * @param isHexString   consider the literal as an hex string (ie. "0fdA1"). Used for binary finds.
     *                      Will replace full bytes only, odd number of hex characters will have a leading '0' added.
     */
    public void replace(String replaceString, boolean isHexString)
    {
        handleSelectedPreModify();
        byte[] replaceData = replaceString.getBytes(Charset.defaultCharset());
        if (isHexString) {
            replaceData = hexStringToByte(replaceString);
        }
        ByteBuffer newSelection = ByteBuffer.wrap(replaceData);
        if (isInserting) {
            content.insert(newSelection, startPosition);
        } else {
            newSelection.limit((int) Math.min(newSelection.limit(), content.length() - startPosition));
            content.overwrite(newSelection, startPosition);
        }
        endPosition = startPosition + newSelection.limit() - newSelection.position();
        caretStickToStart = false;
        redrawTextAreas(true);
        restoreStateAfterModify();
    }


    /**
     * Replaces all occurrences of findString with replaceString.
     * The find starts at the current caret position.
     *
     * @param findString         the literal to find
     * @param isFindHexString    consider the literal as an hex string (ie. "0fdA1"). Used for binary finds.
     *                           Will search full bytes only, odd number of hex characters will have a leading '0' added.
     * @param searchForward      look for matches after current position
     * @param ignoreCase         match upper case with lower case characters
     * @param replaceString      the new string
     * @param isReplaceHexString consider the literal as an hex string (ie. "0fdA1"). Used for binary
     *                           finds. Will replace full bytes only, odd number of hex characters will have a leading '0' added.
     * @return number of replacements
     */
    public int replaceAll(String findString, boolean isFindHexString, boolean searchForward,
                          boolean ignoreCase, String replaceString, boolean isReplaceHexString)
        throws IOException
    {
        int result = 0;
        stopSearching = false;
        while (!stopSearching &&
            findAndSelectInternal(findString, isFindHexString, searchForward, ignoreCase, false)) {
            ++result;
            replace(replaceString, isReplaceHexString);
        }
        if (result > 0) {
            setSelection(getSelection()[0], getSelection()[1]);
        }

        return result;
    }


    void restoreStateAfterModify()
    {
        ensureCaretIsVisible();
        redrawTextAreas(true);
        updateScrollBar();

        notifyListeners(SWT.Modify, null);
        notifyLongSelectionListeners();
    }


    void runnableAdd(Runnable delayed)
    {
        if (delayedInQueue) {
            delayedWaiting = delayed;
        } else {
            delayedInQueue = true;
            UIUtils.asyncExec(delayed);
        }
    }


    void runnableEnd()
    {
        if (delayedWaiting != null) {
            UIUtils.asyncExec(delayedWaiting);
            delayedWaiting = null;
        } else {
            delayedInQueue = false;
        }
    }


    /**
     * Sets the selection to the entire text. Caret remains either at the selection start or end
     */
    public void selectAll()
    {
        select(0L, content.length());
        refreshSelections();
    }

    /**
     * Sets the selection from start to end.
     */
    public void selectBlock(long start, long end)
    {
        select(start, end);
        refreshSelections();
        showMark(start);
    }

    void select(long start, long end)
    {
        upANibble = 0;
        //boolean selection = startPosition != endPosition;
        startPosition = 0L;
        if (start > 0L) {
            startPosition = start;
            if (startPosition > content.length()) startPosition = content.length();
        }

        endPosition = startPosition;
        if (end > startPosition) {
            endPosition = end;
            if (endPosition > content.length()) endPosition = content.length();
        }

        notifyLongSelectionListeners();
    }

    void setAddressesGridDataWidthHint()
    {
        ((GridData) linesText.getLayoutData()).widthHint = charsForAddress * fontCharWidth;
    }

    void setCaretsSize(boolean insert)
    {
        isInserting = insert;
        int width = 0;
        int height = hexText.getCaret().getSize().y;
        if (!isInserting)
            width = fontCharWidth;

        hexText.getCaret().setSize(width, height);
        previewText.getCaret().setSize(width, height);
    }


    /**
     * Sets the content to be displayed. Replacing an existing content keeps the display area in the
     * same position, but only if it falls within the new content's limits.
     *
     * @param aContent the content to be displayed
     */
    public void setContentProvider(BinaryContent aContent, boolean notify)
    {
        if (isDisposed()) {
            return;
        }
        boolean firstContent = content == null;
        if (!firstContent) {
            content.dispose();
        }
        content = aContent;
        finder = null;
        if (content != null) {
            content.setActionsHistory();

            if (firstContent || endPosition > content.length() || textAreasStart >= content.length()) {
                textAreasStart = startPosition = endPosition = 0L;
                caretStickToStart = false;
            }

            charsForFileSizeAddress = Long.toHexString(content.length()).length();

        }

        updateScrollBar();
        redrawTextAreas(true);
        notifyLongSelectionListeners();
        if (notify) {
            notifyListeners(SWT.Modify, null);
        }
    }

    public void setContent(byte[] data, String charset)
    {
        setContent(data, charset, true);
    }

    public void setContent(byte[] data, String charset, boolean notify)
    {
        BinaryContent binaryContent = new BinaryContent();
        if (charset != null) {
            setCharset(charset);
        }
        ByteBuffer byteBuffer = ByteBuffer.wrap(data);
        binaryContent.insert(byteBuffer, 0);

        setContentProvider(binaryContent, notify);
    }


    /**
     * Causes the receiver to have the keyboard focus. Within Eclipse, never call setFocus() before
     * the workbench has called EditorActionBarContributor.setActiveEditor()
     *
     * @see Composite#setFocus()
     */
    @Override
    public boolean setFocus()
    {
        redrawCaret(false);
        if (lastFocusedTextArea == 1)
            return hexText.setFocus();
        else
            return previewText.setFocus();
    }


    /**
     * @throws IllegalArgumentException if font height is 1 or 2
     * @see Control#setFont(org.eclipse.swt.graphics.Font)
     *      Font height must not be 1 or 2.
     */
    @Override
    public void setFont(Font font)
    {
        // bugfix: HexText's raw array overflows when font is very small and window very big
        // very small sizes would compromise responsiveness in large windows, and they are too small
        // to see anyway
        if (font != null) {
            int newSize = UIUtils.getFontHeight(font);
            if (newSize == 1 || newSize == 2)
                throw new IllegalArgumentException("Font size is " + newSize + ", too small");
        }

        fontCurrent = font;
        if (fontCurrent == null) {
            fontCurrent = fontDefault;
        }
        super.setFont(fontCurrent);
        hexHeaderText.setFont(fontCurrent);
        hexHeaderText.pack(true);
        GC gc = new GC(hexHeaderText);
        fontCharWidth = gc.getFontMetrics().getAverageCharWidth();
        gc.dispose();
        makeFirstRowSameHeight();
        linesText.setFont(fontCurrent);
        setAddressesGridDataWidthHint();
        linesText.pack(true);
        hexText.setFont(fontCurrent);
        hexText.pack(true);
        previewText.setFont(fontCurrent);
        previewText.pack(true);
        updateTextsMetrics();
        layout();
        setCaretsSize(isInserting);
    }


    /**
     * Sets the selection. The caret may change position but stays at the same selection point (if it
     * was at the start of the selection it will move to the new start, otherwise to the new end point).
     * The new selection is made visible
     *
     * @param start inclusive start selection char
     * @param end   exclusive end selection char
     */
    public void setSelection(long start, long end)
    {
        select(start, end);
        ensureCaretIsVisible();
        redrawTextAreas(false);
    }


    void shiftStartAndEnd(long newPos)
    {
        if (caretStickToStart) {
            startPosition = Math.min(newPos, endPosition);
            endPosition = Math.max(newPos, endPosition);
        } else {
            endPosition = Math.max(newPos, startPosition);
            startPosition = Math.min(newPos, startPosition);
        }
        caretStickToStart = endPosition != newPos;
    }


    /**
     * Shows the position on screen.
     *
     * @param position where relocation should go
     */
    public void showMark(long position)
    {
        lastLocationPosition = position;
        if (position < 0) return;

        position = position - position % bytesPerLine;
        textAreasStart = position;
        if (numberOfLines > 2)
            textAreasStart = position - (numberOfLines / 2) * bytesPerLine;
        ensureWholeScreenIsVisible();
        redrawTextAreas(true);
//	setFocus();
        updateScrollBar();
    }


    /**
     * Stop findAndSelect() or replaceAll() calls. Long running searches can be stopped from another
     * thread.
     */
    public void stopSearching()
    {
        stopSearching = true;
        if (finder != null) {
            finder.stopSearching();
        }
    }


    long totalNumberOfLines()
    {
        long result = 1L;
        if (content != null) {
            if (bytesPerLine > 0) {
                result = (content.length() - 1L) / bytesPerLine + 1L;
            }
        }

        return result;
    }


    /**
     * Undoes the last action
     */
    public void undo()
    {
        undo(true);
    }


    void undo(boolean previousAction)
    {
        long[] selection = previousAction ? content.undo() : content.redo();
        if (selection == null) return;

        upANibble = 0;
        startPosition = selection[0];
        endPosition = selection[1];
        caretStickToStart = false;
        ensureWholeScreenIsVisible();
        restoreStateAfterModify();
    }


    void updateNumberOfLines()
    {
        int height = getClientArea().height - hexHeaderText.computeSize(SWT.DEFAULT, SWT.DEFAULT, false).y;

        numberOfLines = height / linesText.getLineHeight();
        if (numberOfLines < 1)
            numberOfLines = 1;

        numberOfLines_1 = numberOfLines - 1;

        ((DisplayedContent) linesText.getContent()).setDimensions(charsForAddress, numberOfLines);
        ((DisplayedContent) hexText.getContent()).setDimensions(bytesPerLine * 3, numberOfLines);
        ((DisplayedContent) previewText.getContent()).setDimensions(bytesPerLine, numberOfLines);
    }


    private void updateScrollBar()
    {
        ScrollBar vertical = getVerticalBar();
        long max = totalNumberOfLines();
        verticalBarFactor = 0;
        while (max > Integer.MAX_VALUE) {
            max >>>= 1;
            ++verticalBarFactor;
        }
        vertical.setMaximum((int) max);
        vertical.setSelection((int) ((textAreasStart / bytesPerLine) >>> verticalBarFactor));
        vertical.setPageIncrement(numberOfLines_1);
        vertical.setThumb(numberOfLines);
    }


    void updateTextsMetrics()
    {
        int width = getClientArea().width - linesText.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
        int displayedNumberWidth = fontCharWidth * 4;  // hexText and previewText
        int commonWidth = width / displayedNumberWidth;
        if (commonWidth <= 0) {
            return;
        }
        bytesPerLine = commonWidth;
        
        textGridData.widthHint = hexText.computeTrim(0, 0, bytesPerLine * 3 * fontCharWidth, 100).width;
        previewGridData.widthHint = previewText.computeTrim(0, 0, bytesPerLine * fontCharWidth, 100).width;
        updateNumberOfLines();
        changed(new Control[]{hexHeaderText, linesText, hexText, previewText});
        updateScrollBar();
        refreshHeader();
        textAreasStart = (((long) getVerticalBar().getSelection()) * bytesPerLine) << verticalBarFactor;
        redrawTextAreas(true);
    }

	public void setDefWidth(int defValue) {
		this.defWidth = defValue ;
	}

    public boolean isReadOnly() {
        return readOnly;
    }

    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }
}
