package nxm.sys.libg;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

import javax.swing.JPanel;

/** A class to handle a text area (SWING) using a NeXtMidas theme.
    This can be used to implement Area, List, or Tree functions.
    Since it is often used as a long menu, it is one-based. Line 0 is the input.<br>
    @since NeXtMidas 2.7.1
    @author  Jeff Schoen / Jim Bauer
 */
@SuppressWarnings("serial")
public class MJText extends JPanel
  implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener, FocusListener {

  /**
   * comma separated list of possible flags
   */
  public static final String flagsList =
    "Multiple,Fifo,Tree,Columns,NoSelect,NoHilite,BackLite,Input,Text,Wrap,Color,Banner,AddPlus,Logger";
  /** Multiple flag*/
  public static final int MULT    =    0x1;
  /** FIFO flag*/
  public static final int FIFO    =    0x2;
  /** Tree flag*/
  public static final int TREE    =    0x4;
  /** Columns flag*/
  public static final int COLS    =    0x8;
  /** No Select flag*/
  public static final int NOSEL   =   0x10;
  /** No Highlight flag*/
  public static final int NOHIL   =   0x20;
  /** Backlight flag*/
  public static final int BACKL   =   0x40;
  /** Input flag*/
  public static final int INPUT   =   0x80;
  /** Text flag*/
  public static final int TEXT    =  0x100;
  /** Wrap flag*/
  public static final int WRAP    =  0x200;
  /** Color flag*/
  public static final int COLOR   =  0x400;
  /** Banner flag*/
  public static final int BANNER  =  0x800;
  /** Add '+ at end of wrapped line flag*/
  public static final int ADDPLUS = 0x1000;  // Show the '+' ad the end of a wrapped line (BUG 1563)
  /** indicates the MJText is from MSGLOG */
  public static final int LOGGER  = 0x2000;  // indicates the MJText is from MSGLOG (BUG 1563)
  /** indicates the MJText is from SHELLGUI */
  public static final int SHELLGUI = 0x4000;

  /** flags */
  int flags;
  /** font */
  Font font;
  /** Theme */
  Theme theme;
  /** Number of rows that can be displayed */
  int rows;
  /** Total rows available for display on the screen */
  int screenRows;
  /** Number of columns (i.e number of characters) that can be displayed */
  int cols;
  /** Contains the output data
      The first element always contains the prompt text
      The last element always contains blank  ""   */
  String[] line;
  /** Total number of output data elements in the "line" String Array.
      This number does not account for the 0 element which represents the
      prompt    */
  int items;
  /** Maximum number of characters for any element contained in lines[]*/
  int maxlen=0;
  /** Text Width */
  int tw=10;
  /** Text Height */
  int th=20;
  /** Text Ascent */
  int ta=10;
  /** Banner Height */
  int bh=0;
  /** first line selected */
  int selecti1=-1;
  /** last line selected */
  int selecti2=-1;
  /** first character selected */
  int selectc1=-1;
  /** last character selected */
  int selectc2=-1;
  /** first line selected - used during updating main value*/
  int selecti1_;
  /** last line selected - used during updating main value*/
  int selecti2_;
  /** first character selected - used during updating main value*/
  int selectc1_;
  /** last character selected - used during updating main value*/
  int selectc2_;
  /** Row number of the input prompt (0 based)*/
  int inrow;
  /** Number of rows of data that can be displayed on screen (i.e. rows available
      after banner and input prompt (if applicable) */
  int nRowAvail;
  /** Scroll Index. Index immediately preceding index of data displayed on top
      of screen (i.e. index of item just scrolled off the screen)*/
  int scrollIndex=0;
  /** Scroll Row. Row immediately preceding row of data displayed on top
      of screen (i.e. row of item just scrolled off the screen).  This value
      is the same as si when there is only one column of data. */
  int scrollRow=0;
  /** Used only for calculating scrollBarWidth */
  int sv=5;
  /** Width of scroll bar */
  int scrollBarWidth=sv*3;
  /** Indicator if window should contain a scroll bar */
  boolean hasScroll;
  /** Indicator if scroll bar is being dragged */
  boolean dragScroll;
  /** Indicator if scroll bar is selected */
  boolean dragSelect;
  /** Action Listener */
  ActionListener actionListener;
  /** refresh input */
  boolean refreshI;
  /** refresh output */
  boolean refreshO;
  /** Row to Item mapping - some items wrap over multiple display rows */
  int[] row2line;
  /** The banner text */
  String banner;
  /** The prompt text */
  String prompt;
  /** where all the input data is stored */
  String input;
  /** cursor location */
  int cursor=-1;
  /** line/item selected, where 0 is prompt and 1 is first text line */
  int posi;
  /** first character selected (or end of line) */
  int posc;
  /** Number of characters per wrap */
  int wraplen;
  /** MWindow */
  MWindow MW;

  /** Constructor for generic text list display
      @param lines The number of text lines to setup for.
      @param rows The number of rows to initially display.
      @param cols The number of columns to initially display.
      @param fontsize The fontsize in pixels to use
      @param theme The color theme to use
      @param flags The options to enable
   */
  public MJText (int lines, int rows, int cols, int fontsize, Theme theme, int flags) {
    setFlags(flags);
    setFontSize(fontsize);
    setTheme(theme);
    setSize (cols*tw,rows*th);
    setLines(lines);
    addMouseListener(this);
    addMouseMotionListener(this);
    addKeyListener(this);
    addFocusListener(this);
    addMouseWheelListener(this);
  }

  /** add ActionListener
   *  @param obj ActionListener to add
   */
  public void addActionListener (ActionListener obj) {
    actionListener = obj;
  }

  private void refreshI() {
    refreshI = true;

    // JWB - The line below was changed to eliminate glitches when using double-buffering
    if (isVisible()) repaint();
  }

  /** enable refresh output */
  public void refreshO() {
    refreshO = true;

    // JWB - The line below was changed to eliminate glitches when using double-buffering
    if (isVisible()) repaint();
  }

  /** set MWindow
   * @param mw MWindow associated with this MJText
   */
  public void setMWindow (MWindow mw) {
    MW = mw;
    theme = MW.theme;
  }

  /** set Font size
   *  @param size font size
   */
  public void setFontSize (int size) {
    if (font != null && font.getSize() == size) return; // since 3.7.0, skip if size is same as existing as part Bug 2948
    font = new Font ("Monospaced",Font.PLAIN,size);
    FontMetrics fm = getFontMetrics(font);
    tw = fm.stringWidth("0");
    th = fm.getHeight();
    ta = fm.getAscent();
  }

  /**
     Initialize a String array resize if it exists
     The String array line contains all know lines of text
     @param lines The size of the array
   */
  public void setLines (int lines) {
    String[] oline = line;
    line = new String[lines+1];
    if (oline != null) for (int i=0; i<=items; i++) line[i]=oline[i];
    items = 0;
    maxlen = 0;
  }

  /** set prompt
   *  @param text prompt text
   */
  public void setPrompt (String text) {
    if (prompt == text || (text != null && text.equals(prompt))) return; // since 3.7.0, skip since text is same as prompt as part of Bug 2948
    prompt = text;
    refreshI();
  }

  /** set input
   *  @param text input text
   */
  public void setInput (String text) {
    if (input == text || (text != null && text.equals(input))) return; // since 3.7.0, skip since text is same as prompt as part of Bug 2948
    input = text;
    cursor = text.length();
    refreshI();
  }

  /** get input text
   *  @return input text
   */
  public String getInput() {
    return input;
  }

  /** set banner text
   *  @param text banner text
   */
  public void setBanner (String text) {
    if (banner == text || (text != null && text.equals(banner))) return; // since 3.7.0, skip since text is same as banner as part of Bug 2948
    banner = text;
    refreshO();
  }

  /** get banner text
   *  @return banner text
   */
  public String getBanner() {
    return banner;
  }

  /**
    Add a line of text to the text area
    @param text String of text to add to text area
   */
  public void addText (String text) {
    int lf = text.indexOf('\n');
    if (items==0) line[++items]="";
    while (lf>=0) {
      line[items] += text.substring(0,lf);
      add("");
      text = text.substring(lf+1);
      lf = text.indexOf('\n');
    }
    line[items] += text;
    refreshO();
  }

  /** add text
   *  @param text text to add
   */
  public void add (String text) {
    boolean fifo = (flags&FIFO) != 0;
    boolean full = items+1 >= line.length;
    if (full && fifo) {        // shift data
      for (int i=0; i<items; i++) line[i] = line[i+1];
      setLine(items,text);
      return;
    }
    if (full) setLines(line.length*2);    // expand array

    setLine(++items,text);
  }

  /** Determine number of wrapped lines. Note: This does not account for line[0]
      which represents the prompt text
      This is a slow way for proof of concept.
      This could be maintained in sync with "items" and recalculated if screen size is changed.
   */
  private int calcNumItemsWithWrap(int startIndex) {
    int numRowsWithWrap=0;
    // If we are not displaying "wrapped text", then 0 wraps - since 3.9.1 for NXM-3337
    if ((flags&WRAP) == 0) return 0;
    for (int i=startIndex;i<=items;i++) { // since 3.9.1 include i=items
      // Add to total any additional wrapped lines
      numRowsWithWrap = numRowsWithWrap + (getNbrWraps(line[i]) - 1 );
    }
    return numRowsWithWrap;
  }

  /** Determine how many display rows would be occupied by wrapped text
      @param text String to determine how many rows it would occupy
      @return number of displayed row occupied by a line of text
    */
  public int getNbrWraps(String text) {
    if (text==null || text.length() == 0 || (flags&WRAP)==0 ) return 1; // add no wrap case in 3.9.1 NXM-3337
    return (int)Math.ceil((float)text.length() / calcNumCharsWidth());
  }

  /** Return the number of display characters that should fit in width of column */
  private int calcNumCharsWidth() {
    return Math.max(2,calcColWidth()/tw-1);
  }

  /**
     Calculate Column Width

     Calculate column width field by subtracting the scroll bar width, if it exists,
     from the total width available and then dividing by the # of columns
     @return column width
     @since 3.5.3
   */
  private int calcColWidth(){
    return (getWidth() - (isScroll()?scrollBarWidth:0)) / calcNumSplitScreenColumns();
  }

  /** Determine the number of split screens based on
   *  overall width and number of items to render
   */
  private int calcNumSplitScreenColumns() {
    int numberOfRows = Math.max(1,rows);              // number of rows
    int numSplitScreen = Math.max(1,Math.min((items-1)/numberOfRows+1,cols/(maxlen+2)));
    if ((flags&COLS)==0) numSplitScreen=1;
    return numSplitScreen;
  }

  /** Calculate the number of rows needed for the prompt. */
  private int calcPromptRows() {
    // If logger, no rows needed for prompt
    if ((flags&LOGGER)!=0 ) {
      return 0;
    } else {
      return getNbrWraps(line[0]);
    }
  }

  /** Get line at specified index
   *  @param i index to get line at
   *  @return line at specified index, null if index is out of bounds
   */
  public String getLine (int i) {
    if (i<0 || i>items) return null;
    return line[i];
  }

  /** replace text
   *  @param text replacement text for last line
   */
  public void replace (String text) {
    setLine(items,text);
  }

  /** set line text
   *
   *  @param i    line
   *  @param text text
   */
  public void setLine (int i, String text) {
    if (i<0 || i>items) return;
    line[i] = text;
    maxlen = Math.max(maxlen,text.length());
    if ((flags&(LOGGER|SHELLGUI))!=0) resetScreenIndex();
    refreshO();
  }

  @Override
  public void removeAll() {
    for (int i=1; i<=items; i++) line[i]=null;
    items = scrollIndex = scrollRow = maxlen = 0;
    selecti1 = selecti2 = selectc1 = selectc2 = -1;
    refreshO();
  }

  /** set Flags
   * @param flags int flags
   */
  public void setFlags (int flags) {
    this.flags = flags;
    refreshO();
  }

  /** get Flags
   *  @return flags as int
   */
  public int getFlags() {
    return flags;
  }

  /** set Theme
   *  @param theme The MJText Theme
   */
  public void setTheme (Theme theme) {
    this.theme = theme;

    // Make any color adjustments for GUI components that may not be
    // visible due to poor color combinations.  Bug 2797
    this.theme.adjustThemeColorVisibilityIssues(null,true);

    repaint();
  }

  /** revalidate and repaint */
  public void repaint() {
    // Running revalidate to make sure that all the components are accounted for.
    // This should be good here as it is the only repaint that is overridden
    // Since 3.9.0 NXM-3213
    revalidate();
    super.repaint();
  }

  /** select an item
   *  @param item item to select
   */
  public void select (int item) {
    select (item,-1);
  }

  /** select at item and offset
   *  @param item item
   *  @param ich  offset
   */
  public void select (int item, int ich) {
    if ((flags&NOSEL)!=0) return;
    if (item<0 || item>items) return;
    String text = line[item];
    int len = text.length();
    if (item==selecti1 && (ich<0 || ich>=len || (ich>=selectc1 && ich<selectc2))) {
      if (selectc1==0 && selectc2==len) { selecti1=selecti2=-1; }  // deselect
      else { selectc1=0; selectc2=len; }  // select entire line
    }
    else {
      selecti1=selecti2=item;
      selectc1=0; selectc2=len;
      // check for word select
      if ((flags&TEXT) != 0 && ich>=0 && ich<len && text.charAt(ich)>' ') {
        for (selectc1=ich; selectc1>0 && text.charAt(selectc1-1)>' '; selectc1--);
        for (selectc2=ich; selectc2<len && text.charAt(selectc2)>' '; selectc2++);
      }
    }
    if (actionListener != null) {
      ActionEvent e = new ActionEvent(this,0,"SELECT",selecti1);
      actionListener.actionPerformed(e);
    }
    copy(1);
    refreshO();
  }

  /** Copy the selected text to the system clipboard */
  public void copy() { copy(0); }

  /** Copy selected text to either selection buffer (when mode = 1) or the
      system clipboard buffer (when mode != 0).
      @param mode set to 1 to copy to selection buffer (X-Windows), otherwise copy to system clipboard.
   */
  private void copy (int mode) {
    if ((flags&TEXT)==0);
    else if (mode==1) GraphicsUtil.toSystemSelection(getSelectedItem());
    else GraphicsUtil.toSystemClipboard(getSelectedItem());
  }

  /** deselect an item
   *  @param item item to deselect
   */
  public void deselect (int item) {
    selecti1 = selecti2 = -1;
    refreshO();
  }

  /** get selected item
   *  @return selected item as String
   */
  public String getSelectedItem() {
    String s=null;
    if (selecti1<0);
    else if (selecti1==selecti2) s=line[selecti1].substring(selectc1,selectc2);
    else for (int i=selecti1; i<=selecti2; i++) {
           if (i==selecti1) s = line[i].substring(selectc1) + "\n";
      else if (i==selecti2) s += line[i].substring(0,selectc2);
      else s += line[i] + "\n";
    }
    return s;
  }

  /** get selected items - NOT IMPLEMENTED
   * @return null
   */
  public String[] getSelectedItems() {
    return null;
  }

  /** Get the number of visible rows
      @return # rows visible
   */
  public int getRows() { return rows; }

  /** Get the number of visible columns
      @return # cols visible
   */
  public int getColumns() { return cols; }

  @Override
  public void update (Graphics g) {
    paintComponent(g);
  }

  @Override
  public void paintComponent (Graphics g) {
    if (g != null && (Theme.getOption(Theme.SWINGDB))) super.paintComponent(g); // Swing Double-buffering requires full repaint
    bh = ((flags&BANNER) != 0)? th+6 : 0;  // Banner height
    boolean isInput = (flags&INPUT) != 0;  // there is an input prompt displayed
    boolean inputOnly = isInput && refreshI && !refreshO; // text is being entered at input prompt
    refreshI = false;
    refreshO = false;


    int w = getWidth(); // get the component/screen width
    boolean numColsChanged = updateCols(w); // updated in 3.6.0 to see if the number of columns changed - bug 2948
    boolean numRowsChanged = updateAvailableScreenRows(); // updated in 3.6.0 to see if the number of rows changed - bug 2948
    nRowAvail = Math.max(1,rows); // number of rows available for text (excludes banner & input row)

    int numSplitScreensCols = calcNumSplitScreenColumns(); // get the number of columns

    setHasScroll();
    if (((flags&(LOGGER|SHELLGUI))!=0) && (numRowsChanged || numColsChanged)) resetScreenIndex(); // MJText resized, so first text line need updating since 3.6.0 for bug 2948
    if (!isScroll()) scrollIndex=scrollRow=0;
    // first index/row displayed)
    if (scrollIndex>0) { scrollRow=scrollIndex/numSplitScreensCols; scrollIndex=scrollRow*numSplitScreensCols; }

    int colWidth = calcColWidth(); // get column width
    int alen = calcNumCharsWidth();   // The width (in characters) of the display window
    wraplen = alen-2;                // how many chars to draw when wrapping
    if (g==null) return; // just calculating rows for setScroll
    g.setFont(font);
    if (inputOnly) { // input only
      paintInput(g); // paint input line
      // Done if the width and height of the MJText has not changed
      // and if graphics are not set to SWINGDB - updated for bug 2948 in 3.6.0
      if (!Theme.getOption(Theme.SWINGDB) && !numRowsChanged && !numColsChanged) return;
    }
    g.setColor(theme.cwbg);
    g.drawLine(0,1,w-1,1);
    g.drawLine(0,2,w-1,2);
    if (isScroll()) paintScroll(g);     // scroll bar
    if (bh>0) paintBanner(g);       // banner

    // loop through text lines
    if (row2line==null || row2line.length!=nRowAvail) row2line = new int[nRowAvail];
    int iScreenRow=0;  // indexes through displayed rows
    int wrap=0;
    // Display items that are to be displayed (those not scrolled back off the screen)

    int itemIndex = scrollIndex + 1; // moved here in 3.9.1

    // If the newest item wraps enough times that it does not fully fit,
    // at least display the lines that do fit - since 3.9.1 for NXM-3337
    int numWrapsNewest = getNbrWraps(line[items]);
    if (scrollIndex > 0 && numWrapsNewest > nRowAvail) {
      itemIndex--;
      wrap = (numWrapsNewest- nRowAvail)*alen;
    }

    for (int iCol=0; itemIndex<=items; itemIndex++) {
      int x = iCol*colWidth;
      int y = calcRowY(iScreenRow);
      paintLine (g,itemIndex,x,y,alen,wrap);
      // Assign an item to each line. If the item wraps, it will be assigned to
      // multiple lines
      if (iCol==0) row2line[iScreenRow] = itemIndex;
      // if we have cycled through all the columns, then increment the
      // row index
      if (++iCol >= numSplitScreensCols) { iCol=0; iScreenRow++; }
      // If we have reached the number of rows available for text, stop
      if (iScreenRow == nRowAvail) break;
      // if wrapping is allowed and the portion of this line, that has not
      // already been wrapped, is still longer than the allowed length, than
      // wrap again
      if ((flags&WRAP)!=0 && line[itemIndex]!=null && wrap+alen<line[itemIndex].length()) {
        wrap += wraplen; itemIndex--;
      }
      else wrap=0;
    }
    // Blank fill (erase) space below last text line
    //   iScreenRow     is pointing to line after last text line (0 based index)
    //   + th           switch to 1 based to get y of first row to be erased
    //   + bh           adjust down for banner (bh=0 if there is no banner)
    //   - ta           adjust up to get to top of text area for this first blank row
    int y = calcRowY(iScreenRow) - ta;
    g.setColor(theme.cwbg);      // fill space
    g.fillRect(0,y,w-1-(isScroll()?scrollBarWidth:0),getHeight()-y);

    updateInputRowIndex(); // Set the screen row index of the input prompt
    if (isInput) paintInput(g);
    // frame border
    g.setColor(theme.cwms);
    g.drawLine(1,0,w-3,0);
    g.drawLine(1,getHeight()-2,w-3,getHeight()-2);
    // column dividers
    for (int i=1; i<numSplitScreensCols; i++) g.drawLine(i*colWidth,bh+tw,i*colWidth,getHeight()-tw-1);
    // place holders
    for (;iScreenRow<row2line.length;iScreenRow++) row2line[iScreenRow]=-1;
  }

  /** Set the screen row index of the input prompt.
      corrected in 3.5.3 Bug 2872
   */
  private void updateInputRowIndex() {
    if (isScroll()) {
      // Index of input row is the last available position accounting for
      // more then one row in the prompt. Since this is a 0 based index, subtract
      // 1 to convert from the Nth row to the row index of the Nth row.
        inrow = screenRows - calcPromptRows();
    } else {
      // Index of input row is the same as the number of rows taken up by items,
      // since we have a 0 based index
      inrow = items + calcNumItemsWithWrap(1);
    }
  }

  /** Set Number of Columns (number of characters that can be displayed)
      @param w width
      @return true of value of cols changed  - since 3.6.0 for bug 2948
   */
  private boolean updateCols (int w) {
    int origCols = cols;
    cols = Math.max(0, (w-scrollBarWidth) / tw);
    return (cols != origCols);
  }

  /** Calculate y location of the given row
      @param lineIndex 0 based index of row on screen/component
      @return y for text line (top of text line = y + text_ascent, bottom = y - text_descent)
      @since 3.5.3  Bug 2872
   */
  private int calcRowY (int lineIndex){
    // switch from 0 based index to row # by adding 1, and then add banner height
    return (lineIndex+1)*th + bh;
  }

  /** Determine number of rows available for display on the screen. Calculation factors in banner.
   *  @return true of value of rows changed - since 3.6.0 for bug 2948
   */
  private boolean updateAvailableScreenRows() {
    int origRows = rows;
    boolean isInput = (flags&INPUT) != 0;
    // Since painting of text stretches from y location to text assent (ta) above
    // and down to text descent below (th -ta) and the y of the first line of text
    // is th, there is a text descent margin (th-ta) at the top of the text area
    screenRows = rows = Math.max(0, (getHeight()-bh -(th-ta)) / th);
    if (isInput) rows = Math.max(0, rows -= calcPromptRows());
    return (origRows != rows); // has rows been updated to a new number
  }

  /** Calculate Maximum (Total) Rows of Text Available for Display
      @return max/total number of rows of text available
   */
  private int calcMaxRowsText() {
    return Math.max(0,(items-1)/calcNumSplitScreenColumns())+1;
  }

  /** Paint a line of text
      @param g The Graphics context
      @param i The line number to draw
      @param x The X Position
      @param y The Y Position (text to stretch from text ascent above y to text descent below)
      @param alen The width of the display window
      @param off Offset. 0 unless this is a wrapped line.
   */
  private void paintLine (Graphics g, int i, int x, int y, int alen, int off) {
    if (alen <= 0) return;   // can not display line if display window width is 0 - since 3.6.0 for bug 2948
    String text = line[i];   // get the text string for that line
    g.setColor(theme.cwbg);
    g.fillRect(x,y-ta,calcColWidth()-1,th);
    if (text==null) return;
    x += tw;

    // unless disabled, color the text of certain lines
    Color c = theme.cwfg;
    if ((flags&COLOR)==0);
    else if (text.startsWith("INFO:"))  c = Color.green;
    else if (text.startsWith("WARN:"))  c = Color.yellow;
    else if (text.startsWith("ERROR:")) c = Color.red;

    if (off>0) text = text.substring(off);    // if this is the continuation of a previous line
    int tl = text.length();
    if (tl>alen) {                            // if length of line is longer than width of screen
      tl=alen-2;                              // go back 2 chars and then get the substring up to that point
      if (tl > 0) {
        text = text.substring(0,tl);
      }
      else {                                  // width anything but the + for continuation - since 3.6.0 for bug 2948
        text = "";
      }
      if ((flags&ADDPLUS)!=0) text += " +";   // add a plus sign unless it is turned off
    }

    // selected / highlighted text
    if (i>=selecti1 && i<=selecti2) {
      int c1 = Math.max(0,Math.min(tl,(i==selecti1)? selectc1-off : 0));
      int c2 = Math.max(0,Math.min(tl,(i==selecti2)? selectc2-off : tl));
      if ((flags&BACKL)!=0) {   // backlit selected text
        g.setColor(theme.cwfh);
        g.fillRect(x+c1*tw,y-ta,(c2-c1)*tw,th);
        g.setColor(c);
        g.drawString(text,x,y);
      } else {
        g.setColor(c);
        g.drawString(text,x,y);
        g.setColor(theme.cwfh);
        g.drawString(text.substring(c1,c2),x+c1*tw,y);
      }
    }
    else {
      if (i==0) { // input prompt
        int pl = Math.min(Math.min(alen,prompt.length()), text.length());
        g.setColor(theme.cwfh);
        g.drawString(text.substring(0,pl),x,y);
        text = text.substring(pl); x += tw*pl;
      }
      g.setColor(c);
      g.drawString(text,x,y);  // draw the text
    }
  }

  private void paintBanner (Graphics g) {
    int x = tw;
    int y = th;
    int w = getWidth() - (isScroll()?scrollBarWidth-2:2);
    int h = bh;
    if (banner==null) banner = "Null Banner";
    g.setColor(theme.cwms);
    g.fillRect(2,0,w-2,h);
    g.setColor(theme.cwfh);
    g.drawString(banner,x,y);
  }

  private void paintInput (Graphics g) {
    line[0] = prompt;
    if (input != null) line[0] += input;
    if (line[0]==null) return;
    int lp = prompt.length();
    int x = 0;
    int y = calcRowY(inrow);
    int w = getWidth() - x - tw - (isScroll()?scrollBarWidth:4);
    int alen = (w-2)/tw - 1;
    paintLine (g,0,x,y,alen,0);
    if (!hasFocus()) return;
    x += (cursor+lp+1)*tw;
    g.setColor(theme.cwfh);
    if (x<w) g.drawLine(x,y-2*th/3,x,y);
    g.setColor(theme.cwfg);
  }

  private void paintScroll (Graphics g) {
    int x=getWidth()-scrollBarWidth/2-2, y=0, w=scrollBarWidth, h=getHeight()-2;
    double f=(double)scrollRow/(calcMaxRowsText()-nRowAvail);
    int flags = MWindow.SLIDER_INSET | MWindow.SLIDER_NOTRACK |
                (dragScroll? MWindow.SLIDER_ACTIVE : 0);
    MWindow.drawVSlider(x,y,w,h, f,flags, g,theme);
  }

  /** set scroll index
   *  @param index index of scroll (negative means display end)
   */
  public void setScroll (int index) {
    paintComponent((Graphics)null); // just calculate rows
    if (index<0) index = Math.max(0,items-rows);
    else if (index>=items) index = items-1;
    scrollIndex = scrollRow = index;
    refreshO();
  }

  private boolean updateScroll (int y) {
    dragScroll = true;
    int h = getHeight()-scrollBarWidth-6;
    if (h<th) return false;
    int r = (y-scrollBarWidth/2-3)*calcMaxRowsText()/h;
    int i = r*calcNumSplitScreenColumns();
    if (i<0 || i>=items) return false;
    scrollIndex = i;
    scrollRow = r;
    return true;
  }

  /** Determine if the window should contain a scroll bar. */
  private void setHasScroll() {
    hasScroll = items+calcNumItemsWithWrap(1) > calcNumSplitScreenColumns()*nRowAvail;
  }

  /** Should the screen contain a scroll bar?
      @return true to use scroll bar, false otherwise
   */
  public boolean isScroll() {
    return hasScroll;
  }

  /** Set scrollIndex and scrollRow. This is one less then the start of the line[] String array that
      should be displayed on the screen.
  */
  private void resetScreenIndex()  {
    scrollIndex=scrollRow=calcBeginLineIndex(true)-1;
  }

  /** Calculate the index of the line[] array that should be displayed at the top of the screen. */
  private int calcBeginLineIndex (boolean includeWraps) {

    int numDispRowsForData = screenRows-calcPromptRows();
    int beginLineIdx = Math.max(0,items-numDispRowsForData)+1;
    if ( includeWraps) {
      int wrapLines = 0; // Lines including wrap
      int count=items+1;
      for (int idx=items;idx>=beginLineIdx;idx--) {
        wrapLines += getNbrWraps(line[idx]);
        if (wrapLines> numDispRowsForData) {
          break;
        }
        count--;
      }
      beginLineIdx=count;
    }
    return Math.max(1,beginLineIdx);
  }

  @Override
  public void keyPressed (KeyEvent e) {
    if ((flags&INPUT)==0) return;
    int keycode = e.getKeyCode();
    char keychar = e.getKeyChar();
    boolean cc = e.isControlDown();
    String action = null;
    int modifier = 0;

    if (keycode == KeyEvent.VK_ENTER) {
      action = "ENTER";
      if ((flags&(LOGGER|SHELLGUI))!=0) resetScreenIndex();
    }
    else if (keycode == KeyEvent.VK_BACK_SPACE && cursor>0) {
      input = input.substring(0,cursor-1) + input.substring(cursor);
      cursor--;
      refreshI();
    }
    else if (keycode == KeyEvent.VK_DELETE && selecti1==0) {
      int i1 = Math.max(0,selectc1-prompt.length());
      int i2 = Math.max(0,selectc2-prompt.length());
      input = input.substring(0,i1) + input.substring(i2);
      selecti1 = selecti2 = -1;
      cursor = i1;
      refreshI();
    }
    else if (keycode == KeyEvent.VK_DELETE && cursor<input.length()) {
      input = input.substring(0,cursor) + input.substring(cursor+1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_RIGHT || (cc && keycode == KeyEvent.VK_F)) {  // right arrow
      cursor = Math.min(input.length(),cursor+1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_LEFT) {  // left arrow
      cursor = Math.max(0,cursor-1);
      refreshI();
    }
    else if (keycode == KeyEvent.VK_UP) {  // up arrow
      modifier = e.isShiftDown()? -2 : -1;
      action = "POPCODE";
    }
    else if (keycode == KeyEvent.VK_DOWN) {  // down arrow
      modifier = e.isShiftDown()? 2 : 1;
      action = "POPCODE";
    }
    else if ((keycode == KeyEvent.VK_END) || (cc && keycode == KeyEvent.VK_E)) {  // end of line
      cursor = input.length();
      refreshI();
    }
    else if ((keycode == KeyEvent.VK_HOME) || (cc && (keycode == KeyEvent.VK_A || keycode == KeyEvent.VK_H))) {  // beginning of line
      cursor = 0;
      refreshI();
    }
    else if (cc && keycode == KeyEvent.VK_U) {  // delete line
      setInput("");
    }
    else if (cc && keycode == KeyEvent.VK_D) {  // interrupt
      action = "INTERRUPT";
    }
    else if (cc && keycode == KeyEvent.VK_P) {  // pop
      if (MW == null) action = "POP"; // JWB - 27 Aug 09: This is usually caught by MWindow.keyPressed
    }
    else if (cc && keycode == KeyEvent.VK_C) {  // copy
      String selectedText = getSelectedItem();
      if (selectedText==null || selectedText.length() <= 0 ) {
        // print info message to user to use CTRL-D instead of CTRL-C for BUG 1109
        addText("INFO: Use Ctrl-D=Interrupt, Ctrl-C=CopySelectedText\n"); // BUG 2218: always display message in this MJText/SHELLGUI terminal
      }
      else copy();
    }
    else if (cc && keycode == KeyEvent.VK_V) {  // paste
      paste();
    }
    else if (keychar>=' ' && keychar<='~' && !cc) {
      // Since 3.9.2 for support of webswing - in cases where the combination of keys should represent something different.
      if (keychar=='\\'&&e.isShiftDown()) keychar='|';
      else if (keychar=='-'&&e.isShiftDown()) keychar='_';
      input = input.substring(0,cursor) + keychar + input.substring(cursor);
      cursor++;
      if (cursor==1) action = "KEYPRESS";
      refreshI();
    }
    if (actionListener != null && action != null) {
      ActionEvent ae = new ActionEvent(this,0,action,modifier);
      actionListener.actionPerformed(ae);
    }
  }

  /** add input
   *  @param text additional text to add
   */
  public void addInput (String text) {
    input = input.substring(0,cursor) + text + input.substring(cursor);
    cursor += text.length();
    refreshI();
  }

  @Override
  public void keyReleased (KeyEvent e) {
  }

  @Override
  public void keyTyped (KeyEvent e) {
  }

  @Override
  public void mouseEntered (MouseEvent e) {
  }

  @Override
  public void mouseExited (MouseEvent e) {
  }

  @Override
  public void focusGained (FocusEvent e) {
    if (!Theme.getOption(Theme.SWINGPOPUPS)) refreshI(); // Removing in 3.8.1 because it repaints the text area over the popup menu.
  }

  @Override
  public void focusLost (FocusEvent e) {
    if (!Theme.getOption(Theme.SWINGPOPUPS)) refreshI(); // Removing in 3.8.1 because it repaints the text area over the popup menu.
  }

  private boolean hit (int x, int y) {
    int colWidth = calcColWidth(); // get Column Width
    int ic = Math.max(0,Math.min(calcNumSplitScreenColumns()-1, (x-tw)/colWidth ));
    int ir = (y-bh)/th; // index of row selected (0 based index)
    int c  = (x-tw-ic*colWidth)/tw; //index of first character selected (0 based index)
    int i = -1; // default to invalid value
    posi = posc = i; // default to invalid value
    if ((flags&INPUT)!=0 && ir==inrow && c<line[0].length()) i=0;
    // add row2line[ir] >=0 because if it is less than 0 we can consider that row to be blank. Since 3.9.1
    else if ((ir>=0) && (ir<nRowAvail) && (row2line[ir]>=0)) i = row2line[ir] + ic;
    // no hit if not in range of lines of text or input prompt
    if (i<0 || i>items) return false;
    // Do not check for an item (original line) having been broken into
    // multiple display lines if we are at the input prompt or this is the first
    // item/text line
    for (;i>0 && ir>0 && row2line[ir-1]==row2line[ir]; ir--) c+=wraplen;
    posi = i; // line/item selected, where 0 is prompt & 1 is first text line
    posc = Math.max(0,Math.min(line[i].length(), c)); // first character selected (or end of line)
    return true;
  }

  private void startSelect (int x, int y) {
    if (hit(x,y)) select(posi,posc);
  }

  private boolean updateSelect (int x, int y) {
    boolean paint = false;
    hit(x,y);
    if (posi<0 || posi>items || line[posi]==null) return paint;
    if (dragSelect) {
      paint = (posi!=selecti2_ || posc!=selectc2_);
      selecti2_ = posi;
      selectc2_ = posc;
    } else {
      selecti1_ = selecti2_ = posi;
      selectc1_ = selectc2_ = posc;
      dragSelect = true;
    }
    if (selecti2_<selecti1_ || (selecti2_==selecti1_ && selectc2_<=selectc1_)) {
      selecti1 = selecti2_; selectc1 = selectc2_;
      selecti2 = selecti1_; selectc2 = selectc1_;
    } else {
      selecti1 = selecti1_; selectc1 = selectc1_;
      selecti2 = selecti2_; selectc2 = selectc2_;
    }
    if (paint) copy(1);
    return paint;
  }

  @Override
  public void mousePressed (MouseEvent e) {
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    int w = getWidth();
    if (event2button(e)!=1) return;
    if (isScroll() && x>w-scrollBarWidth) {
      if (updateScroll(y)) repaint();
    }
  }

  @Override
  public void mouseReleased (MouseEvent e) {
    if (dragScroll) { dragScroll=false; repaint(); }
    dragSelect = false;
  }

  @Override
  public void mouseClicked (MouseEvent e) {
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    int button = event2button(e);
    if (button==1) {
      if (y<bh) {
        ActionEvent ae = new ActionEvent(this,0,"BANNER",button);
        if (actionListener!=null) actionListener.actionPerformed(ae);
      }
      else if (x<calcColWidth()*calcNumSplitScreenColumns()) {
        startSelect (x,y);
      }
    }
    else if (button==2 && (flags&INPUT)!=0) {
      paste(1);
    }
    else {
      ActionEvent ae = new ActionEvent(this,0,"BUTTON",button);
      if (actionListener!=null) actionListener.actionPerformed(ae);
    }
  }

  /** paste from clipboard */
  public void paste() { paste(0); }

  /** paste
   * @param mode 1 paste text for system selection, other from clipboard
   */
  private void paste (int mode) {
    if ((flags&INPUT)==0) return;
    String text = (mode==1)? GraphicsUtil.fromSystemSelection()
                           : GraphicsUtil.fromSystemClipboard();
    if (text != null) addInput(text);
  }

  @Override
  public void mouseMoved (MouseEvent e) {
    if (MW==null) return;
    Rectangle pos = getBounds();
    MW.px = pos.x + e.getPoint().x;
    MW.py = pos.y + e.getPoint().y;
  }

  private void mouseMovedAction() {
  }

  @Override
  public void mouseDragged (MouseEvent e) {
    if (event2button(e) != 1) return;
    int x = e.getPoint().x;
    int y = e.getPoint().y;
    if (dragScroll) {
      if (updateScroll(y)) repaint();
    }
    else if ((flags&TEXT)!=0) {
      if (updateSelect(x,y)) repaint();
    }
  }

  /** Allow use of Mouse Scroll Wheel to scroll through lists
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseWheelMoved (MouseWheelEvent e) {
    if (e.getWheelRotation() > 0) {

      int beginLineIdx = calcBeginLineIndex(true);
      if (scrollIndex  < beginLineIdx-1 ) {
        if ( (scrollIndex-1) < beginLineIdx && (calcNumSplitScreenColumns() > 1)) {
          resetScreenIndex();
        } else {
          scrollIndex++;
        }
        repaint();
      }
    } else {
      if (scrollIndex> 0) { scrollIndex--; repaint(); } // scroll up
    }
  }

  /** Convert event to button number. */
  private int event2button (AWTEvent e) {
    MouseEvent me = (MouseEvent)e;
    int button = me.getButton(); // Updated in 3.9.0 to get the mouse button
    int modifiers = me.getModifiersEx(); // Updated in 3.9.0 to get the extended modifiers
    if(button == 0) { // lets try and get it from the extended modifiers, this will handle buttons that are still being pressed and not released
      if ((modifiers&MouseEvent.BUTTON1_DOWN_MASK)!=0 ) button = MouseEvent.BUTTON1;
      else if ((modifiers&MouseEvent.BUTTON2_DOWN_MASK)!=0 ) button = MouseEvent.BUTTON2;
      else if ((modifiers&MouseEvent.BUTTON3_DOWN_MASK)!=0 ) button = MouseEvent.BUTTON3;
      else button = MouseEvent.BUTTON1;
    }
    else if(button>MouseEvent.BUTTON3) button = MouseEvent.BUTTON1;
    if (me.isShiftDown()) button = button+100; // Updated in 3.9.0 to get the extended modifiers
    if (me.isControlDown()) button = button+200; // Updated in 3.9.0 to get the extended modifiers
    return button;
  }

}
