/*
 * ConnectBot: simple, powerful, open-source SSH client for Android
 * Copyright 2007 Kenny Root, Jeffrey Sharkey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.connectbot.service;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.Bitmap.Config;
import android.graphics.Paint.FontMetrics;
import android.text.ClipboardManager;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.R;
import com.googlecode.android_scripting.facade.ui.UiFacade;
import com.googlecode.android_scripting.interpreter.InterpreterProcess;
import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;

import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
import de.mud.terminal.vt320;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;

import org.connectbot.TerminalView;
import org.connectbot.transport.AbsTransport;
import org.connectbot.util.Colors;
import org.connectbot.util.PreferenceConstants;
import org.connectbot.util.SelectionArea;

/**
 * Provides a bridge between a MUD terminal buffer and a possible TerminalView. This separation
 * allows us to keep the TerminalBridge running in a background service. A TerminalView shares down
 * a bitmap that we can use for rendering when available.
 *
 *
 */
public class TerminalBridge implements VDUDisplay, OnSharedPreferenceChangeListener {

  private final static int FONT_SIZE_STEP = 2;

  private final int[] color = new int[Colors.defaults.length];

  private final TerminalManager manager;

  private final InterpreterProcess mProcess;

  private int mDefaultFgColor;
  private int mDefaultBgColor;

  private int scrollback;

  private String delKey;
  private String encoding;

  private AbsTransport transport;

  private final Paint defaultPaint;

  private Relay relay;

  private Bitmap bitmap = null;
  private final VDUBuffer buffer;

  private TerminalView parent = null;
  private final Canvas canvas = new Canvas();

  private boolean forcedSize = false;
  private int columns;
  private int rows;

  private final TerminalKeyListener keyListener;

  private boolean selectingForCopy = false;
  private final SelectionArea selectionArea;
  private ClipboardManager clipboard;

  public int charWidth = -1;
  public int charHeight = -1;
  private int charTop = -1;

  private float fontSize = -1;

  private final List<FontSizeChangedListener> fontSizeChangedListeners;

  /**
   * Flag indicating if we should perform a full-screen redraw during our next rendering pass.
   */
  private boolean fullRedraw = false;

  private final PromptHelper promptHelper;

  /**
   * Create a new terminal bridge suitable for unit testing.
   */
  public TerminalBridge() {
    buffer = new vt320() {
      @Override
      public void write(byte[] b) {
      }

      @Override
      public void write(int b) {
      }

      @Override
      public void sendTelnetCommand(byte cmd) {
      }

      @Override
      public void setWindowSize(int c, int r) {
      }

      @Override
      public void debug(String s) {
      }
    };

    manager = null;

    defaultPaint = new Paint();

    selectionArea = new SelectionArea();
    scrollback = 1;

    fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();

    transport = null;

    keyListener = new TerminalKeyListener(manager, this, buffer, null);

    mProcess = null;

    mDefaultFgColor = 0;
    mDefaultBgColor = 0;
    promptHelper = null;

    updateCharset();
  }

  /**
   * Create new terminal bridge with following parameters.
   */
  public TerminalBridge(final TerminalManager manager, InterpreterProcess process, AbsTransport t)
      throws IOException {
    this.manager = manager;
    transport = t;
    mProcess = process;

    String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null);
    if (string != null) {
      scrollback = Integer.parseInt(string);
    } else {
      scrollback = PreferenceConstants.DEFAULT_SCROLLBACK;
    }

