/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.terminal.swing;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.DefaultKeyDecodingProfile;
import com.googlecode.lanterna.input.InputDecoder;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
import com.googlecode.lanterna.terminal.TerminalResizeListener;
import com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This is the class that does the heavy lifting for both {@link AWTTerminal} and {@link SwingTerminal}. It maintains
 * most of the external terminal state and also the main back buffer that is copied to the components area on draw
 * operations.
 *
 * @author martin
 */
abstract class GraphicalTerminalImplementation implements IOSafeTerminal {
    private final TerminalEmulatorDeviceConfiguration deviceConfiguration;
    private final TerminalEmulatorColorConfiguration colorConfiguration;
    private final DefaultVirtualTerminal virtualTerminal;
    private final BlockingQueue<KeyStroke> keyQueue;
    private final TerminalScrollController scrollController;
    private final DirtyCellsLookupTable dirtyCellsLookupTable;

    private final String enquiryString;

    private boolean cursorIsVisible;
    private boolean enableInput;
    private Timer blinkTimer;
    private boolean hasBlinkingText;
    private boolean blinkOn;
    private boolean bellOn;
    private boolean needFullRedraw;

    private TerminalPosition lastDrawnCursorPosition;
    private int lastBufferUpdateScrollPosition;
    private int lastComponentWidth;
    private int lastComponentHeight;

    // We use two different data structures to optimize drawing
    //  * A list of modified characters since the last draw (stored in VirtualTerminal)
    //  * A backbuffer with the graphics content
    //
    // The buffer is the most important one as it allows us to re-use what was drawn earlier. It is not reset on every
    // drawing operation but updates just in those places where the map tells us the character has changed.
    private BufferedImage backbuffer;

    // Used as a middle-ground when copying large segments when scrolling
    private BufferedImage copybuffer;

    /**
     * Creates a new GraphicalTerminalImplementation component using custom settings and a custom scroll controller. The
     * scrolling controller will be notified when the terminal's history size grows and will be called when this class
     * needs to figure out the current scrolling position.
     * @param initialTerminalSize Initial size of the terminal, which will be used when calculating the preferred size
     *                            of the component. If null, it will default to 80x25. If the AWT layout manager forces
     *                            the component to a different size, the value of this parameter won't have any meaning
     * @param deviceConfiguration Device configuration to use for this SwingTerminal
     * @param colorConfiguration Color configuration to use for this SwingTerminal
     * @param scrollController Controller to use for scrolling, the object passed in will be notified whenever the
     *                         scrollable area has changed
     */
    GraphicalTerminalImplementation(
            TerminalSize initialTerminalSize,
            TerminalEmulatorDeviceConfiguration deviceConfiguration,
            TerminalEmulatorColorConfiguration colorConfiguration,
            TerminalScrollController scrollController) {

        //This is kind of meaningless since we don't know how large the
        //component is at this point, but we should set it to something
        if(initialTerminalSize == null) {
            initialTerminalSize = new TerminalSize(80, 24);
        }
        this.virtualTerminal = new DefaultVirtualTerminal(initialTerminalSize);
        this.keyQueue = new LinkedBlockingQueue<>();
        this.deviceConfiguration = deviceConfiguration;
        this.colorConfiguration = colorConfiguration;
        this.scrollController = scrollController;
        this.dirtyCellsLookupTable = new DirtyCellsLookupTable();

        this.cursorIsVisible = true;        //Always start with an activate and visible cursor
        this.enableInput = false;           //Start with input disabled and activate it once the window is visible
        this.enquiryString = "TerminalEmulator";
        this.lastDrawnCursorPosition = null;
        this.lastBufferUpdateScrollPosition = 0;
        this.lastComponentHeight = 0;
        this.lastComponentWidth = 0;
        this.backbuffer = null;  // We don't know the dimensions yet
        this.copybuffer = null;
        this.blinkTimer = null;
        this.hasBlinkingText = false;   // Assume initial content doesn't have any blinking text
        this.blinkOn = true;
        this.needFullRedraw = false;


        virtualTerminal.setBacklogSize(deviceConfiguration.getLineBufferScrollbackSize());
    }

    TerminalEmulatorDeviceConfiguration getDeviceConfiguration() {
        return deviceConfiguration;
    }

    TerminalEmulatorColorConfiguration getColorConfiguration() {
        return colorConfiguration;
    }

    ///////////
    // First abstract methods that are implemented in AWTTerminalImplementation and SwingTerminalImplementation
    ///////////

    /**
     * Used to find out the font height, in pixels
     * @return Terminal font height in pixels
     */
    abstract int getFontHeight();

