/*
 * Copyright (C) 2007 The Android Open Source Project
 * Copyright (C) 2018-2019 Roumen Petrov.  All rights reserved.
 *
 * 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 jackpal.androidterm.emulatorview;

import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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.Locale;

import android.util.Log;

import androidx.annotation.IntDef;

/**
 * Renders text into a screen. Contains all the terminal-specific knowledge and
 * state. Emulates a subset of the X Window System xterm terminal, which in turn
 * is an emulator for a subset of the Digital Equipment Corporation vt100
 * terminal. Missing functionality: text attributes (bold, underline, reverse
 * video, color) alternate screen cursor key and keypad escape sequences.
 */
class TerminalEmulator {
    public void setKeyListener(TermKeyListener l) {
        mKeyListener = l;
    }
    private TermKeyListener mKeyListener;
    /**
     * The cursor row. Numbered 0..mRows-1.
     */
    private int mCursorRow;

    /**
     * The cursor column. Numbered 0..mColumns-1.
     */
    private int mCursorCol;

    /**
     * The number of character rows in the terminal screen.
     */
    private int mRows;

    /**
     * The number of character columns in the terminal screen.
     */
    private int mColumns;

    /**
     * Stores the characters that appear on the screen of the emulated terminal.
     */
    private TranscriptScreen mMainBuffer;
    private TranscriptScreen mAltBuffer;
    private TranscriptScreen mScreen;

    /**
     * The terminal session this emulator is bound to.
     */
    private TermSession mSession;

    /**
     * Keeps track of the current argument of the current escape sequence.
     * Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.)
     */
    private int mArgIndex;

    /**
     * The number of parameter arguments. This name comes from the ANSI standard
     * for terminal escape codes.
     */
    private static final int MAX_ESCAPE_PARAMETERS = 16;