    string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null);
    if (string != null) {
      fontSize = Float.parseFloat(string);
    } else {
      fontSize = PreferenceConstants.DEFAULT_FONT_SIZE;
    }

    mDefaultFgColor =
        manager.getIntParameter(PreferenceConstants.COLOR_FG, PreferenceConstants.DEFAULT_FG_COLOR);
    mDefaultBgColor =
        manager.getIntParameter(PreferenceConstants.COLOR_BG, PreferenceConstants.DEFAULT_BG_COLOR);

    delKey = manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL);

    // create prompt helper to relay password and hostkey requests up to gui
    promptHelper = new PromptHelper(this);

    // create our default paint
    defaultPaint = new Paint();
    defaultPaint.setAntiAlias(true);
    defaultPaint.setTypeface(Typeface.MONOSPACE);
    defaultPaint.setFakeBoldText(true); // more readable?

    fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();

    setFontSize(fontSize);

    // create terminal buffer and handle outgoing data
    // this is probably status reply information
    buffer = new vt320() {
      @Override
      public void debug(String s) {
        Log.d(s);
      }

      @Override
      public void write(byte[] b) {
        try {
          if (b != null && transport != null) {
            transport.write(b);
          }
        } catch (IOException e) {
          Log.e("Problem writing outgoing data in vt320() thread", e);
        }
      }

      @Override
      public void write(int b) {
        try {
          if (transport != null) {
            transport.write(b);
          }
        } catch (IOException e) {
          Log.e("Problem writing outgoing data in vt320() thread", e);
        }
      }

      // We don't use telnet sequences.
      @Override
      public void sendTelnetCommand(byte cmd) {
      }

      // We don't want remote to resize our window.
      @Override
      public void setWindowSize(int c, int r) {
      }

      @Override
      public void beep() {
        if (parent.isShown()) {
          manager.playBeep();
        }
      }
    };

    // Don't keep any scrollback if a session is not being opened.

    buffer.setBufferSize(scrollback);

    resetColors();
    buffer.setDisplay(this);

    selectionArea = new SelectionArea();

    keyListener = new TerminalKeyListener(manager, this, buffer, encoding);

    updateCharset();

    manager.registerOnSharedPreferenceChangeListener(this);

  }

  /**
   * Spawn thread to open connection and start login process.
   */
  protected void connect() {
    transport.setBridge(this);
    transport.setManager(manager);
    transport.connect();

    ((vt320) buffer).reset();

    // previously tried vt100 and xterm for emulation modes
    // "screen" works the best for color and escape codes
    ((vt320) buffer).setAnswerBack("screen");

    if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) {
      ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE);
    } else {
      ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL);
    }

    // create thread to relay incoming connection data to buffer
    relay = new Relay(this, transport, (vt320) buffer, encoding);
    Thread relayThread = new Thread(relay);
    relayThread.setDaemon(true);
    relayThread.setName("Relay");
    relayThread.start();

    // force font-size to make sure we resizePTY as needed
    setFontSize(fontSize);

  }

  private void updateCharset() {
    encoding =
        manager.getStringParameter(PreferenceConstants.ENCODING, Charset.defaultCharset().name());
    if (relay != null) {
      relay.setCharset(encoding);
    }
    keyListener.setCharset(encoding);
  }

  /**
   * Inject a specific string into this terminal. Used for post-login strings and pasting clipboard.
   */
  public void injectString(final String string) {
    if (string == null || string.length() == 0) {
      return;
    }

    Thread injectStringThread = new Thread(new Runnable() {
      public void run() {
        try {
          transport.write(string.getBytes(encoding));
        } catch (Exception e) {
          Log.e("Couldn't inject string to remote host: ", e);
        }
      }
    });
    injectStringThread.setName("InjectString");
    injectStringThread.start();
  }

  /**
   * @return whether a session is open or not
   */
  public boolean isSessionOpen() {
    if (transport != null) {
      return transport.isSessionOpen();
    }
    return false;
  }

  /**
   * Force disconnection of this terminal bridge.
   */
  public void dispatchDisconnect(boolean immediate) {

    // Cancel any pending prompts.
    promptHelper.cancelPrompt();

    if (immediate) {
      manager.closeConnection(TerminalBridge.this, true);
    } else {
      Thread disconnectPromptThread = new Thread(new Runnable() {
        public void run() {
          String prompt = null;
          if (transport != null && transport.isConnected()) {
            prompt = manager.getResources().getString(R.string.prompt_confirm_exit);
          } else {
            prompt = manager.getResources().getString(R.string.prompt_process_exited);
          }
          Boolean result = promptHelper.requestBooleanPrompt(null, prompt);

          if (transport != null && transport.isConnected()) {
            manager.closeConnection(TerminalBridge.this, result != null && result.booleanValue());
          } else if (result != null && result.booleanValue()) {
            manager.closeConnection(TerminalBridge.this, false);
          }
        }
      });
      disconnectPromptThread.setName("DisconnectPrompt");
      disconnectPromptThread.setDaemon(true);
      disconnectPromptThread.start();
    }
  }

  public void setSelectingForCopy(boolean selectingForCopy) {
    this.selectingForCopy = selectingForCopy;
  }

  public boolean isSelectingForCopy() {
    return selectingForCopy;
  }

  public SelectionArea getSelectionArea() {
    return selectionArea;
  }

  public synchronized void tryKeyVibrate() {
    manager.tryKeyVibrate();
  }

  /**
   * Request a different font size. Will make call to parentChanged() to make sure we resize PTY if
   * needed.
   */
  /* package */final void setFontSize(float size) {
    if (size <= 0.0) {
      return;
    }

    defaultPaint.setTextSize(size);
    fontSize = size;

    // read new metrics to get exact pixel dimensions
    FontMetrics fm = defaultPaint.getFontMetrics();
    charTop = (int) Math.ceil(fm.top);

    float[] widths = new float[1];
    defaultPaint.getTextWidths("X", widths);
    charWidth = (int) Math.ceil(widths[0]);
    charHeight = (int) Math.ceil(fm.descent - fm.top);

    // refresh any bitmap with new font size
    if (parent != null) {
      parentChanged(parent);
    }

    for (FontSizeChangedListener ofscl : fontSizeChangedListeners) {
      ofscl.onFontSizeChanged(size);
    }
    forcedSize = false;
  }

  /**
   * Add an {@link FontSizeChangedListener} to the list of listeners for this bridge.
   *
   * @param listener
   *          listener to add
   */
  public void addFontSizeChangedListener(FontSizeChangedListener listener) {
    fontSizeChangedListeners.add(listener);
  }

  /**
   * Remove an {@link FontSizeChangedListener} from the list of listeners for this bridge.
   *
   * @param listener
   */
  public void removeFontSizeChangedListener(FontSizeChangedListener listener) {
    fontSizeChangedListeners.remove(listener);
  }

  /**
   * Something changed in our parent {@link TerminalView}, maybe it's a new parent, or maybe it's an
   * updated font size. We should recalculate terminal size information and request a PTY resize.
   */
  public final synchronized void parentChanged(TerminalView parent) {
    if (manager != null && !manager.isResizeAllowed()) {
      Log.d("Resize is not allowed now");
      return;
    }

    this.parent = parent;
    final int width = parent.getWidth();
    final int height = parent.getHeight();

    // Something has gone wrong with our layout; we're 0 width or height!
    if (width <= 0 || height <= 0) {
      return;
    }

    clipboard = (ClipboardManager) parent.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
    keyListener.setClipboardManager(clipboard);

    if (!forcedSize) {
      // recalculate buffer size
      int newColumns, newRows;

      newColumns = width / charWidth;
      newRows = height / charHeight;

      // If nothing has changed in the terminal dimensions and not an intial
      // draw then don't blow away scroll regions and such.
      if (newColumns == columns && newRows == rows) {
        return;
      }

      columns = newColumns;
      rows = newRows;
    }

    // reallocate new bitmap if needed
    boolean newBitmap = (bitmap == null);
    if (bitmap != null) {
      newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height);
    }

    if (newBitmap) {
      discardBitmap();
      bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
      canvas.setBitmap(bitmap);
    }

    // clear out any old buffer information
    defaultPaint.setColor(Color.BLACK);
    canvas.drawPaint(defaultPaint);

    // Stroke the border of the terminal if the size is being forced;
    if (forcedSize) {
      int borderX = (columns * charWidth) + 1;
      int borderY = (rows * charHeight) + 1;

      defaultPaint.setColor(Color.GRAY);
      defaultPaint.setStrokeWidth(0.0f);
      if (width >= borderX) {
        canvas.drawLine(borderX, 0, borderX, borderY + 1, defaultPaint);
      }
      if (height >= borderY) {
        canvas.drawLine(0, borderY, borderX + 1, borderY, defaultPaint);
      }
    }

    try {
      // request a terminal pty resize
      synchronized (buffer) {
        buffer.setScreenSize(columns, rows, true);
      }

      if (transport != null) {
        transport.setDimensions(columns, rows, width, height);
      }
    } catch (Exception e) {
      Log.e("Problem while trying to resize screen or PTY", e);
    }

    // force full redraw with new buffer size
    fullRedraw = true;
    redraw();

    parent.notifyUser(String.format("%d x %d", columns, rows));

    Log.i(String.format("parentChanged() now width=%d, height=%d", columns, rows));
  }

  /**
   * Somehow our parent {@link TerminalView} was destroyed. Now we don't need to redraw anywhere,
   * and we can recycle our internal bitmap.
   */
  public synchronized void parentDestroyed() {
    parent = null;
    discardBitmap();
  }

  private void discardBitmap() {
    if (bitmap != null) {
      bitmap.recycle();
    }
    bitmap = null;
  }

  public void onDraw() {
    int fg, bg;
    synchronized (buffer) {
      boolean entireDirty = buffer.update[0] || fullRedraw;
      boolean isWideCharacter = false;

      // walk through all lines in the buffer
      for (int l = 0; l < buffer.height; l++) {

        // check if this line is dirty and needs to be repainted
        // also check for entire-buffer dirty flags
        if (!entireDirty && !buffer.update[l + 1]) {
          continue;
        }

        // reset dirty flag for this line
        buffer.update[l + 1] = false;

        // walk through all characters in this line
        for (int c = 0; c < buffer.width; c++) {
          int addr = 0;
          int currAttr = buffer.charAttributes[buffer.windowBase + l][c];
          // check if foreground color attribute is set
          if ((currAttr & VDUBuffer.COLOR_FG) != 0) {
            int fgcolor = ((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1;
            if (fgcolor < 8 && (currAttr & VDUBuffer.BOLD) != 0) {
              fg = color[fgcolor + 8];
            } else {
              fg = color[fgcolor];
            }
          } else {
            fg = mDefaultFgColor;
          }

          // check if background color attribute is set
          if ((currAttr & VDUBuffer.COLOR_BG) != 0) {
            bg = color[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1];
          } else {
            bg = mDefaultBgColor;
          }

          // support character inversion by swapping background and foreground color
          if ((currAttr & VDUBuffer.INVERT) != 0) {
            int swapc = bg;
            bg = fg;
            fg = swapc;
          }

          // set underlined attributes if requested
          defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0);

          isWideCharacter = (currAttr & VDUBuffer.FULLWIDTH) != 0;

          if (isWideCharacter) {
            addr++;
          } else {
            // determine the amount of continuous characters with the same settings and print them
            // all at once
            while (c + addr < buffer.width
                && buffer.charAttributes[buffer.windowBase + l][c + addr] == currAttr) {
              addr++;
            }
          }

          // Save the current clip region
          canvas.save(Canvas.CLIP_SAVE_FLAG);

          // clear this dirty area with background color
          defaultPaint.setColor(bg);
          if (isWideCharacter) {
            canvas.clipRect(c * charWidth, l * charHeight, (c + 2) * charWidth, (l + 1)
                * charHeight);
          } else {
            canvas.clipRect(c * charWidth, l * charHeight, (c + addr) * charWidth, (l + 1)
                * charHeight);
          }
          canvas.drawPaint(defaultPaint);

          // write the text string starting at 'c' for 'addr' number of characters
          defaultPaint.setColor(fg);
          if ((currAttr & VDUBuffer.INVISIBLE) == 0) {
            canvas.drawText(buffer.charArray[buffer.windowBase + l], c, addr, c * charWidth,
                (l * charHeight) - charTop, defaultPaint);
          }

          // Restore the previous clip region
          canvas.restore();

          // advance to the next text block with different characteristics
          c += addr - 1;
          if (isWideCharacter) {
            c++;
          }
        }
      }

      // reset entire-buffer flags
      buffer.update[0] = false;
    }
    fullRedraw = false;
  }

  public void redraw() {
    if (parent != null) {
      parent.postInvalidate();
    }
  }

  // We don't have a scroll bar.
  public void updateScrollBar() {
  }

  /**
   * Resize terminal to fit [rows]x[cols] in screen of size [width]x[height]
   *
   * @param rows
   * @param cols
   * @param width
   * @param height
   */
  public synchronized void resizeComputed(int cols, int rows, int width, int height) {
    float size = 8.0f;
    float step = 8.0f;
    float limit = 0.125f;

    int direction;

    while ((direction = fontSizeCompare(size, cols, rows, width, height)) < 0) {
      size += step;
    }

    if (direction == 0) {
      Log.d(String.format("Fontsize: found match at %f", size));
      return;
    }

    step /= 2.0f;
    size -= step;

    while ((direction = fontSizeCompare(size, cols, rows, width, height)) != 0 && step >= limit) {
      step /= 2.0f;
      if (direction > 0) {
        size -= step;
      } else {
        size += step;
      }
    }

    if (direction > 0) {
      size -= step;
    }

    columns = cols;
    this.rows = rows;
    setFontSize(size);
    forcedSize = true;
  }

  private int fontSizeCompare(float size, int cols, int rows, int width, int height) {
    // read new metrics to get exact pixel dimensions
    defaultPaint.setTextSize(size);
    FontMetrics fm = defaultPaint.getFontMetrics();

    float[] widths = new float[1];
    defaultPaint.getTextWidths("X", widths);
    int termWidth = (int) widths[0] * cols;
    int termHeight = (int) Math.ceil(fm.descent - fm.top) * rows;

    Log.d(String.format("Fontsize: font size %f resulted in %d x %d", size, termWidth, termHeight));

    // Check to see if it fits in resolution specified.
    if (termWidth > width || termHeight > height) {
      return 1;
    }

    if (termWidth == width || termHeight == height) {
      return 0;
    }

    return -1;
  }

  /*
   * (non-Javadoc)
   *
   * @see de.mud.terminal.VDUDisplay#setVDUBuffer(de.mud.terminal.VDUBuffer)
   */
  @Override
  public void setVDUBuffer(VDUBuffer buffer) {
  }

  /*
   * (non-Javadoc)
   *
   * @see de.mud.terminal.VDUDisplay#setColor(byte, byte, byte, byte)
   */
  public void setColor(int index, int red, int green, int blue) {
    // Don't allow the system colors to be overwritten for now. May violate specs.
    if (index < color.length && index >= 16) {
      color[index] = 0xff000000 | red << 16 | green << 8 | blue;
    }
  }

  public final void resetColors() {
    System.arraycopy(Colors.defaults, 0, color, 0, Colors.defaults.length);
  }

  public TerminalKeyListener getKeyHandler() {
    return keyListener;
  }

  public void resetScrollPosition() {
    // if we're in scrollback, scroll to bottom of window on input
    if (buffer.windowBase != buffer.screenBase) {
      buffer.setWindowBase(buffer.screenBase);
    }
  }

  public void increaseFontSize() {
    setFontSize(fontSize + FONT_SIZE_STEP);
  }

  public void decreaseFontSize() {
    setFontSize(fontSize - FONT_SIZE_STEP);
  }

  public int getId() {
    return mProcess.getPort();
  }

  public String getName() {
    return mProcess.getName();
  }

  public InterpreterProcess getProcess() {
    return mProcess;
  }

  public int getForegroundColor() {
    return mDefaultFgColor;
  }

  public int getBackgroundColor() {
    return mDefaultBgColor;
  }

  public VDUBuffer getVDUBuffer() {
    return buffer;
  }

  public PromptHelper getPromptHelper() {
    return promptHelper;
  }

  public Bitmap getBitmap() {
    return bitmap;
  }

  public AbsTransport getTransport() {
    return transport;
  }

  public Paint getPaint() {
    return defaultPaint;
  }

  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    if (mProcess.isAlive()) {
      RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory();
      for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) {
        UiFacade facade = manager.getReceiver(UiFacade.class);
        facade.onCreateContextMenu(menu, v, menuInfo);
      }
    }
  }

  public boolean onPrepareOptionsMenu(Menu menu) {
    boolean returnValue = false;
    if (mProcess.isAlive()) {
      RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory();
      for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) {
        UiFacade facade = manager.getReceiver(UiFacade.class);
        returnValue = returnValue || facade.onPrepareOptionsMenu(menu);
      }
      return returnValue;
    }
    return false;
  }

  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    if (PreferenceConstants.ENCODING.equals(key)) {
      updateCharset();
    } else if (PreferenceConstants.FONTSIZE.equals(key)) {
      String string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null);
      if (string != null) {
        fontSize = Float.parseFloat(string);
      } else {
        fontSize = PreferenceConstants.DEFAULT_FONT_SIZE;
      }
      setFontSize(fontSize);
      fullRedraw = true;
    } else if (PreferenceConstants.SCROLLBACK.equals(key)) {
      String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null);
      if (string != null) {
        scrollback = Integer.parseInt(string);
      } else {
        scrollback = PreferenceConstants.DEFAULT_SCROLLBACK;
      }
      buffer.setBufferSize(scrollback);
    } else if (PreferenceConstants.COLOR_FG.equals(key)) {
      mDefaultFgColor =
          manager.getIntParameter(PreferenceConstants.COLOR_FG,
              PreferenceConstants.DEFAULT_FG_COLOR);
      fullRedraw = true;
    } else if (PreferenceConstants.COLOR_BG.equals(key)) {
      mDefaultBgColor =
          manager.getIntParameter(PreferenceConstants.COLOR_BG,
              PreferenceConstants.DEFAULT_BG_COLOR);
      fullRedraw = true;
    }
    if (PreferenceConstants.DELKEY.equals(key)) {
      delKey =
          manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL);
      if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) {
        ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE);
      } else {
        ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL);
      }
    }
  }
}