    /**
     * Used to find out the font width, in pixels
     * @return Terminal font width in pixels
     */
    abstract int getFontWidth();

    /**
     * Used when requiring the total height of the terminal component, in pixels
     * @return Height of the terminal component, in pixels
     */
    abstract int getHeight();

    /**
     * Used when requiring the total width of the terminal component, in pixels
     * @return Width of the terminal component, in pixels
     */
    abstract int getWidth();

    /**
     * Returning the AWT font to use for the specific character. This might not always be the same, in case a we are
     * trying to draw an unusual character (probably CJK) which isn't contained in the standard terminal font.
     * @param character Character to get the font for
     * @return Font to be used for this character
     */
    abstract Font getFontForCharacter(TextCharacter character);

    /**
     * Returns {@code true} if anti-aliasing is enabled, {@code false} otherwise
     * @return {@code true} if anti-aliasing is enabled, {@code false} otherwise
     */
    abstract boolean isTextAntiAliased();

    /**
     * Called by the {@code GraphicalTerminalImplementation} when it would like the OS to schedule a repaint of the
     * window
     */
    abstract void repaint();

    synchronized void onCreated() {
        startBlinkTimer();
        enableInput = true;

        // Reset the queue, just be to sure
        keyQueue.clear();
    }

    synchronized void onDestroyed() {
        stopBlinkTimer();
        enableInput = false;

        // If a thread is blocked, waiting on something in the keyQueue...
        keyQueue.add(new KeyStroke(KeyType.EOF));
    }