    /**
     * Holds the arguments of the current escape sequence.
     */
    private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];

    /**
     * Holds OSC arguments, which can be strings.
     */
    private byte[] mOSCArg = new byte[MAX_OSC_STRING_LENGTH];

    private int mOSCArgLength;

    private int mOSCArgTokenizerIndex;

    /**
     * Don't know what the actual limit is, this seems OK for now.
     */
    private static final int MAX_OSC_STRING_LENGTH = 512;

    /**
     * True if the current escape sequence should continue, false if the current
     * escape sequence should be terminated. Used when parsing a single
     * character.
     */
    private boolean mContinueSequence;

    /**
     * The current state of the escape sequence state machine.
     */
    @EscapeProcessingState
    private int mEscapeState;

    /**
     * Saved state of the cursor row, Used to implement the save/restore cursor
     * position escape sequences.
     */
    private int mSavedCursorRow;

    /**
     * Saved state of the cursor column, Used to implement the save/restore
     * cursor position escape sequences.
     */
    private int mSavedCursorCol;

    private int mSavedEffect;

    private int mSavedDecFlags_DECSC_DECRC;


    // DecSet booleans

    /**
     * This mask indicates 132-column mode is set. (As opposed to 80-column
     * mode.)
     */
    private static final int K_132_COLUMN_MODE_MASK = 1 << 3;

    /**
     * DECSCNM - set means reverse video (light background.)
     */
    private static final int K_REVERSE_VIDEO_MASK = 1 << 5;

    /**
     * This mask indicates that origin mode is set. (Cursor addressing is
     * relative to the absolute screen size, rather than the currently set top
     * and bottom margins.)
     */
    private static final int K_ORIGIN_MODE_MASK = 1 << 6;

    /**
     * This mask indicates that wraparound mode is set. (As opposed to
     * stop-at-right-column mode.)
     */
    private static final int K_WRAPAROUND_MODE_MASK = 1 << 7;

    /**
     * This mask indicates that the cursor should be shown. DECTCEM
     */

    private static final int K_SHOW_CURSOR_MASK = 1 << 25;

    /** This mask is the subset of DecSet bits that are saved / restored by
     * the DECSC / DECRC commands
     */
    private static final int K_DECSC_DECRC_MASK =
            K_ORIGIN_MODE_MASK | K_WRAPAROUND_MODE_MASK;

    /**
     * Holds multiple DECSET flags. The data is stored this way, rather than in
     * separate booleans, to make it easier to implement the save-and-restore
     * semantics. The various k*ModeMask masks can be used to extract and modify
     * the individual flags current states.
     */
    private int mDecFlags;

    /**
     * Saves away a snapshot of the DECSET flags. Used to implement save and
     * restore escape sequences.
     */
    private int mSavedDecFlags;

    /**
     * The current DECSET mouse tracking mode, zero for no mouse tracking.
     */
    private int mMouseTrackingMode;

    // Modes set with Set Mode / Reset Mode

    /**
     * True if insert mode (as opposed to replace mode) is active. In insert
     * mode new characters are inserted, pushing existing text to the right.
     */
    private boolean mInsertMode;

    /**
     * An array of tab stops. mTabStop[i] is true if there is a tab stop set for
     * column i.
     */
    private boolean[] mTabStop;

    // The margins allow portions of the screen to be locked.

    /**
     * The top margin of the screen, for scrolling purposes. Ranges from 0 to
     * mRows-2.
     */
    private int mTopMargin;

    /**
     * The bottom margin of the screen, for scrolling purposes. Ranges from
     * mTopMargin + 2 to mRows. (Defines the first row after the scrolling
     * region.
     */
    private int mBottomMargin;

    /**
     * True if the next character to be emitted will be automatically wrapped to
     * the next line. Used to disambiguate the case where the cursor is
     * positioned on column mColumns-1.
     */
    private boolean mAboutToAutoWrap;

    /**
     * The width of the last emitted spacing character.  Used to place
     * combining characters into the correct column.
     */
    private int mLastEmittedCharWidth = 0;

    /**
     * True if we just auto-wrapped and no character has been emitted on this
     * line yet.  Used to ensure combining characters following a character
     * at the edge of the screen are stored in the proper place.
     */
    private boolean mJustWrapped = false;

    /**
     * Used for debugging, counts how many chars have been processed.
     */
    private int mProcessedCharCount;

    /**
     * Foreground color, 0..255
     */
    private int mForeColor;
    private int mDefaultForeColor;

    /**
     * Background color, 0..255
     */
    private int mBackColor;
    private int mDefaultBackColor;

    /**
     * Current TextStyle effect
     */
    private int mEffect;

    private boolean mbKeypadApplicationMode;

    /** false == G0, true == G1 */
    private boolean mAlternateCharSet;

    private final static int CHAR_SET_UK = 0;
    private final static int CHAR_SET_ASCII = 1;
    private final static int CHAR_SET_SPECIAL_GRAPHICS = 2;
    private final static int CHAR_SET_ALT_STANDARD = 3;
    private final static int CHAR_SET_ALT_SPECIAL_GRAPICS = 4;

    /** What is the current graphics character set. [0] == G0, [1] == G1 */
    private int[] mCharSet = new int[2];

    /** Derived from mAlternateCharSet and mCharSet.
     *  True if we're supposed to be drawing the special graphics.
     */
    private boolean mUseAlternateCharSet;

    /**
     * Special graphics character set
     */
    private static final char[] mSpecialGraphicsCharMap = new char[128];
    static {
        for (char i = 0; i < 128; ++i) {
            mSpecialGraphicsCharMap[i] = i;
        }
        mSpecialGraphicsCharMap['_'] = ' ';	// Blank
        mSpecialGraphicsCharMap['b'] = 0x2409;	// Tab
        mSpecialGraphicsCharMap['c'] = 0x240C;	// Form feed
        mSpecialGraphicsCharMap['d'] = 0x240D;	// Carriage return
        mSpecialGraphicsCharMap['e'] = 0x240A;	// Line feed
        mSpecialGraphicsCharMap['h'] = 0x2424;	// New line
        mSpecialGraphicsCharMap['i'] = 0x240B;	// Vertical tab/"lantern"
        mSpecialGraphicsCharMap['}'] = 0x00A3;	// Pound sterling symbol
        mSpecialGraphicsCharMap['f'] = 0x00B0;	// Degree symbol
        mSpecialGraphicsCharMap['`'] = 0x2B25;	// Diamond
        mSpecialGraphicsCharMap['~'] = 0x2022;	// Bullet point
        mSpecialGraphicsCharMap['y'] = 0x2264;	// Less-than-or-equals sign (<=)
        mSpecialGraphicsCharMap['|'] = 0x2260;	// Not equals sign (!=)
        mSpecialGraphicsCharMap['z'] = 0x2265;	// Greater-than-or-equals sign (>=)
        mSpecialGraphicsCharMap['g'] = 0x00B1;	// Plus-or-minus sign (+/-)
        mSpecialGraphicsCharMap['{'] = 0x03C0;	// Lowercase Greek letter pi
        mSpecialGraphicsCharMap['.'] = 0x25BC;	// Down arrow
        mSpecialGraphicsCharMap[','] = 0x25C0;	// Left arrow
        mSpecialGraphicsCharMap['+'] = 0x25B6;	// Right arrow
        mSpecialGraphicsCharMap['-'] = 0x25B2;	// Up arrow
        mSpecialGraphicsCharMap['h'] = '#';	// Board of squares
        mSpecialGraphicsCharMap['a'] = 0x2592;	// Checkerboard
        mSpecialGraphicsCharMap['0'] = 0x2588;	// Solid block
        mSpecialGraphicsCharMap['q'] = 0x2500;	// Horizontal line (box drawing)
        mSpecialGraphicsCharMap['x'] = 0x2502;	// Vertical line (box drawing)
        mSpecialGraphicsCharMap['m'] = 0x2514;	// Lower left hand corner (box drawing)
        mSpecialGraphicsCharMap['j'] = 0x2518;	// Lower right hand corner (box drawing)
        mSpecialGraphicsCharMap['l'] = 0x250C;	// Upper left hand corner (box drawing)
        mSpecialGraphicsCharMap['k'] = 0x2510;	// Upper right hand corner (box drawing)
        mSpecialGraphicsCharMap['w'] = 0x252C;	// T pointing downwards (box drawing)
        mSpecialGraphicsCharMap['u'] = 0x2524;	// T pointing leftwards (box drawing)
        mSpecialGraphicsCharMap['t'] = 0x251C;	// T pointing rightwards (box drawing)
        mSpecialGraphicsCharMap['v'] = 0x2534;	// T pointing upwards (box drawing)
        mSpecialGraphicsCharMap['n'] = 0x253C;	// Large plus/lines crossing (box drawing)
        mSpecialGraphicsCharMap['o'] = 0x23BA;	// Horizontal scanline 1
        mSpecialGraphicsCharMap['p'] = 0x23BB;	// Horizontal scanline 3
        mSpecialGraphicsCharMap['r'] = 0x23BC;	// Horizontal scanline 7
        mSpecialGraphicsCharMap['s'] = 0x23BD;	// Horizontal scanline 9
    }

    /**
     * Used for moving selection up along with the scrolling text
     */
    private int mScrollCounter = 0;

    /**
     * UTF-8 support
     */
    private static final int UNICODE_REPLACEMENT_CHAR = 0xfffd;
    private boolean mUTF8Mode = false;
    private boolean mUTF8EscapeUsed = false;
    private int mUTF8ToFollow = 0;
    private ByteBuffer mUTF8ByteBuffer;
    private CharBuffer mInputCharBuffer;
    private CharsetDecoder mUTF8Decoder;
    private UpdateCallback mUTF8ModeNotify;

    /** This is not accurate, but it makes the terminal more useful on
     * small screens.
     */
    private final static boolean DEFAULT_TO_AUTOWRAP_ENABLED = true;

    /**
     * Construct a terminal emulator that uses the supplied screen
     *
     * @param session the terminal session the emulator is attached to
     * @param screen the screen to render characters into.
     * @param columns the number of columns to emulate
     * @param rows the number of rows to emulate
     * @param scheme the default color scheme of this emulator
     */
    public TerminalEmulator(TermSession session, TranscriptScreen screen, int columns, int rows, ColorScheme scheme) {
        mSession = session;
        mMainBuffer = screen;
        mScreen = mMainBuffer;
        mAltBuffer = new TranscriptScreen(columns, rows, rows, scheme);
        mRows = rows;
        mColumns = columns;
        mTabStop = new boolean[mColumns];

        setColorScheme(scheme);

        mUTF8ByteBuffer = ByteBuffer.allocate(4);
        mInputCharBuffer = CharBuffer.allocate(2);
        mUTF8Decoder = Charset.forName("UTF-8").newDecoder();
        mUTF8Decoder.onMalformedInput(CodingErrorAction.REPLACE);
        mUTF8Decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);

        reset();
    }

    public TranscriptScreen getScreen() {
        return mScreen;
    }

    public void updateSize(int columns, int rows) {
        if (mRows == rows && mColumns == columns) {
            return;
        }
        if (columns <= 0) {
            throw new IllegalArgumentException("rows:" + columns);
        }

        if (rows <= 0) {
            throw new IllegalArgumentException("rows:" + rows);
        }

        TranscriptScreen screen = mScreen;
        TranscriptScreen altScreen;
        if (screen != mMainBuffer) {
            altScreen = mMainBuffer;
        } else {
            altScreen = mAltBuffer;
        }

        // Try to resize the screen without getting the transcript
        int[] cursor = { mCursorCol, mCursorRow };
        boolean fastResize = screen.fastResize(columns, rows, cursor);

        GrowableIntArray cursorColor = null;
        String charAtCursor = null;
        GrowableIntArray colors = null;
        String transcriptText = null;
        if (!fastResize) {
            /* Save the character at the cursor (if one exists) and store an
             * ASCII ESC character at the cursor's location
             * This is an epic hack that lets us restore the cursor later...
             */
            cursorColor = new GrowableIntArray(1);
            charAtCursor = screen.getSelectedText(cursorColor, mCursorCol, mCursorRow, mCursorCol, mCursorRow);
            screen.set(mCursorCol, mCursorRow, 27, 0);

            colors = new GrowableIntArray(1024);
            transcriptText = screen.getTranscriptText(colors);
            screen.resize(columns, rows, getStyle());
        }

        boolean altFastResize = true;
        GrowableIntArray altColors = null;
        String altTranscriptText = null;
        if (altScreen != null) {
            altFastResize = altScreen.fastResize(columns, rows, null);

            if (!altFastResize) {
                altColors = new GrowableIntArray(1024);
                altTranscriptText = altScreen.getTranscriptText(altColors);
                altScreen.resize(columns, rows, getStyle());
            }
        }

        if (mRows != rows) {
            mRows = rows;
            mTopMargin = 0;
            mBottomMargin = mRows;
        }
        if (mColumns != columns) {
            int oldColumns = mColumns;
            mColumns = columns;
            boolean[] oldTabStop = mTabStop;
            mTabStop = new boolean[mColumns];
            int toTransfer = Math.min(oldColumns, columns);
            System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer);
        }

        if (!altFastResize) {
            boolean wasAboutToAutoWrap = mAboutToAutoWrap;

            // Restore the contents of the inactive screen's buffer
            mScreen = altScreen;
            mCursorRow = 0;
            mCursorCol = 0;
            mAboutToAutoWrap = false;

            int end = altTranscriptText.length()-1;
            /* Unlike for the main transcript below, don't trim off trailing
             * newlines -- the alternate transcript lacks a cursor marking, so
             * we might introduce an unwanted vertical shift in the screen
             * contents this way */
            char c, cLow;
            int colorOffset = 0;
            for (int i = 0; i <= end; i++) {
                c = altTranscriptText.charAt(i);
                int style = altColors.at(i-colorOffset);
                if (Character.isHighSurrogate(c)) {
                    cLow = altTranscriptText.charAt(++i);
                    emit(Character.toCodePoint(c, cLow), style);
                    ++colorOffset;
                } else if (c == '\n') {
                    setCursorCol(0);
                    doLinefeed();
                } else {
                    emit(c, style);
                }
            }

            mScreen = screen;
            mAboutToAutoWrap = wasAboutToAutoWrap;
        }

        if (fastResize) {
            // Only need to make sure the cursor is in the right spot
            if (cursor[0] >= 0 && cursor[1] >= 0) {
                mCursorCol = cursor[0];
                mCursorRow = cursor[1];
            } else {
                // Cursor scrolled off screen, reset the cursor to top left
                mCursorCol = 0;
                mCursorRow = 0;
            }

            return;
        }

        mCursorRow = 0;
        mCursorCol = 0;
        mAboutToAutoWrap = false;

        int newCursorRow = -1;
        int newCursorCol = -1;
        int newCursorTranscriptPos = -1;
        int end = transcriptText.length()-1;
        while ((end >= 0) && transcriptText.charAt(end) == '\n') {
            end--;
        }
        char c, cLow;
        int colorOffset = 0;
        for(int i = 0; i <= end; i++) {
            c = transcriptText.charAt(i);
            int style = colors.at(i-colorOffset);
            if (Character.isHighSurrogate(c)) {
                cLow = transcriptText.charAt(++i);
                emit(Character.toCodePoint(c, cLow), style);
                ++colorOffset;
            } else if (c == '\n') {
                setCursorCol(0);
                doLinefeed();
            } else if (c == 27) {
                /* We marked the cursor location with ESC earlier, so this
                   is the place to restore the cursor to */
                newCursorRow = mCursorRow;
                newCursorCol = mCursorCol;
                newCursorTranscriptPos = screen.getActiveRows();
                if (charAtCursor != null && charAtCursor.length() > 0) {
                    // Emit the real character that was in this spot
                    int encodedCursorColor = cursorColor.at(0);
                    emit(charAtCursor.toCharArray(), 0, charAtCursor.length(), encodedCursorColor);
                }
            } else {
                emit(c, style);
            }
        }

        // If we marked a cursor location, move the cursor there now
        if (newCursorRow != -1 && newCursorCol != -1) {
            mCursorRow = newCursorRow;
            mCursorCol = newCursorCol;

            /* Adjust for any scrolling between the time we marked the cursor
               location and now */
            int scrollCount = screen.getActiveRows() - newCursorTranscriptPos;
            if (scrollCount > 0 && scrollCount <= newCursorRow) {
                mCursorRow -= scrollCount;
            } else if (scrollCount > newCursorRow) {
                // Cursor scrolled off screen -- reset to top left corner
                mCursorRow = 0;
                mCursorCol = 0;
            }
        }
    }

    /**
     * Get the cursor's current row.
     *
     * @return the cursor's current row.
     */
    public final int getCursorRow() {
        return mCursorRow;
    }

    /**
     * Get the cursor's current column.
     *
     * @return the cursor's current column.
     */
    public final int getCursorCol() {
        return mCursorCol;
    }

    public final boolean getReverseVideo() {
        return (mDecFlags & K_REVERSE_VIDEO_MASK) != 0;
    }

    public final boolean getShowCursor() {
        return (mDecFlags & K_SHOW_CURSOR_MASK) != 0;
    }

    public final boolean getKeypadApplicationMode() {
        return mbKeypadApplicationMode;
    }

    /**
     * Get the current DECSET mouse tracking mode, zero for no mouse tracking.
     *
     * @return the current DECSET mouse tracking mode.
     */
    public final int getMouseTrackingMode() {
        return mMouseTrackingMode;
    }

    private void setDefaultTabStops() {
        for (int i = 0; i < mColumns; i++) {
            mTabStop[i] = (i & 7) == 0 && i != 0;
        }
    }

    /**
     * Accept bytes (typically from the pseudo-teletype) and process them.
     *
     * @param buffer a byte array containing the bytes to be processed
     * @param base the first index of the array to process
     * @param length the number of bytes in the array to process
     */
    public void append(byte[] buffer, int base, int length) {
        if (EmulatorDebug.LOG_CHARACTERS_FLAG) {
            Log.d(EmulatorDebug.LOG_TAG, "In: '" + EmulatorDebug.bytesToString(buffer, base, length) + "'");
        }
        for (int i = 0; i < length; i++) {
            byte b = buffer[base + i];
            try {
                process(b);
                mProcessedCharCount++;
            } catch (Exception e) {
                Log.e(EmulatorDebug.LOG_TAG, "Exception while processing character "
                        + Integer.toString(mProcessedCharCount) + " code "
                        + Integer.toString(b), e);
            }
        }
    }

    private void process(byte b) {
        process(b, true);
    }

    private void process(byte b, boolean doUTF8) {
        // Let the UTF-8 decoder try to handle it if we're in UTF-8 mode
        if (doUTF8 && mUTF8Mode && handleUTF8Sequence(b)) {
            return;
        }

        // Handle C1 control characters
        if ((b & 0x80) == 0x80 && (b & 0x7f) <= 0x1f) {
            /* ESC ((code & 0x7f) + 0x40) is the two-byte escape sequence
               corresponding to a particular C1 code */
            process((byte) 27, false);
            process((byte) ((b & 0x7f) + 0x40), false);
            return;
        }

        switch (b) {
        case 0: // NUL
            // Do nothing
            break;

        case 7: // BEL
            /* If in an OSC sequence, BEL may terminate a string; otherwise do
             * nothing */
            if (mEscapeState == EscapeProcessingState.RIGHT_SQUARE_BRACKET) {
                doEscRightSquareBracket(b);
            }
            break;

        case 8: // BS
            setCursorCol(Math.max(0, mCursorCol - 1));
            break;

        case 9: // HT
            // Move to next tab stop, but not past edge of screen
            setCursorCol(nextTabStop(mCursorCol));
            break;

        case 13:
            setCursorCol(0);
            break;

        case 10: // CR
        case 11: // VT
        case 12: // LF
            doLinefeed();
            break;

        case 14: // SO:
            setAltCharSet(true);
            break;

        case 15: // SI:
            setAltCharSet(false);
            break;


        case 24: // CAN
        case 26: // SUB
            if (mEscapeState != EscapeProcessingState.NONE) {
                finishSequence();
                emit((byte) 127);
            }
            break;

        case 27: // ESC
            // Starts an escape sequence unless we're parsing a string
            if (mEscapeState != EscapeProcessingState.RIGHT_SQUARE_BRACKET) {
                startSequence();
            } else {
                doEscRightSquareBracket(b);
            }
            break;

        default:
            mContinueSequence = false;
            switch (mEscapeState) {
            case EscapeProcessingState.NONE:
                if (b >= 32) {
                    emit(b);
                }
                break;

            case EscapeProcessingState.START:
                doEsc(b);
                break;

            case EscapeProcessingState.POUND:
                doEscPound(b);
                break;

            case EscapeProcessingState.SELECT_LEFT_PAREN:
                doEscSelectLeftParen(b);
                break;

            case EscapeProcessingState.SELECT_RIGHT_PAREN:
                doEscSelectRightParen(b);
                break;

            case EscapeProcessingState.LEFT_SQUARE_BRACKET:
                doEscLeftSquareBracket(b); // CSI
                break;

            case EscapeProcessingState.LEFT_SQUARE_BRACKET_QUESTION_MARK:
                doEscLSBQuest(b); // CSI ?
                break;

            case EscapeProcessingState.PERCENT:
                doEscPercent(b);
                break;

            case EscapeProcessingState.RIGHT_SQUARE_BRACKET:
                doEscRightSquareBracket(b);
                break;

            case EscapeProcessingState.RIGHT_SQUARE_BRACKET_ESC:
                doEscRightSquareBracketEsc(b);
                break;

            default:
                unknownSequence(b);
                break;
            }
            if (!mContinueSequence)
                finishSequence();
            break;
        }
    }

    private void startSequence() {
        mEscapeState = EscapeProcessingState.START;
        mArgIndex = 0;
        for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++)
            mArgs[j] = -1;
    }

    private void finishSequence() {
        mEscapeState = EscapeProcessingState.NONE;
    }

    private boolean handleUTF8Sequence(byte b) {
        if (mUTF8ToFollow == 0 && (b & 0x80) == 0) {
            // ASCII character -- we don't need to handle this
            return false;
        }

        if (mUTF8ToFollow > 0) {
            if ((b & 0xc0) != 0x80) {
                /* Not a UTF-8 continuation byte (doesn't begin with 0b10)
                   Replace the entire sequence with the replacement char */
                mUTF8ToFollow = 0;
                mUTF8ByteBuffer.clear();
                emit(UNICODE_REPLACEMENT_CHAR);

                /* The Unicode standard (section 3.9, definition D93) requires
                 * that we now attempt to process this byte as though it were
                 * the beginning of another possibly-valid sequence */
                return handleUTF8Sequence(b);
            }

            mUTF8ByteBuffer.put(b);
            if (--mUTF8ToFollow == 0) {
                // Sequence complete -- decode and emit it
                ByteBuffer byteBuf = mUTF8ByteBuffer;
                CharBuffer charBuf = mInputCharBuffer;
                CharsetDecoder decoder = mUTF8Decoder;

                byteBuf.rewind();
                decoder.reset();
                decoder.decode(byteBuf, charBuf, true);
                decoder.flush(charBuf);

                char[] chars = charBuf.array();
                if (chars[0] >= 0x80 && chars[0] <= 0x9f) {
                    /* Sequence decoded to a C1 control character which needs
                       to be sent through process() again */
                    process((byte) chars[0], false);
                } else {
                    emit(chars);
                }

                byteBuf.clear();
                charBuf.clear();
            }
        } else {
            if ((b & 0xe0) == 0xc0) { // 0b110 -- two-byte sequence
                mUTF8ToFollow = 1;
            } else if ((b & 0xf0) == 0xe0) { // 0b1110 -- three-byte sequence
                mUTF8ToFollow = 2;
            } else if ((b & 0xf8) == 0xf0) { // 0b11110 -- four-byte sequence
                mUTF8ToFollow = 3;
            } else {
                // Not a valid UTF-8 sequence start -- replace this char
                emit(UNICODE_REPLACEMENT_CHAR);
                return true;
            }

            mUTF8ByteBuffer.put(b);
        }

        return true;
    }

    private void setAltCharSet(boolean alternateCharSet) {
        mAlternateCharSet = alternateCharSet;
        computeEffectiveCharSet();
    }

    private void computeEffectiveCharSet() {
        int charSet = mCharSet[mAlternateCharSet ? 1 : 0];
        mUseAlternateCharSet = charSet == CHAR_SET_SPECIAL_GRAPHICS;
    }

    private int nextTabStop(int cursorCol) {
        for (int i = cursorCol + 1; i < mColumns; i++) {
            if (mTabStop[i]) {
                return i;
            }
        }
        return mColumns - 1;
    }

    private int prevTabStop(int cursorCol) {
        for (int i = cursorCol - 1; i >= 0; i--) {
            if (mTabStop[i]) {
                return i;
            }
        }
        return 0;
    }

    private void doEscPercent(byte b) {
        switch (b) {
        case '@': // Esc % @ -- return to ISO 2022 mode
           setUTF8Mode(false);
           mUTF8EscapeUsed = true;
           break;
        case 'G': // Esc % G -- UTF-8 mode
           setUTF8Mode(true);
           mUTF8EscapeUsed = true;
           break;
        default: // unimplemented character set
           break;
        }
    }

    private void doEscLSBQuest(byte b) {
        int arg = getArg0(0);
        int mask = getDecFlagsMask(arg);
        int oldFlags = mDecFlags;
        switch (b) {
        case 'h': // Esc [ ? Pn h - DECSET
            mDecFlags |= mask;
            switch (arg) {
            case 1:
                mKeyListener.setCursorKeysApplicationMode(true);
                break;
            case 47:
            case 1047:
            case 1049:
                if (mAltBuffer != null) {
                    mScreen = mAltBuffer;
                }
                break;
            }
            if (arg >= 1000 && arg <= 1003) {
                mMouseTrackingMode = arg;
            }
            break;

        case 'l': // Esc [ ? Pn l - DECRST
            mDecFlags &= ~mask;
            switch (arg) {
            case 1:
                mKeyListener.setCursorKeysApplicationMode(false);
                break;
            case 47:
            case 1047:
            case 1049:
                mScreen = mMainBuffer;
                break;
            }
            if (arg >= 1000 && arg <= 1003) {
                mMouseTrackingMode = 0;
            }
            break;

        case 'r': // Esc [ ? Pn r - restore
            mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask);
            break;

        case 's': // Esc [ ? Pn s - save
            mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask);
            break;

        default:
            parseArg(b);
            break;
        }

        int newlySetFlags = (~oldFlags) & mDecFlags;
        int changedFlags = oldFlags ^ mDecFlags;

        // 132 column mode
        if ((changedFlags & K_132_COLUMN_MODE_MASK) != 0) {
            // We don't actually set/reset 132 cols, but we do want the
            // side effect of clearing the screen and homing the cursor.
            blockClear(0, 0, mColumns, mRows);
            setCursorRowCol(0, 0);
        }

        // origin mode
        if ((newlySetFlags & K_ORIGIN_MODE_MASK) != 0) {
            // Home the cursor.
            setCursorPosition(0, 0);
        }
    }

    private int getDecFlagsMask(int argument) {
        if (argument >= 1 && argument <= 32) {
            return (1 << argument);
        }

        return 0;
    }

    private void doLinefeed() {
        int newCursorRow = mCursorRow + 1;
        if (newCursorRow >= mBottomMargin) {
            scroll();
            newCursorRow = mBottomMargin - 1;
        }
        setCursorRow(newCursorRow);
    }

    private void continueSequence() {
        mContinueSequence = true;
    }

    private void continueSequence(int state) {
        mEscapeState = state;
        mContinueSequence = true;
    }

    private void doEscSelectLeftParen(byte b) {
        doSelectCharSet(0, b);
    }

    private void doEscSelectRightParen(byte b) {
        doSelectCharSet(1, b);
    }

    private void doSelectCharSet(int charSetIndex, byte b) {
        int charSet;
        switch (b) {
        case 'A': // United Kingdom character set
            charSet = CHAR_SET_UK;
            break;
        case 'B': // ASCII set
            charSet = CHAR_SET_ASCII;
            break;
        case '0': // Special Graphics
            charSet = CHAR_SET_SPECIAL_GRAPHICS;
            break;
        case '1': // Alternate character set
            charSet = CHAR_SET_ALT_STANDARD;
            break;
        case '2':
            charSet = CHAR_SET_ALT_SPECIAL_GRAPICS;
            break;
        default:
            unknownSequence(b);
            return;
        }
        mCharSet[charSetIndex] = charSet;
        computeEffectiveCharSet();
    }

    private void doEscPound(byte b) {
        switch (b) {
        case '8': // Esc # 8 - DECALN alignment test
            mScreen.blockSet(0, 0, mColumns, mRows, 'E',
                    getStyle());
            break;

        default:
            unknownSequence(b);
            break;
        }
    }

    private void doEsc(byte b) {
        switch (b) {
        case '#':
            continueSequence(EscapeProcessingState.POUND);
            break;

        case '(':
            continueSequence(EscapeProcessingState.SELECT_LEFT_PAREN);
            break;

        case ')':
            continueSequence(EscapeProcessingState.SELECT_RIGHT_PAREN);
            break;

        case '7': // DECSC save cursor
            mSavedCursorRow = mCursorRow;
            mSavedCursorCol = mCursorCol;
            mSavedEffect = mEffect;
            mSavedDecFlags_DECSC_DECRC = mDecFlags & K_DECSC_DECRC_MASK;
            break;

        case '8': // DECRC restore cursor
            setCursorRowCol(mSavedCursorRow, mSavedCursorCol);
            mEffect = mSavedEffect;
            mDecFlags = (mDecFlags & ~ K_DECSC_DECRC_MASK)
                    | mSavedDecFlags_DECSC_DECRC;
            break;

        case 'D': // INDEX
            doLinefeed();
            break;

        case 'E': // NEL
            setCursorCol(0);
            doLinefeed();
            break;

        case 'F': // Cursor to lower-left corner of screen
            setCursorRowCol(0, mBottomMargin - 1);
            break;

        case 'H': // Tab set
            mTabStop[mCursorCol] = true;
            break;

        case 'M': // Reverse index
            if (mCursorRow <= mTopMargin) {
                mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin
                        - (mTopMargin + 1), 0, mTopMargin + 1);
                blockClear(0, mTopMargin, mColumns);
            } else {
                mCursorRow--;
            }

            break;

        case 'N': // SS2
            unimplementedSequence(b);
            break;

        case '0': // SS3
            unimplementedSequence(b);
            break;

        case 'P': // Device control string
            unimplementedSequence(b);
            break;

        case 'Z': // return terminal ID
            sendDeviceAttributes();
            break;

        case '[':
            continueSequence(EscapeProcessingState.LEFT_SQUARE_BRACKET);
            break;

        case '=': // DECKPAM
            mbKeypadApplicationMode = true;
            break;

        case ']': // OSC
            startCollectingOSCArgs();
            continueSequence(EscapeProcessingState.RIGHT_SQUARE_BRACKET);
            break;

        case '>' : // DECKPNM
            mbKeypadApplicationMode = false;
            break;

        default:
            unknownSequence(b);
            break;
        }
    }

    private void doEscLeftSquareBracket(byte b) {
        // CSI
        switch (b) {
        case '@': // ESC [ Pn @ - ICH Insert Characters
        {
            int charsAfterCursor = mColumns - mCursorCol;
            int charsToInsert = Math.min(getArg0(1), charsAfterCursor);
            int charsToMove = charsAfterCursor - charsToInsert;
            mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1,
                    mCursorCol + charsToInsert, mCursorRow);
            blockClear(mCursorCol, mCursorRow, charsToInsert);
        }
            break;

        case 'A': // ESC [ Pn A - Cursor Up
            setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1)));
            break;

        case 'B': // ESC [ Pn B - Cursor Down
            setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1)));
            break;

        case 'C': // ESC [ Pn C - Cursor Right
            setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1)));
            break;

        case 'D': // ESC [ Pn D - Cursor Left
            setCursorCol(Math.max(0, mCursorCol - getArg0(1)));
            break;

        case 'G': // ESC [ Pn G - Cursor Horizontal Absolute
            setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1);
            break;

        case 'H': // ESC [ Pn ; H - Cursor Position
            setHorizontalVerticalPosition();
            break;

        case 'J': // ESC [ Pn J - ED - Erase in Display
            // ED ignores the scrolling margins.
            switch (getArg0(0)) {
            case 0: // Clear below
                blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
                blockClear(0, mCursorRow + 1, mColumns,
                        mRows - (mCursorRow + 1));
                break;

            case 1: // Erase from the start of the screen to the cursor.
                blockClear(0, 0, mColumns, mCursorRow);
                blockClear(0, mCursorRow, mCursorCol + 1);
                break;

            case 2: // Clear all
                blockClear(0, 0, mColumns, mRows);
                break;

            default:
                unknownSequence(b);
                break;
            }
            break;

        case 'K': // ESC [ Pn K - Erase in Line
            switch (getArg0(0)) {
            case 0: // Clear to right
                blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
                break;

            case 1: // Erase start of line to cursor (including cursor)
                blockClear(0, mCursorRow, mCursorCol + 1);
                break;

            case 2: // Clear whole line
                blockClear(0, mCursorRow, mColumns);
                break;

            default:
                unknownSequence(b);
                break;
            }
            break;

        case 'L': // Insert Lines
        {
            int linesAfterCursor = mBottomMargin - mCursorRow;
            int linesToInsert = Math.min(getArg0(1), linesAfterCursor);
            int linesToMove = linesAfterCursor - linesToInsert;
            mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0,
                    mCursorRow + linesToInsert);
            blockClear(0, mCursorRow, mColumns, linesToInsert);
        }
            break;

        case 'M': // Delete Lines
        {
            int linesAfterCursor = mBottomMargin - mCursorRow;
            int linesToDelete = Math.min(getArg0(1), linesAfterCursor);
            int linesToMove = linesAfterCursor - linesToDelete;
            mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns,
                    linesToMove, 0, mCursorRow);
            blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete);
        }
            break;

        case 'P': // Delete Characters
        {
            int charsAfterCursor = mColumns - mCursorCol;
            int charsToDelete = Math.min(getArg0(1), charsAfterCursor);
            int charsToMove = charsAfterCursor - charsToDelete;
            mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow,
                    charsToMove, 1, mCursorCol, mCursorRow);
            blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete);
        }
            break;

        case 'T': // Mouse tracking
            unimplementedSequence(b);
            break;

        case 'X': // Erase characters
            blockClear(mCursorCol, mCursorRow, getArg0(0));
            break;

        case 'Z': // Back tab
            setCursorCol(prevTabStop(mCursorCol));
            break;

        case '?': // Esc [ ? -- start of a private mode set
            continueSequence(EscapeProcessingState.LEFT_SQUARE_BRACKET_QUESTION_MARK);
            break;

        case 'c': // Send device attributes
            sendDeviceAttributes();
            break;

        case 'd': // ESC [ Pn d - Vert Position Absolute
            setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1);
            break;

        case 'f': // Horizontal and Vertical Position
            setHorizontalVerticalPosition();
            break;

        case 'g': // Clear tab stop
            switch (getArg0(0)) {
            case 0:
                mTabStop[mCursorCol] = false;
                break;

            case 3:
                for (int i = 0; i < mColumns; i++) {
                    mTabStop[i] = false;
                }
                break;

            default:
                // Specified to have no effect.
                break;
            }
            break;

        case 'h': // Set Mode
            doSetMode(true);
            break;

        case 'l': // Reset Mode
            doSetMode(false);
            break;

        case 'm': // Esc [ Pn m - character attributes.
                  // (can have up to 16 numerical arguments)
            selectGraphicRendition();
            break;

        case 'n': // Esc [ Pn n - ECMA-48 Status Report Commands
            //sendDeviceAttributes()
            switch (getArg0(0)) {
            case 5: // Device status report (DSR):
                    // Answer is ESC [ 0 n (Terminal OK).
                byte[] dsr = { (byte) 27, (byte) '[', (byte) '0', (byte) 'n' };
                mSession.write(dsr, 0, dsr.length);
                break;

            case 6: // Cursor position report (CPR):
                    // Answer is ESC [ y ; x R, where x,y is
                    // the cursor location.
                byte[] cpr = String.format(Locale.US, "\033[%d;%dR",
                                 mCursorRow + 1, mCursorCol + 1).getBytes();
                mSession.write(cpr, 0, cpr.length);
                break;

            default:
                break;
            }
            break;

        case 'r': // Esc [ Pn ; Pn r - set top and bottom margins
        {
            // The top margin defaults to 1, the bottom margin
            // (unusually for arguments) defaults to mRows.
            //
            // The escape sequence numbers top 1..23, but we
            // number top 0..22.
            // The escape sequence numbers bottom 2..24, and
            // so do we (because we use a zero based numbering
            // scheme, but we store the first line below the
            // bottom-most scrolling line.
            // As a result, we adjust the top line by -1, but
            // we leave the bottom line alone.
            //
            // Also require that top + 2 <= bottom

            int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2));
            int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows));
            mTopMargin = top;
            mBottomMargin = bottom;

            // The cursor is placed in the home position
            setCursorRowCol(mTopMargin, 0);
        }
            break;

        default:
            parseArg(b);
            break;
        }
    }

    private void selectGraphicRendition() {
        // SGR
        for (int i = 0; i <= mArgIndex; i++) {
            int code = mArgs[i];
            if ( code < 0) {
                if (mArgIndex > 0) {
                    continue;
                } else {
                    code = 0;
                }
            }

            // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics

            if (code == 0) { // reset
                mForeColor = mDefaultForeColor;
                mBackColor = mDefaultBackColor;
                mEffect = TextStyle.fxNormal;
            } else if (code == 1) { // bold
                mEffect |= TextStyle.fxBold;
            } else if (code == 3) { // italics, but rarely used as such; "standout" (inverse colors) with TERM=screen
                mEffect |= TextStyle.fxItalic;
            } else if (code == 4) { // underscore
                mEffect |= TextStyle.fxUnderline;
            } else if (code == 5) { // blink
                mEffect |= TextStyle.fxBlink;
            } else if (code == 7) { // inverse
                mEffect |= TextStyle.fxInverse;
            } else if (code == 8) { // invisible
                mEffect |= TextStyle.fxInvisible;
            } else if (code == 10) { // exit alt charset (TERM=linux)
                setAltCharSet(false);
            } else if (code == 11) { // enter alt charset (TERM=linux)
                setAltCharSet(true);
            } else if (code == 22) { // Normal color or intensity, neither bright, bold nor faint
                //mEffect &= ~(TextStyle.fxBold | TextStyle.fxFaint);
                mEffect &= ~TextStyle.fxBold;
            } else if (code == 23) { // not italic, but rarely used as such; clears standout with TERM=screen
                mEffect &= ~TextStyle.fxItalic;
            } else if (code == 24) { // underline: none
                mEffect &= ~TextStyle.fxUnderline;
            } else if (code == 25) { // blink: none
                mEffect &= ~TextStyle.fxBlink;
            } else if (code == 27) { // image: positive
                mEffect &= ~TextStyle.fxInverse;
            } else if (code == 28) { // invisible
                mEffect &= ~TextStyle.fxInvisible;
            } else if (code >= 30 && code <= 37) { // foreground color
                mForeColor = code - 30;
            } else if (code == 38 && i+2 <= mArgIndex && mArgs[i+1] == 5) { // foreground 256 color
                int color = mArgs[i+2];
                if (isValidColorIndex(color))
                    mForeColor = color;
                i += 2;
            } else if (code == 39) { // set default text color
                mForeColor = mDefaultForeColor;
            } else if (code >= 40 && code <= 47) { // background color
                mBackColor = code - 40;
            } else if (code == 48 && i+2 <= mArgIndex && mArgs[i+1] == 5) { // background 256 color
                int color = mArgs[i+2];
                if (isValidColorIndex(color))
                    mBackColor = color;
                i += 2;
            } else if (code == 49) { // set default background color
                mBackColor = mDefaultBackColor;
            } else if (code >= 90 && code <= 97) { // bright foreground color
                mForeColor = code - 90 + 8;
            } else if (code >= 100 && code <= 107) { // bright background color
                mBackColor = code - 100 + 8;
            } else {
                if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
                    Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
                }
            }
        }
    }

    private boolean isValidColorIndex(int color) {
        // if is in  pre-defined set of 256 colors
        boolean result = (0 <= color && color <= 255);
        if (!result) {
            if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
                Log.w(EmulatorDebug.LOG_TAG,
                        String.format("Invalid color index %d", color));
            }
        }
        return result;
    }

    private void doEscRightSquareBracket(byte b) {
        switch (b) {
        case 0x7:
            doOSC();
            break;
        case 0x1b: // Esc, probably start of Esc \ sequence
            continueSequence(EscapeProcessingState.RIGHT_SQUARE_BRACKET_ESC);
            break;
        default:
            collectOSCArgs(b);
            break;
        }
    }

    private void doEscRightSquareBracketEsc(byte b) {
        switch (b) {
        case '\\':
            doOSC();
            break;

        default:
            // The ESC character was not followed by a \, so insert the ESC and
            // the current character in arg buffer.
            collectOSCArgs((byte) 0x1b);
            collectOSCArgs(b);
            continueSequence(EscapeProcessingState.RIGHT_SQUARE_BRACKET);
            break;
        }
    }

    private void doOSC() { // Operating System Controls
        startTokenizingOSC();
        int ps = nextOSCInt(';');
        switch (ps) {
        case 0: // Change icon name and window title to T
        case 1: // Change icon name to T
        case 2: // Change window title to T
            changeTitle(ps, nextOSCString(-1));
            break;
        default:
            unknownParameter(ps);
            break;
        }
        finishSequence();
    }

    private void changeTitle(int parameter, String title) {
        if (parameter == 0 || parameter == 2) {
            mSession.setTitle(title);
        }
    }

    private void blockClear(int sx, int sy, int w) {
        blockClear(sx, sy, w, 1);
    }

    private void blockClear(int sx, int sy, int w, int h) {
        mScreen.blockSet(sx, sy, w, h, ' ', getStyle());
    }

    private int getStyle() {
        return TextStyle.encode(mForeColor, mBackColor,  mEffect);
    }

    private int getDefaultStyle() {
        return TextStyle.encode(mDefaultForeColor, mDefaultBackColor,  TextStyle.fxNormal);
    }

    private void doSetMode(boolean newValue) {
        int modeBit = getArg0(0);
        switch (modeBit) {
        case 4:
            mInsertMode = newValue;
            break;

        default:
            unknownParameter(modeBit);
            break;
        }
    }

    private void setHorizontalVerticalPosition() {

        // Parameters are Row ; Column

        setCursorPosition(getArg1(1) - 1, getArg0(1) - 1);
    }

    private void setCursorPosition(int x, int y) {
        int effectiveTopMargin = 0;
        int effectiveBottomMargin = mRows;
        if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) {
            effectiveTopMargin = mTopMargin;
            effectiveBottomMargin = mBottomMargin;
        }
        int newRow =
                Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y,
                        effectiveBottomMargin - 1));
        int newCol = Math.max(0, Math.min(x, mColumns - 1));
        setCursorRowCol(newRow, newCol);
    }

    private void sendDeviceAttributes() {
        // This identifies us as a DEC vt100 with advanced
        // video options. This is what the xterm terminal
        // emulator sends.
        byte[] attributes =
                {
                /* VT100 */
                 (byte) 27, (byte) '[', (byte) '?', (byte) '1',
                 (byte) ';', (byte) '2', (byte) 'c'

                /* VT220
                (byte) 27, (byte) '[', (byte) '?', (byte) '6',
                (byte) '0',  (byte) ';',
                (byte) '1',  (byte) ';',
                (byte) '2',  (byte) ';',
                (byte) '6',  (byte) ';',
                (byte) '8',  (byte) ';',
                (byte) '9',  (byte) ';',
                (byte) '1',  (byte) '5', (byte) ';',
                (byte) 'c'
                */
                };

        mSession.write(attributes, 0, attributes.length);
    }

    private void scroll() {
        mScrollCounter ++;
        mScreen.scroll(mTopMargin, mBottomMargin, getDefaultStyle());
    }

    /**
     * Process the next ASCII character of a parameter.
     *
     * @param b The next ASCII character of the paramater sequence.
     */
    private void parseArg(byte b) {
        if (b >= '0' && b <= '9') {
            if (mArgIndex < mArgs.length) {
                int oldValue = mArgs[mArgIndex];
                int thisDigit = b - '0';
                int value;
                if (oldValue >= 0) {
                    value = oldValue * 10 + thisDigit;
                } else {
                    value = thisDigit;
                }
                mArgs[mArgIndex] = value;
            }
            continueSequence();
        } else if (b == ';') {
            if (mArgIndex < mArgs.length) {
                mArgIndex++;
            }
            continueSequence();
        } else {
            unknownSequence(b);
        }
    }

    private int getArg0(int defaultValue) {
        return getArg(0, defaultValue, true);
    }

    private int getArg1(int defaultValue) {
        return getArg(1, defaultValue, true);
    }

    private int getArg(int index, int defaultValue,
            boolean treatZeroAsDefault) {
        int result = mArgs[index];
        if (result < 0 || (result == 0 && treatZeroAsDefault)) {
            result = defaultValue;
        }
        return result;
    }

    private void startCollectingOSCArgs() {
        mOSCArgLength = 0;
    }

    private void collectOSCArgs(byte b) {
        if (mOSCArgLength < MAX_OSC_STRING_LENGTH) {
            mOSCArg[mOSCArgLength++] = b;
            continueSequence();
        } else {
            unknownSequence(b);
        }
    }

    private void startTokenizingOSC() {
        mOSCArgTokenizerIndex = 0;
    }

    private String nextOSCString(int delimiter) {
        int start = mOSCArgTokenizerIndex;
        int end = start;
        while (mOSCArgTokenizerIndex < mOSCArgLength) {
            byte b = mOSCArg[mOSCArgTokenizerIndex++];
            if ((int) b == delimiter) {
                break;
            }
            end++;
        }
        if (start == end) {
            return "";
        }
        try {
            return new String(mOSCArg, start, end-start, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return new String(mOSCArg, start, end-start);
        }
    }

    private int nextOSCInt(int delimiter) {
        int value = -1;
        while (mOSCArgTokenizerIndex < mOSCArgLength) {
            byte b = mOSCArg[mOSCArgTokenizerIndex++];
            if ((int) b == delimiter) {
                break;
            } else if (b >= '0' && b <= '9') {
                if (value < 0) {
                    value = 0;
                }
                value = value * 10 + b - '0';
            } else {
                unknownSequence(b);
            }
        }
        return value;
    }

    private void unimplementedSequence(byte b) {
        if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
            logError("unimplemented", b);
        }
        finishSequence();
    }

    private void unknownSequence(byte b) {
        if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
            logError("unknown", b);
        }
        finishSequence();
    }

    private void unknownParameter(int parameter) {
        if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
            StringBuilder buf = new StringBuilder();
            buf.append("Unknown parameter");
            buf.append(parameter);
            logError(buf.toString());
        }
    }

    private void logError(String errorType, byte b) {
        if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
            StringBuilder buf = new StringBuilder();
            buf.append(errorType);
            buf.append(" sequence ");
            buf.append(" EscapeState: ");
            buf.append(mEscapeState);
            buf.append(" char: '");
            buf.append((char) b);
            buf.append("' (");
            buf.append(b);
            buf.append(")");
            boolean firstArg = true;
            for (int i = 0; i <= mArgIndex; i++) {
                int value = mArgs[i];
                if (value >= 0) {
                    if (firstArg) {
                        firstArg = false;
                        buf.append("args = ");
                    }
                    buf.append(String.format("%d; ", value));
                }
            }
            logError(buf.toString());
        }
    }

    private void logError(String error) {
        if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
            Log.e(EmulatorDebug.LOG_TAG, error);
        }
        finishSequence();
    }

    private boolean autoWrapEnabled() {
        return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0;
    }

    /**
     * Send a Unicode code point to the screen.
     *
     * @param c The code point of the character to display
     * @param style The encoded text style (effects, foreground and background colors)
     */
    private void emit(int c, int style) {
        boolean autoWrap = autoWrapEnabled();
        int width = UnicodeTranscript.charWidth(c);

        if (autoWrap) {
            if (mCursorCol == mColumns - 1 && (mAboutToAutoWrap || width == 2)) {
                mScreen.setLineWrap(mCursorRow);
                mCursorCol = 0;
                mJustWrapped = true;
                if (mCursorRow + 1 < mBottomMargin) {
                    mCursorRow++;
                } else {
                    scroll();
                }
            }
        }

        if (mInsertMode & width != 0) { // Move character to right one space
            int destCol = mCursorCol + width;
            if (destCol < mColumns) {
                mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol,
                        1, destCol, mCursorRow);
            }
        }

        if (width == 0) {
            // Combining character -- store along with character it modifies
            if (mJustWrapped) {
                mScreen.set(mColumns - mLastEmittedCharWidth, mCursorRow - 1, c, style);
            } else {
                mScreen.set(mCursorCol - mLastEmittedCharWidth, mCursorRow, c, style);
            }
        } else {
            mScreen.set(mCursorCol, mCursorRow, c, style);
            mJustWrapped = false;
        }

        if (autoWrap) {
            mAboutToAutoWrap = (mCursorCol == mColumns - 1);

            //Force line-wrap flag to trigger even for lines being typed
            if(mAboutToAutoWrap)
                mScreen.setLineWrap(mCursorRow);
        }

        mCursorCol = Math.min(mCursorCol + width, mColumns - 1);
        if (width > 0) {
            mLastEmittedCharWidth = width;
        }
    }

    private void emit(int c) {
        emit(c, getStyle());
    }

    private void emit(byte b) {
        if (mUseAlternateCharSet && b < 128) {
            emit((int) mSpecialGraphicsCharMap[b]);
        } else {
            emit((int) b);
        }
    }

    /**
     * Send a UTF-16 char or surrogate pair to the screen.
     *
     * @param c A char[2] containing either a single UTF-16 char or a surrogate pair to be sent to the screen.
     */
    private void emit(char[] c) {
        if (Character.isHighSurrogate(c[0])) {
            emit(Character.toCodePoint(c[0], c[1]));
        } else {
            emit((int) c[0]);
        }
    }

    /**
     * Send an array of UTF-16 chars to the screen.
     *
     * @param c A char[] array whose contents are to be sent to the screen.
     */
    private void emit(char[] c, int offset, int length, int style) {
        for (int i = offset; i < length; ++i) {
            if (c[i] == 0) {
                break;
            }
            if (Character.isHighSurrogate(c[i])) {
                emit(Character.toCodePoint(c[i], c[i+1]), style);
                ++i;
            } else {
                emit((int) c[i], style);
            }
        }
    }

    private void setCursorRow(int row) {
        mCursorRow = row;
        mAboutToAutoWrap = false;
    }

    private void setCursorCol(int col) {
        mCursorCol = col;
        mAboutToAutoWrap = false;
    }

    private void setCursorRowCol(int row, int col) {
        mCursorRow = Math.min(row, mRows-1);
        mCursorCol = Math.min(col, mColumns-1);
        mAboutToAutoWrap = false;
    }

    public int getScrollCounter() {
        return mScrollCounter;
    }

    public void clearScrollCounter() {
        mScrollCounter = 0;
    }

    /**
     * Reset the terminal emulator to its initial state.
     */
    public void reset() {
        mCursorRow = 0;
        mCursorCol = 0;
        mArgIndex = 0;
        mContinueSequence = false;
        mEscapeState = EscapeProcessingState.NONE;
        mSavedCursorRow = 0;
        mSavedCursorCol = 0;
        mSavedEffect = 0;
        mSavedDecFlags_DECSC_DECRC = 0;
        mDecFlags = 0;
        if (DEFAULT_TO_AUTOWRAP_ENABLED) {
            mDecFlags |= K_WRAPAROUND_MODE_MASK;
        }
        mDecFlags |= K_SHOW_CURSOR_MASK;
        mSavedDecFlags = 0;
        mInsertMode = false;
        mTopMargin = 0;
        mBottomMargin = mRows;
        mAboutToAutoWrap = false;
        mForeColor = mDefaultForeColor;
        mBackColor = mDefaultBackColor;
        mbKeypadApplicationMode = false;
        mAlternateCharSet = false;
        mCharSet[0] = CHAR_SET_ASCII;
        mCharSet[1] = CHAR_SET_SPECIAL_GRAPHICS;
        computeEffectiveCharSet();
        // mProcessedCharCount is preserved unchanged.
        setDefaultTabStops();
        blockClear(0, 0, mColumns, mRows);

        mUTF8EscapeUsed = false;
        mUTF8ToFollow = 0;
        mUTF8ByteBuffer.clear();
        mInputCharBuffer.clear();
        mUTF8Mode = mSession.mDefaultUTF8Mode;
    }

    public void notifyUTF8ModeDefaultChange() {
        if (!mUTF8EscapeUsed) {
            setUTF8Mode(mSession.mDefaultUTF8Mode);
        }
    }

    private void setUTF8Mode(boolean utf8Mode) {
        if (utf8Mode && !mUTF8Mode) {
            mUTF8ToFollow = 0;
            mUTF8ByteBuffer.clear();
            mInputCharBuffer.clear();
        }
        mUTF8Mode = utf8Mode;
        if (mUTF8ModeNotify != null) {
            mUTF8ModeNotify.onUpdate();
        }
    }

    public boolean getUTF8Mode() {
        return mUTF8Mode;
    }

    public void setUTF8ModeUpdateCallback(UpdateCallback utf8ModeNotify) {
        mUTF8ModeNotify = utf8ModeNotify;
    }

    public void setColorScheme(ColorScheme scheme) {
        mDefaultForeColor = TextStyle.ciForeground;
        mDefaultBackColor = TextStyle.ciBackground;
        mMainBuffer.setColorScheme(scheme);
        if (mAltBuffer != null) {
            mAltBuffer.setColorScheme(scheme);
        }
    }

    public String getSelectedText(int x1, int y1, int x2, int y2) {
        return mScreen.getSelectedText(x1, y1, x2, y2);
    }

    public void finish() {
        if (mAltBuffer != null) {
            mAltBuffer.finish();
            mAltBuffer = null;
        }
    }

    @IntDef({
            EscapeProcessingState.NONE,
            EscapeProcessingState.START,
            EscapeProcessingState.POUND,
            EscapeProcessingState.SELECT_LEFT_PAREN,
            EscapeProcessingState.SELECT_RIGHT_PAREN,
            EscapeProcessingState.LEFT_SQUARE_BRACKET,
            EscapeProcessingState.LEFT_SQUARE_BRACKET_QUESTION_MARK,
            EscapeProcessingState.PERCENT,
            EscapeProcessingState.RIGHT_SQUARE_BRACKET,
            EscapeProcessingState.RIGHT_SQUARE_BRACKET_ESC
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface EscapeProcessingState {
        int NONE = 0; // Not currently in an escape sequence
        int START = 1; // Have seen an ESC character
        int POUND = 2; // ESC #
        int SELECT_LEFT_PAREN = 3; // ESC (
        int SELECT_RIGHT_PAREN = 4; // ESC )
        int LEFT_SQUARE_BRACKET = 5; // ESC [
        int LEFT_SQUARE_BRACKET_QUESTION_MARK = 6; // ESC [ ?
        int PERCENT = 7; // ESC %
        int RIGHT_SQUARE_BRACKET = 8; // ESC ] (AKA OSC - Operating System Controls)
        int RIGHT_SQUARE_BRACKET_ESC = 9; //ESC ] (AKA OSC - Operating System Controls)
    }
}