    /**
     * Start the timer that triggers blinking
     */
    synchronized void startBlinkTimer() {
        if(blinkTimer != null) {
            // Already on!
            return;
        }
        blinkTimer = new Timer("LanternaTerminalBlinkTimer", true);
        blinkTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                blinkOn = !blinkOn;
                if(hasBlinkingText) {
                    repaint();
                }
            }
        }, deviceConfiguration.getBlinkLengthInMilliSeconds(), deviceConfiguration.getBlinkLengthInMilliSeconds());
    }

    /**
     * Stops the timer the triggers blinking
     */
    synchronized void stopBlinkTimer() {
        if(blinkTimer == null) {
            // Already off!
            return;
        }
        blinkTimer.cancel();
        blinkTimer = null;
    }

    ///////////
    // Implement all the Swing-related methods
    ///////////
    /**
     * Calculates the preferred size of this terminal
     * @return Preferred size of this terminal
     */
    synchronized Dimension getPreferredSize() {
        return new Dimension(getFontWidth() * virtualTerminal.getTerminalSize().getColumns(),
                getFontHeight() * virtualTerminal.getTerminalSize().getRows());
    }

    /**
     * Updates the back buffer (if necessary) and draws it to the component's surface
     * @param componentGraphics Object to use when drawing to the component's surface
     */
    synchronized void paintComponent(Graphics componentGraphics) {
        int width = getWidth();
        int height = getHeight();

        this.scrollController.updateModel(
                virtualTerminal.getBufferLineCount() * getFontHeight(),
                height);

        boolean needToUpdateBackBuffer =
                // User has used the scrollbar, we need to update the back buffer to reflect this
                lastBufferUpdateScrollPosition != scrollController.getScrollingOffset() ||
                        // There is blinking text to update
                        hasBlinkingText ||
                        // We simply have a hint that we should update everything
                        needFullRedraw;

        // Detect resize
        if(width != lastComponentWidth || height != lastComponentHeight) {
            int columns = width / getFontWidth();
            int rows = height / getFontHeight();
            TerminalSize terminalSize = virtualTerminal.getTerminalSize().withColumns(columns).withRows(rows);
            virtualTerminal.setTerminalSize(terminalSize);

            // Back buffer needs to be updated since the component size has changed
            needToUpdateBackBuffer = true;
        }

        if(needToUpdateBackBuffer) {
            updateBackBuffer(scrollController.getScrollingOffset());
        }

        ensureGraphicBufferHasRightSize();
        Rectangle clipBounds = componentGraphics.getClipBounds();
        if(clipBounds == null) {
            clipBounds = new Rectangle(0, 0, getWidth(), getHeight());
        }
        componentGraphics.drawImage(
                backbuffer,
                // Destination coordinates
                clipBounds.x,
                clipBounds.y,
                clipBounds.width,
                clipBounds.height,
                // Source coordinates
                clipBounds.x,
                clipBounds.y,
                clipBounds.width,
                clipBounds.height,
                null);

        // Take care of the left-over area at the bottom and right of the component where no character can fit
        //int leftoverHeight = getHeight() % getFontHeight();
        int leftoverWidth = getWidth() % getFontWidth();
        componentGraphics.setColor(Color.BLACK);
        if(leftoverWidth > 0) {
            componentGraphics.fillRect(getWidth() - leftoverWidth, 0, leftoverWidth, getHeight());
        }

        //0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
        this.lastComponentWidth = width;
        this.lastComponentHeight = height;
        componentGraphics.dispose();
        notifyAll();
    }

    private synchronized void updateBackBuffer(final int scrollOffsetFromTopInPixels) {
        //long startTime = System.currentTimeMillis();
        final int fontWidth = getFontWidth();
        final int fontHeight = getFontHeight();

        //Retrieve the position of the cursor, relative to the scrolling state
        final TerminalPosition cursorPosition = virtualTerminal.getCursorBufferPosition();
        final TerminalSize viewportSize = virtualTerminal.getTerminalSize();

        final int firstVisibleRowIndex = scrollOffsetFromTopInPixels / fontHeight;
        final int lastVisibleRowIndex = (scrollOffsetFromTopInPixels + getHeight()) / fontHeight;

        //Setup the graphics object
        ensureGraphicBufferHasRightSize();
        final Graphics2D backbufferGraphics = backbuffer.createGraphics();

        if(isTextAntiAliased()) {
            backbufferGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            backbufferGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        }

        final AtomicBoolean foundBlinkingCharacters = new AtomicBoolean(deviceConfiguration.isCursorBlinking());
        buildDirtyCellsLookupTable(firstVisibleRowIndex, lastVisibleRowIndex);

        // Detect scrolling
        if(lastBufferUpdateScrollPosition < scrollOffsetFromTopInPixels) {
            int gap = scrollOffsetFromTopInPixels - lastBufferUpdateScrollPosition;
            if(gap / fontHeight < viewportSize.getRows()) {
                Graphics2D graphics = copybuffer.createGraphics();
                graphics.setClip(0, 0, getWidth(), getHeight() - gap);
                graphics.drawImage(backbuffer, 0, -gap, null);
                graphics.dispose();
                backbufferGraphics.drawImage(copybuffer, 0, 0, getWidth(), getHeight(), 0, 0, getWidth(), getHeight(), null);
                if(!dirtyCellsLookupTable.isAllDirty()) {
                    //Mark bottom rows as dirty so they are repainted
                    int previousLastVisibleRowIndex = (lastBufferUpdateScrollPosition + getHeight()) / fontHeight;
                    for(int row = previousLastVisibleRowIndex; row <= lastVisibleRowIndex; row++) {
                        dirtyCellsLookupTable.setRowDirty(row);
                    }
                }
            }
            else {
                dirtyCellsLookupTable.setAllDirty();
            }
        }
        else if(lastBufferUpdateScrollPosition > scrollOffsetFromTopInPixels) {
            int gap = lastBufferUpdateScrollPosition - scrollOffsetFromTopInPixels;
            if(gap / fontHeight < viewportSize.getRows()) {
                Graphics2D graphics = copybuffer.createGraphics();
                graphics.setClip(0, 0, getWidth(), getHeight() - gap);
                graphics.drawImage(backbuffer, 0, 0, null);
                graphics.dispose();
                backbufferGraphics.drawImage(copybuffer, 0, gap, getWidth(), getHeight(), 0, 0, getWidth(), getHeight() - gap, null);
                if(!dirtyCellsLookupTable.isAllDirty()) {
                    //Mark top rows as dirty so they are repainted
                    int previousFirstVisibleRowIndex = lastBufferUpdateScrollPosition / fontHeight;
                    for(int row = firstVisibleRowIndex; row <= previousFirstVisibleRowIndex; row++) {
                        dirtyCellsLookupTable.setRowDirty(row);
                    }
                }
            }
            else {
                dirtyCellsLookupTable.setAllDirty();
            }
        }

        // Detect component resize
        if(lastComponentWidth < getWidth()) {
            if(!dirtyCellsLookupTable.isAllDirty()) {
                //Mark right columns as dirty so they are repainted
                int lastVisibleColumnIndex = getWidth() / fontWidth;
                int previousLastVisibleColumnIndex = lastComponentWidth / fontWidth;
                for(int column = previousLastVisibleColumnIndex; column <= lastVisibleColumnIndex; column++) {
                    dirtyCellsLookupTable.setColumnDirty(column);
                }
            }
        }
        if(lastComponentHeight < getHeight()) {
            if(!dirtyCellsLookupTable.isAllDirty()) {
                //Mark bottom rows as dirty so they are repainted
                int previousLastVisibleRowIndex = (scrollOffsetFromTopInPixels + lastComponentHeight) / fontHeight;
                for(int row = previousLastVisibleRowIndex; row <= lastVisibleRowIndex; row++) {
                    dirtyCellsLookupTable.setRowDirty(row);
                }
            }
        }

        virtualTerminal.forEachLine(firstVisibleRowIndex, lastVisibleRowIndex, (rowNumber, bufferLine) -> {
            for(int column = 0; column < viewportSize.getColumns(); column++) {
                TextCharacter textCharacter = bufferLine.getCharacterAt(column);
                boolean atCursorLocation = cursorPosition.equals(column, rowNumber);
                //If next position is the cursor location and this is a CJK character (i.e. cursor is on the padding),
                //consider this location the cursor position since otherwise the cursor will be skipped
                if(!atCursorLocation &&
                        cursorPosition.getColumn() == column + 1 &&
                        cursorPosition.getRow() == rowNumber &&
                        TerminalTextUtils.isCharCJK(textCharacter.getCharacter())) {
                    atCursorLocation = true;
                }
                boolean isBlinking = textCharacter.getModifiers().contains(SGR.BLINK);
                if(isBlinking) {
                    foundBlinkingCharacters.set(true);
                }
                if(dirtyCellsLookupTable.isAllDirty() || dirtyCellsLookupTable.isDirty(rowNumber, column) || isBlinking) {
                    int characterWidth = fontWidth * (TerminalTextUtils.isCharCJK(textCharacter.getCharacter()) ? 2 : 1);
                    Color foregroundColor = deriveTrueForegroundColor(textCharacter, atCursorLocation);
                    Color backgroundColor = deriveTrueBackgroundColor(textCharacter, atCursorLocation);
                    //Always draw if the cursor isn't blinking
                    boolean drawCursor = atCursorLocation && (!deviceConfiguration.isCursorBlinking() || blinkOn);    //If the cursor is blinking, only draw when blinkOn is true

                    // Visualize bell as all colors inverted
                    if(bellOn) {
                        Color temp = foregroundColor;
                        foregroundColor = backgroundColor;
                        backgroundColor = temp;
                    }

                    drawCharacter(backbufferGraphics,
                            textCharacter,
                            column,
                            rowNumber,
                            foregroundColor,
                            backgroundColor,
                            fontWidth,
                            fontHeight,
                            characterWidth,
                            scrollOffsetFromTopInPixels,
                            drawCursor);
                }
                if(TerminalTextUtils.isCharCJK(textCharacter.getCharacter())) {
                    column++; //Skip the trailing space after a CJK character
                }
            }
        });

        backbufferGraphics.dispose();

        // Update the blink status according to if there were any blinking characters or not
        this.hasBlinkingText = foundBlinkingCharacters.get();
        this.lastDrawnCursorPosition = cursorPosition;
        this.lastBufferUpdateScrollPosition = scrollOffsetFromTopInPixels;
        this.needFullRedraw = false;

        //System.out.println("Updated backbuffer in " + (System.currentTimeMillis() - startTime) + " ms");
    }

    private void buildDirtyCellsLookupTable(int firstRowOffset, int lastRowOffset) {
        if(virtualTerminal.isWholeBufferDirtyThenReset() || needFullRedraw) {
            dirtyCellsLookupTable.setAllDirty();
            return;
        }

        TerminalSize viewportSize = virtualTerminal.getTerminalSize();
        TerminalPosition cursorPosition = virtualTerminal.getCursorBufferPosition();

        dirtyCellsLookupTable.resetAndInitialize(firstRowOffset, lastRowOffset, viewportSize.getColumns());
        dirtyCellsLookupTable.setDirty(cursorPosition);
        if(lastDrawnCursorPosition != null && !lastDrawnCursorPosition.equals(cursorPosition)) {
            if(virtualTerminal.getCharacter(lastDrawnCursorPosition).isDoubleWidth()) {
                dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition.withRelativeColumn(1));
            }
            if(lastDrawnCursorPosition.getColumn() > 0 && virtualTerminal.getCharacter(lastDrawnCursorPosition.withRelativeColumn(-1)).isDoubleWidth()) {
                dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition.withRelativeColumn(-1));
            }
            dirtyCellsLookupTable.setDirty(lastDrawnCursorPosition);
        }

        TreeSet<TerminalPosition> dirtyCells = virtualTerminal.getAndResetDirtyCells();
        for(TerminalPosition position: dirtyCells) {
            dirtyCellsLookupTable.setDirty(position);
        }
    }

    private void ensureGraphicBufferHasRightSize() {
        if(backbuffer == null) {
            backbuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);
            copybuffer = new BufferedImage(getWidth() * 2, getHeight() * 2, BufferedImage.TYPE_INT_RGB);

            // We only need to set the content of the backbuffer during initialization time
            Graphics2D graphics = backbuffer.createGraphics();
            graphics.setColor(colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false));
            graphics.fillRect(0, 0, getWidth() * 2, getHeight() * 2);
            graphics.dispose();
        }
        if(backbuffer.getWidth() < getWidth() || backbuffer.getWidth() > getWidth() * 4 ||
                backbuffer.getHeight() < getHeight() || backbuffer.getHeight() > getHeight() * 4) {

            BufferedImage newBackbuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
            Graphics2D graphics = newBackbuffer.createGraphics();
            graphics.fillRect(0, 0, newBackbuffer.getWidth(), newBackbuffer.getHeight());
            graphics.drawImage(backbuffer, 0, 0, null);
            graphics.dispose();
            backbuffer = newBackbuffer;

            // Re-initialize the copy buffer, but we don't need to set any content
            copybuffer = new BufferedImage(Math.max(getWidth(), 1) * 2, Math.max(getHeight(), 1) * 2, BufferedImage.TYPE_INT_RGB);
        }
    }

    private void drawCharacter(
            Graphics g,
            TextCharacter character,
            int columnIndex,
            int rowIndex,
            Color foregroundColor,
            Color backgroundColor,
            int fontWidth,
            int fontHeight,
            int characterWidth,
            int scrollingOffsetInPixels,
            boolean drawCursor) {

        int x = columnIndex * fontWidth;
        int y = rowIndex * fontHeight - scrollingOffsetInPixels;
        g.setColor(backgroundColor);
        g.setClip(x, y, characterWidth, fontHeight);
        g.fillRect(x, y, characterWidth, fontHeight);

        g.setColor(foregroundColor);
        Font font = getFontForCharacter(character);
        g.setFont(font);
        FontMetrics fontMetrics = g.getFontMetrics();
        g.drawString(Character.toString(character.getCharacter()), x, y + fontHeight - fontMetrics.getDescent() + 1);

        if(character.isCrossedOut()) {
            //noinspection UnnecessaryLocalVariable
            int lineStartX = x;
            int lineStartY = y + (fontHeight / 2);
            int lineEndX = lineStartX + characterWidth;
            g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
        }
        if(character.isUnderlined()) {
            //noinspection UnnecessaryLocalVariable
            int lineStartX = x;
            int lineStartY = y + fontHeight - fontMetrics.getDescent() + 1;
            int lineEndX = lineStartX + characterWidth;
            g.drawLine(lineStartX, lineStartY, lineEndX, lineStartY);
        }

        if(drawCursor) {
            if(deviceConfiguration.getCursorColor() == null) {
                g.setColor(foregroundColor);
            }
            else {
                g.setColor(colorConfiguration.toAWTColor(deviceConfiguration.getCursorColor(), false, false));
            }
            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.UNDER_BAR) {
                g.fillRect(x, y + fontHeight - 3, characterWidth, 2);
            }
            else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.VERTICAL_BAR) {
                g.fillRect(x, y + 1, 2, fontHeight - 2);
            }
        }
    }


    private Color deriveTrueForegroundColor(TextCharacter character, boolean atCursorLocation) {
        TextColor foregroundColor = character.getForegroundColor();
        TextColor backgroundColor = character.getBackgroundColor();
        boolean reverse = character.isReversed();
        boolean blink = character.isBlinking();

        if(cursorIsVisible && atCursorLocation) {
            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
                    (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
                reverse = true;
            }
        }

        if(reverse && (!blink || !blinkOn)) {
            return colorConfiguration.toAWTColor(backgroundColor, backgroundColor != TextColor.ANSI.DEFAULT, character.isBold());
        }
        else if(!reverse && blink && blinkOn) {
            return colorConfiguration.toAWTColor(backgroundColor, false, character.isBold());
        }
        else {
            return colorConfiguration.toAWTColor(foregroundColor, true, character.isBold());
        }
    }

    private Color deriveTrueBackgroundColor(TextCharacter character, boolean atCursorLocation) {
        TextColor foregroundColor = character.getForegroundColor();
        TextColor backgroundColor = character.getBackgroundColor();
        boolean reverse = character.isReversed();

        if(cursorIsVisible && atCursorLocation) {
            if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.REVERSED &&
                    (!deviceConfiguration.isCursorBlinking() || !blinkOn)) {
                reverse = true;
            }
            else if(deviceConfiguration.getCursorStyle() == TerminalEmulatorDeviceConfiguration.CursorStyle.FIXED_BACKGROUND) {
                backgroundColor = deviceConfiguration.getCursorColor();
            }
        }

        if(reverse) {
            return colorConfiguration.toAWTColor(foregroundColor, backgroundColor == TextColor.ANSI.DEFAULT, character.isBold());
        }
        else {
            return colorConfiguration.toAWTColor(backgroundColor, false, false);
        }
    }

    void addInput(KeyStroke keyStroke) {
        keyQueue.add(keyStroke);
    }

    ///////////
    // Then delegate all Terminal interface methods to the virtual terminal implementation
    //
    // Some of these methods we need to pass to the AWT-thread, which makes the call asynchronous. Hopefully this isn't
    // causing too much problem...
    ///////////
    @Override
    public KeyStroke pollInput() {
        if(!enableInput) {
            return new KeyStroke(KeyType.EOF);
        }
        return keyQueue.poll();
    }

    @Override
    public KeyStroke readInput() {
        // Synchronize on keyQueue here so only one thread is inside keyQueue.take()
        synchronized(keyQueue) {
            if(!enableInput) {
                return new KeyStroke(KeyType.EOF);
            }
            try {
                return keyQueue.take();
            }
            catch(InterruptedException ignore) {
                throw new RuntimeException("Blocking input was interrupted");
            }
        }
    }

    @Override
    public synchronized void enterPrivateMode() {
        virtualTerminal.enterPrivateMode();
        clearBackBuffer();
        flush();
    }

    @Override
    public synchronized void exitPrivateMode() {
        virtualTerminal.exitPrivateMode();
        clearBackBuffer();
        flush();
    }

    @Override
    public synchronized void clearScreen() {
        virtualTerminal.clearScreen();
        clearBackBuffer();
    }

    /**
     * Clears out the back buffer and the resets the visual state so next paint operation will do a full repaint of
     * everything
     */
    private void clearBackBuffer() {
        // Manually clear the backbuffer
        if(backbuffer != null) {
            Graphics2D graphics = backbuffer.createGraphics();
            Color backgroundColor = colorConfiguration.toAWTColor(TextColor.ANSI.DEFAULT, false, false);
            graphics.setColor(backgroundColor);
            graphics.fillRect(0, 0, getWidth(), getHeight());
            graphics.dispose();
        }
    }

    @Override
    public synchronized void setCursorPosition(int x, int y) {
        setCursorPosition(new TerminalPosition(x, y));
    }

    @Override
    public synchronized void setCursorPosition(TerminalPosition position) {
        if(position.getColumn() < 0) {
            position = position.withColumn(0);
        }
        if(position.getRow() < 0) {
            position = position.withRow(0);
        }
        virtualTerminal.setCursorPosition(position);
    }

    @Override
    public TerminalPosition getCursorPosition() {
        return virtualTerminal.getCursorPosition();
    }

    @Override
    public void setCursorVisible(final boolean visible) {
        cursorIsVisible = visible;
    }

    @Override
    public synchronized void putCharacter(final char c) {
        virtualTerminal.putCharacter(c);
    }

    @Override
    public TextGraphics newTextGraphics() {
        return virtualTerminal.newTextGraphics();
    }

    @Override
    public void enableSGR(final SGR sgr) {
        virtualTerminal.enableSGR(sgr);
    }

    @Override
    public void disableSGR(final SGR sgr) {
        virtualTerminal.disableSGR(sgr);
    }

    @Override
    public void resetColorAndSGR() {
        virtualTerminal.resetColorAndSGR();
    }

    @Override
    public void setForegroundColor(final TextColor color) {
        virtualTerminal.setForegroundColor(color);
    }

    @Override
    public void setBackgroundColor(final TextColor color) {
        virtualTerminal.setBackgroundColor(color);
    }

    @Override
    public synchronized TerminalSize getTerminalSize() {
        return virtualTerminal.getTerminalSize();
    }

    @Override
    public byte[] enquireTerminal(int timeout, TimeUnit timeoutUnit) {
        return enquiryString.getBytes();
    }

    @Override
    public void bell() {
        if(bellOn) {
            return;
        }

        // Flash the screen...
        bellOn = true;
        needFullRedraw = true;
        updateBackBuffer(scrollController.getScrollingOffset());
        repaint();
        // Unify this with the blink timer and just do the whole timer logic ourselves?
        new Thread("BellSilencer") {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                }
                catch(InterruptedException ignore) {}
                bellOn = false;
                needFullRedraw = true;
                updateBackBuffer(scrollController.getScrollingOffset());
                repaint();
            }
        }.start();

        // ...and make a sound
        Toolkit.getDefaultToolkit().beep();
    }

    @Override
    public synchronized void flush() {
        updateBackBuffer(scrollController.getScrollingOffset());
        repaint();
    }

    @Override
    public void close() {
        // No action
    }

    @Override
    public void addResizeListener(TerminalResizeListener listener) {
        virtualTerminal.addResizeListener(listener);
    }

    @Override
    public void removeResizeListener(TerminalResizeListener listener) {
        virtualTerminal.removeResizeListener(listener);
    }

    ///////////
    // Remaining are private internal classes used by SwingTerminal
    ///////////
    private static final Set<Character> TYPED_KEYS_TO_IGNORE = new HashSet<>(Arrays.asList('\n', '\t', '\r', '\b', '\33', (char) 127));

    /**
     * Class that translates AWT key events into Lanterna {@link KeyStroke}
     */
    protected class TerminalInputListener extends KeyAdapter {
        @Override
        public void keyTyped(KeyEvent e) {
            char character = e.getKeyChar();
            boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
            boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
            boolean shiftDown = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0;

            if(!TYPED_KEYS_TO_IGNORE.contains(character)) {
                //We need to re-adjust alphabet characters if ctrl was pressed, just like for the AnsiTerminal
                if(ctrlDown && character > 0 && character < 0x1a) {
                    character = (char) ('a' - 1 + character);
                    if(shiftDown) {
                        character = Character.toUpperCase(character);
                    }
                }

                // Check if clipboard is avavilable and this was a paste (ctrl + shift + v) before
                // adding the key to the input queue
                if(!altDown && ctrlDown && shiftDown && character == 'V' && deviceConfiguration.isClipboardAvailable()) {
                    pasteClipboardContent();
                }
                else {
                    keyQueue.add(new KeyStroke(character, ctrlDown, altDown, shiftDown));
                }
            }
        }

        @Override
        public void keyPressed(KeyEvent e) {
            boolean altDown = (e.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0;
            boolean ctrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
            boolean shiftDown = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0;
            if(e.getKeyCode() == KeyEvent.VK_ENTER) {
                keyQueue.add(new KeyStroke(KeyType.Enter, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                keyQueue.add(new KeyStroke(KeyType.Escape, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
                keyQueue.add(new KeyStroke(KeyType.Backspace, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
                keyQueue.add(new KeyStroke(KeyType.ArrowLeft, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
                keyQueue.add(new KeyStroke(KeyType.ArrowRight, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_UP) {
                keyQueue.add(new KeyStroke(KeyType.ArrowUp, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
                keyQueue.add(new KeyStroke(KeyType.ArrowDown, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_INSERT) {
                // This could be a paste (shift+insert) if the clipboard is available
                if(!altDown && !ctrlDown && shiftDown && deviceConfiguration.isClipboardAvailable()) {
                    pasteClipboardContent();
                }
                else {
                    keyQueue.add(new KeyStroke(KeyType.Insert, ctrlDown, altDown, shiftDown));
                }
            }
            else if(e.getKeyCode() == KeyEvent.VK_DELETE) {
                keyQueue.add(new KeyStroke(KeyType.Delete, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_HOME) {
                keyQueue.add(new KeyStroke(KeyType.Home, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_END) {
                keyQueue.add(new KeyStroke(KeyType.End, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
                keyQueue.add(new KeyStroke(KeyType.PageUp, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
                keyQueue.add(new KeyStroke(KeyType.PageDown, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F1) {
                keyQueue.add(new KeyStroke(KeyType.F1, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F2) {
                keyQueue.add(new KeyStroke(KeyType.F2, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F3) {
                keyQueue.add(new KeyStroke(KeyType.F3, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F4) {
                keyQueue.add(new KeyStroke(KeyType.F4, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F5) {
                keyQueue.add(new KeyStroke(KeyType.F5, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F6) {
                keyQueue.add(new KeyStroke(KeyType.F6, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F7) {
                keyQueue.add(new KeyStroke(KeyType.F7, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F8) {
                keyQueue.add(new KeyStroke(KeyType.F8, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F9) {
                keyQueue.add(new KeyStroke(KeyType.F9, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F10) {
                keyQueue.add(new KeyStroke(KeyType.F10, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F11) {
                keyQueue.add(new KeyStroke(KeyType.F11, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_F12) {
                keyQueue.add(new KeyStroke(KeyType.F12, ctrlDown, altDown, shiftDown));
            }
            else if(e.getKeyCode() == KeyEvent.VK_TAB) {
                if(e.isShiftDown()) {
                    keyQueue.add(new KeyStroke(KeyType.ReverseTab, ctrlDown, altDown, false));
                }
                else {
                    keyQueue.add(new KeyStroke(KeyType.Tab, ctrlDown, altDown, shiftDown));
                }
            }
            else {
                //keyTyped doesn't catch this scenario (for whatever reason...) so we have to do it here
                if(altDown && ctrlDown && e.getKeyCode() >= 'A' && e.getKeyCode() <= 'Z') {
                    char character = (char) e.getKeyCode();
                    if(!shiftDown) {
                        character = Character.toLowerCase(character);
                    }
                    keyQueue.add(new KeyStroke(character, true, true, shiftDown));
                }
            }
        }
    }

    // This is mostly unimplemented, we could hook more of this into ExtendedTerminal's mouse functions
    protected class TerminalMouseListener extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            if(MouseInfo.getNumberOfButtons() > 2 &&
                    e.getButton() == MouseEvent.BUTTON2 &&
                    deviceConfiguration.isClipboardAvailable()) {
                pasteSelectionContent();
            }
        }
    }

    private void pasteClipboardContent() {
        try {
            Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            if(systemClipboard != null) {
                injectStringAsKeyStrokes((String) systemClipboard.getData(DataFlavor.stringFlavor));
            }
        }
        catch(Exception ignore) {
        }
    }

    private void pasteSelectionContent() {
        try {
            Clipboard systemSelection = Toolkit.getDefaultToolkit().getSystemSelection();
            if(systemSelection != null) {
                injectStringAsKeyStrokes((String) systemSelection.getData(DataFlavor.stringFlavor));
            }
        }
        catch(Exception ignore) {
        }
    }

    private void injectStringAsKeyStrokes(String string) {
        StringReader stringReader = new StringReader(string);
        InputDecoder inputDecoder = new InputDecoder(stringReader);
        inputDecoder.addProfile(new DefaultKeyDecodingProfile());
        try {
            KeyStroke keyStroke = inputDecoder.getNextCharacter(false);
            while (keyStroke != null && keyStroke.getKeyType() != KeyType.EOF) {
                keyQueue.add(keyStroke);
                keyStroke = inputDecoder.getNextCharacter(false);
            }
        }
        catch(IOException ignore) {
        }
    }

    private static class DirtyCellsLookupTable {
        private final List<BitSet> table;
        private int firstRowIndex;
        private boolean allDirty;

        DirtyCellsLookupTable() {
            table = new ArrayList<>();
            firstRowIndex = -1;
            allDirty = false;
        }

        void resetAndInitialize(int firstRowIndex, int lastRowIndex, int columns) {
            this.firstRowIndex = firstRowIndex;
            this.allDirty = false;
            int rows = lastRowIndex - firstRowIndex + 1;
            while(table.size() < rows) {
                table.add(new BitSet(columns));
            }
            while(table.size() > rows) {
                table.remove(table.size() - 1);
            }
            for(int index = 0; index < table.size(); index++) {
                if(table.get(index).size() != columns) {
                    table.set(index, new BitSet(columns));
                }
                else {
                    table.get(index).clear();
                }
            }
        }

        void setAllDirty() {
            allDirty = true;
        }

        boolean isAllDirty() {
            return allDirty;
        }

        void setDirty(TerminalPosition position) {
            if(position.getRow() < firstRowIndex ||
                    position.getRow() >= firstRowIndex + table.size()) {
                return;
            }
            BitSet tableRow = table.get(position.getRow() - firstRowIndex);
            if(position.getColumn() < tableRow.size()) {
                tableRow.set(position.getColumn());
            }
        }

        void setRowDirty(int rowNumber) {
            BitSet row = table.get(rowNumber - firstRowIndex);
            row.set(0, row.size());
        }

        void setColumnDirty(int column) {
            for(BitSet row: table) {
                if(column < row.size()) {
                    row.set(column);
                }
            }
        }

        boolean isDirty(int row, int column) {
            if(row < firstRowIndex || row >= firstRowIndex + table.size()) {
                return false;
            }
            BitSet tableRow = table.get(row - firstRowIndex);
            if(column < tableRow.size()) {
                return tableRow.get(column);
            }
            else {
                return false;
            }
        }
    }
}