#ifdef _WIN32
#include <windows.h>
#else
#include "../swell/swell.h"
#endif
#include <stdlib.h>
#include <string.h>
#ifndef CURSES_INSTANCE
#define CURSES_INSTANCE ((win32CursesCtx*)m_cursesCtx)
#endif

#include "curses_editor.h"
#include "../wdlutf8.h"
#include "../win32_utf8.h"
#include "../wdlcstring.h"

#ifndef VALIDATE_TEXT_CHAR
#define VALIDATE_TEXT_CHAR(thischar) ((thischar) >= 0 && (thischar >= 128 || isspace(thischar) || isgraph(thischar)) && !(thischar >= KEY_DOWN && thischar <= KEY_F12))
#endif



#ifdef __APPLE__
#define CONTROL_KEY_NAME "Cmd"
#else
#define CONTROL_KEY_NAME "Ctrl"
#endif

WDL_FastString WDL_CursesEditor::s_fake_clipboard;
int WDL_CursesEditor::s_overwrite=0;
int WDL_CursesEditor::s_search_mode; // &1=case sensitive, &2=word. 5/6 = token matching

static WDL_FastString s_goto_line_buf;

static const char *searchmode_desc(int mode)
{
  if (mode == 4 || mode == 5) return (mode&1)  ? "TokenMatch":"tokenmatch";
  return (mode&2) ?
    ((mode&1) ? "WordMatch":"wordmatch") :
    ((mode&1) ? "SubString":"substring");
}


#ifndef WM_MOUSEWHEEL
#define WM_MOUSEWHEEL 0x20A
#endif

static void __curses_onresize(win32CursesCtx *ctx)
{
  WDL_CursesEditor *p = (WDL_CursesEditor *)ctx->user_data;
  if (p)
  {
    p->draw();
    p->setCursorIfVisible();
  }
}
WDL_CursesEditor::WDL_CursesEditor(void *cursesCtx)
{ 
  m_newline_mode=0;
  m_write_leading_tabs=0;
  m_max_undo_states = 500;
  m_indent_size=2;
  m_cursesCtx = cursesCtx;

  m_top_margin=1;
  m_bottom_margin=1;

  m_selecting=0;
  m_select_x1=m_select_y1=m_select_x2=m_select_y2=0;
  m_ui_state=UI_STATE_NORMAL;
  m_offs_x=0;
  m_curs_x=m_curs_y=0;
  m_want_x=-1;
  m_undoStack_pos=-1;
  m_clean_undopos=0;

  m_curpane=0;
  m_pane_div=1.0;
  m_paneoffs_y[0]=m_paneoffs_y[1]=0;

  m_curpane=0;
  m_scrollcap=0;
  m_scrollcap_yoffs=0;
  
  m_filelastmod=0;
  m_status_lastlen=0;

#ifdef WDL_IS_FAKE_CURSES
  if (m_cursesCtx)
  {
    CURSES_INSTANCE->user_data = this;
    CURSES_INSTANCE->onMouseMessage = _onMouseMessage;
    CURSES_INSTANCE->want_scrollbar=1; // 1 or 2 chars wide
    CURSES_INSTANCE->do_update = __curses_onresize;
  }
#endif

  initscr();
  cbreak();
  noecho();
  nonl();
  intrflush(stdscr,FALSE);
  keypad(stdscr,TRUE);
  nodelay(stdscr,TRUE);
  raw(); // disable ctrl+C etc. no way to kill if allow quit isn't defined, yay.
  start_color();

#ifdef WDL_IS_FAKE_CURSES
  if (!curses_win32_global_user_colortab && (!m_cursesCtx || !CURSES_INSTANCE->user_colortab))
#endif
  {
  init_pair(1, COLOR_WHITE, COLOR_BLUE);     // COLOR_BOTTOMLINE
  init_pair(2, COLOR_BLACK, COLOR_CYAN);     // COLOR_SELECTION
  init_pair(3, RGB(0,255,255),COLOR_BLACK);  // SYNTAX_HIGHLIGHT1
  init_pair(4, RGB(0,255,0),COLOR_BLACK);    // SYNTAX_HIGHLIGHT2
  init_pair(5, RGB(96,128,192),COLOR_BLACK); // SYNTAX_COMMENT
  init_pair(6, COLOR_WHITE, COLOR_RED);      // SYNTAX_ERROR
  init_pair(7, RGB(255,255,0), COLOR_BLACK); // SYNTAX_FUNC

#ifdef WDL_IS_FAKE_CURSES
  init_pair(8, RGB(255,128,128), COLOR_BLACK);  // SYNTAX_REGVAR
  init_pair(9, RGB(0,192,255), COLOR_BLACK);    // SYNTAX_KEYWORD
  init_pair(10, RGB(255,192,192), COLOR_BLACK); // SYNTAX_STRING
  init_pair(11, RGB(192,255,128), COLOR_BLACK); // SYNTAX_STRINGVAR
  init_pair(12, COLOR_BLACK, COLOR_CYAN);       // COLOR_MESSAGE (maps to COLOR_SELECTION)
  init_pair(13, COLOR_WHITE, COLOR_RED);        // COLOR_TOPLINE (maps to SYNTAX_ERROR)
  init_pair(14, RGB(192,192,0), COLOR_BLACK);   // SYNTAX_FUNC2
#endif
  }

  erase();
  refresh();
}

int  WDL_CursesEditor::GetPaneDims(int* paney, int* paneh) // returns ypos of divider
{
  const int pane_divy=(int)(m_pane_div*(double)(LINES-m_top_margin-m_bottom_margin-1));
  if (paney)
  {
    paney[0]=m_top_margin;
    paney[1]=m_top_margin+pane_divy+1;
  }
  if (paneh)
  {
    paneh[0]=pane_divy+(m_pane_div >= 1.0 ? 1 : 0);
    paneh[1]=LINES-pane_divy-m_top_margin-m_bottom_margin-1;
    
  }
  return pane_divy;
}


int WDL_CursesEditor::getVisibleLines() const { return LINES-m_bottom_margin-m_top_margin; }


#ifdef WDL_IS_FAKE_CURSES
LRESULT WDL_CursesEditor::onMouseMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  static int s_mousedown[2];

  switch (uMsg)
  {
    case WM_CAPTURECHANGED:
    break;

    case WM_MOUSEMOVE:
      if (GetCapture()==hwnd && CURSES_INSTANCE->m_font_w && CURSES_INSTANCE->m_font_h)
      {
        POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) };
        int cx=pt.x/CURSES_INSTANCE->m_font_w;
        int cy=pt.y/CURSES_INSTANCE->m_font_h;

        int paney[2], paneh[2];
        const int pane_divy=GetPaneDims(paney, paneh);

        if (m_scrollcap)
        {
          int i=m_scrollcap-1;

          if (i == 2) // divider
          {
            int divy=cy-paney[0];
            if (divy != pane_divy)
            {
              if (divy <= 0 || divy >= LINES-m_top_margin-m_bottom_margin-1)
              {
                if (divy <= 0.0) m_paneoffs_y[0]=m_paneoffs_y[1];
                m_curpane=0;
                m_pane_div=1.0;
              }
              else
              {
                m_pane_div=(double)divy/(double)(LINES-m_top_margin-m_bottom_margin-1);
              }
              draw();
              draw_status_state();
            }
          }
          else
          {
            int prevoffs=m_paneoffs_y[i];
            int pxy=paney[i]*CURSES_INSTANCE->m_font_h;
            int pxh=paneh[i]*CURSES_INSTANCE->m_font_h;

            if (pxh > CURSES_INSTANCE->scroll_h[i])
            {
              m_paneoffs_y[i]=(m_text.GetSize()-paneh[i])*(pt.y-m_scrollcap_yoffs-pxy)/(pxh-CURSES_INSTANCE->scroll_h[i]);
              int maxscroll=m_text.GetSize()-paneh[i]+4;
              if (m_paneoffs_y[i] > maxscroll) m_paneoffs_y[i]=maxscroll;
              if (m_paneoffs_y[i] < 0) m_paneoffs_y[i]=0;
            }

            if (m_paneoffs_y[i] != prevoffs)
            {
              draw();
              draw_status_state();
              
              const int col=m_curs_x-m_offs_x;
              int line=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
              if (line >= paney[m_curpane] && line < paney[m_curpane]+paneh[m_curpane]) move(line, col);
            }
          }

          return 0;
        }

        if (!m_selecting && (cx != s_mousedown[0] || cy != s_mousedown[1]))
        {
          m_select_x2=m_select_x1=m_curs_x;
          m_select_y2=m_select_y1=m_curs_y;
          m_selecting=1;
        }

        int x=cx+m_offs_x;
        int y=cy+m_paneoffs_y[m_curpane]-paney[m_curpane];
        if (m_selecting && (m_select_x2!=x || m_select_y2 != y))
        {
          if (y < m_paneoffs_y[m_curpane] && m_paneoffs_y[m_curpane] > 0)
          {
            m_paneoffs_y[m_curpane]--;
          }
          else if (y >= m_paneoffs_y[m_curpane]+paneh[m_curpane] && m_paneoffs_y[m_curpane]+paneh[m_curpane] < m_text.GetSize())
          {
            m_paneoffs_y[m_curpane]++;
          }
          if (x < m_offs_x && m_offs_x > 0)
          {
            m_offs_x--;
          }
          else if (x > m_offs_x+COLS)
          {
            int maxlen=0;
            int a;
            for (a=0; a < paneh[m_curpane]; a++)
            {
              WDL_FastString* s=m_text.Get(m_paneoffs_y[m_curpane]+a);
              if (s)
              {
                const int l = WDL_utf8_get_charlen(s->Get());
                if (l > maxlen) maxlen=l;
              }
            }
            if (maxlen > m_offs_x+COLS-8) m_offs_x++;
          }

          m_select_y2=y;
          m_select_x2=x;
          if (m_select_y2<0) m_select_y2=0;
          else if (m_select_y2>=m_text.GetSize())
          {
            m_select_y2=m_text.GetSize()-1;
            WDL_FastString *s=m_text.Get(m_select_y2);
            if (s) m_select_x2 = WDL_utf8_get_charlen(s->Get());
          }
          if (m_select_x2<0)m_select_x2=0;
          WDL_FastString *s=m_text.Get(m_select_y2);
          if (s)
          {
            const int l = WDL_utf8_get_charlen(s->Get());
            if (m_select_x2>l) m_select_x2 = l;
          }
          draw();

          int y=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
          if (y >= paney[m_curpane] && y < paney[m_curpane]+paneh[m_curpane]) setCursor();
        }
      }
      break;
    case WM_LBUTTONDBLCLK:
      if (CURSES_INSTANCE && CURSES_INSTANCE->m_font_w && CURSES_INSTANCE->m_font_h)
      {
        const int y = ((short)HIWORD(lParam)) / CURSES_INSTANCE->m_font_h - m_top_margin;
        const int x = ((short)LOWORD(lParam)) / CURSES_INSTANCE->m_font_w + m_offs_x;
        WDL_FastString *fs=m_text.Get(y + m_paneoffs_y[m_curpane]);
        if (fs && y >= 0)
        {
          const char *url=fs->Get();
          
          while (NULL != (url = strstr(url,"http://")))
          {
            if (url != fs->Get() && url[-1] > 0 && isalnum(url[-1]))
            {
              url+=7;
            }
            else
            {
              const int soffs = (int) (url - fs->Get());
              char tmp[512];
              char *p=tmp;
              while (p < (tmp+sizeof(tmp)-1) &&
                    *url && *url != ' ' && *url != ')' && *url != '\t' && *url != '"' && *url != '\'' )
              {
                *p++ = *url++;
              }
              *p=0;
              if (strlen(tmp) >= 10 && x >= soffs && x<(url-fs->Get()))
              {
                ShellExecute(hwnd,"open",tmp,"","",0);
                return 1;
              }
            }
          }
        }
      }
      WDL_FALLTHROUGH;

    case WM_LBUTTONDOWN:
      if (CURSES_INSTANCE && CURSES_INSTANCE->m_font_w && CURSES_INSTANCE->m_font_h)
      {
        int x = ((short)LOWORD(lParam)) / CURSES_INSTANCE->m_font_w;
        int y = ((short)HIWORD(lParam)) / CURSES_INSTANCE->m_font_h;
        const int tabcnt=GetTabCount();
        if (y==0 && tabcnt>1)
        {
          int tsz=COLS/tabcnt;
          // this is duplicated in draw_top_line
          if (tsz>128)tsz=128;
          if (tsz<12) tsz=12;
          SwitchTab(x/tsz,false);

          return 1;
        }
      }

      WDL_FALLTHROUGH;
    case WM_RBUTTONDOWN:

    if (CURSES_INSTANCE->m_font_w && CURSES_INSTANCE->m_font_h)
    {
      int mousex=(short)LOWORD(lParam);
      int mousey=(short)HIWORD(lParam);

      int cx=mousex/CURSES_INSTANCE->m_font_w;
      int cy=mousey/CURSES_INSTANCE->m_font_h;
      if (cx > COLS) cx=COLS;
      if (cx < 0) cx=0;
      if (cy > LINES) cy=LINES;
      if (cy < 0) cy=0;

      m_ui_state=UI_STATE_NORMAL; // any click clears the state
      s_mousedown[0]=cx;
      s_mousedown[1]=cy;

      int paney[2], paneh[2];
      const int pane_divy=GetPaneDims(paney, paneh);

      if (uMsg == WM_LBUTTONDOWN && m_pane_div > 0.0 && m_pane_div < 1.0 && cy == m_top_margin+pane_divy)
      {
        SetCapture(hwnd);
        m_scrollcap=3;
        return 0;
      }

      int pane=-1;
      if (cy >= paney[0] && cy < paney[0]+paneh[0]) pane=0;
      else if (cy >= paney[1] && cy < paney[1]+paneh[1]) pane=1;

      if ((uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONDBLCLK) && pane >= 0 &&
          cx >= COLS-CURSES_INSTANCE->drew_scrollbar[pane])
      {
        const int st=paney[pane]*CURSES_INSTANCE->m_font_h+CURSES_INSTANCE->scroll_y[pane];
        const int sb=st+CURSES_INSTANCE->scroll_h[pane];

        int prevoffs=m_paneoffs_y[pane];

        if (mousey < st)
        {
          m_paneoffs_y[pane] -= paneh[pane];
          if (m_paneoffs_y[pane] < 0) m_paneoffs_y[pane]=0;
        }
        else if (mousey < sb)
        {
          if (uMsg == WM_LBUTTONDOWN)
          {
            SetCapture(hwnd);
            m_scrollcap=pane+1;
            m_scrollcap_yoffs=mousey-st;
          }
        }
        else
        {
          m_paneoffs_y[pane] += paneh[pane];
          int maxscroll=m_text.GetSize()-paneh[pane]+4;
          if (maxscroll < 0) maxscroll = 0;
          if (m_paneoffs_y[pane] > maxscroll) m_paneoffs_y[pane]=maxscroll;
        }
        
        if (prevoffs != m_paneoffs_y[pane])
        {
          draw();
          draw_status_state();

          int y=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
          if (y >= paney[m_curpane] && y < paney[m_curpane]+paneh[m_curpane]) setCursor();
        }
        return 0;
      }

      int ox=m_curs_x , oy=m_curs_y;
      if (m_selecting && ox == m_select_x2 && oy == m_select_y2)
      {
        ox = m_select_x1;
        oy = m_select_y1;
      }

      if (uMsg == WM_LBUTTONDOWN) m_selecting=0;

      if (pane >= 0) m_curpane=pane;           

      m_curs_x=cx+m_offs_x;
      m_curs_y=cy+m_paneoffs_y[m_curpane]-paney[m_curpane];

      bool end = (m_curs_y > m_text.GetSize()-1);
      if (end) m_curs_y=m_text.GetSize()-1;
      if (m_curs_y < 0) m_curs_y = 0;

      WDL_FastString *s=m_text.Get(m_curs_y); 
      if (m_curs_x < 0) m_curs_x = 0;
      const int slen = s ? WDL_utf8_get_charlen(s->Get()) : 0;

      if (s && (end || m_curs_x > slen)) m_curs_x=slen;
      
      if (uMsg == WM_LBUTTONDOWN && !!(GetAsyncKeyState(VK_SHIFT)&0x8000) && 
        (m_curs_x != ox || m_curs_y != oy))
      {
        m_select_x1=ox;
        m_select_y1=oy;
        m_select_x2=m_curs_x;
        m_select_y2=m_curs_y;
        m_selecting=1;
      }
      else if (uMsg == WM_LBUTTONDBLCLK && s && slen)
      {
        if (m_curs_x < slen)
        {     
          int x1=WDL_utf8_charpos_to_bytepos(s->Get(),m_curs_x);
          int x2=x1+1;
          const char* p=s->Get();
          while (x1 > 0 && p[x1-1] > 0 && (isalnum(p[x1-1]) || p[x1-1] == '_')) --x1;
          while (x2 < s->GetLength() && p[x2] > 0 && (isalnum(p[x2]) || p[x2] == '_')) ++x2;
          if (x2 > x1)
          {
            m_select_x1=WDL_utf8_bytepos_to_charpos(s->Get(),x1);
            m_curs_x=m_select_x2=WDL_utf8_bytepos_to_charpos(s->Get(),x2);
            m_select_y1=m_select_y2=m_curs_y;
            m_selecting=1;
          }
        }
      }

      onChar('L'-'A'+1); // refresh, update suggestions

      if (uMsg == WM_LBUTTONDOWN) 
      {
        SetCapture(hwnd);
      }
      else if (uMsg == WM_RBUTTONDOWN)
      {
        onRightClick(hwnd);
      }
    }
    return 0;

    case WM_LBUTTONUP:
      ReleaseCapture();
      m_scrollcap=0;
      m_scrollcap_yoffs=0;
    return 0;

    case WM_MOUSEWHEEL:
      if (CURSES_INSTANCE->m_font_h)
      {
        POINT p;
        GetCursorPos(&p);
        ScreenToClient(hwnd, &p);
        p.y /= CURSES_INSTANCE->m_font_h;

        int paney[2], paneh[2];
        GetPaneDims(paney, paneh);
        int pane=-1;
        if (p.y >= paney[0] && p.y < paney[0]+paneh[0]) pane=0;
        else if (p.y >= paney[1] && p.y < paney[1]+paneh[1]) pane=1;
        if (pane < 0) pane=m_curpane;

        m_paneoffs_y[pane] -= ((short)HIWORD(wParam))/20;

        int maxscroll=m_text.GetSize()-paneh[pane]+4;
        if (m_paneoffs_y[pane] > maxscroll) m_paneoffs_y[pane]=maxscroll;
        if (m_paneoffs_y[pane] < 0) m_paneoffs_y[pane]=0;

        draw();

        int y=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
        if (y >= paney[m_curpane] && y < paney[m_curpane]+paneh[m_curpane]) setCursor();
        else draw_status_state();
      }
    break;
  }
  return 0;
}
#endif


WDL_CursesEditor::~WDL_CursesEditor()
{
  endwin();
  m_text.Empty(true);
  m_undoStack.Empty(true);
}

int WDL_CursesEditor::init(const char *fn, const char *init_if_empty)
{
  m_filename.Set(fn);
  FILE *fh=fopenUTF8(fn,"rb");

  if (!fh) 
  {
    if (init_if_empty)
    {
      fh=fopenUTF8(fn,"w+b");
      if (fh)
      {
        fwrite(init_if_empty,1,strlen(init_if_empty),fh);

        fseek(fh,0,SEEK_SET);
      }
    }
    if (!fh)
    {
      saveUndoState();
      m_clean_undopos=m_undoStack_pos;
      return 1;
    }
  }
  
  loadLines(fh);
  fclose(fh);

  return 0;  
}

int WDL_CursesEditor::reload_file(bool clearundo)
{
  FILE *fh=fopenUTF8(m_filename.Get(),"rb");
  if (fh)
  {
    if (!clearundo)
    {
      preSaveUndoState();
    }
    else
    {
      m_undoStack.Empty(true);
      m_undoStack_pos=-1;
    }

    m_text.Empty(true);
    loadLines(fh);
    fclose(fh);

    return 0;
  }
  return 1;
}

static void ReplaceTabs(WDL_FastString *str, int tabsz)
{
  int x;
  char s[128];
  // replace any \t with spaces
  int insert_sz=tabsz - 1;
  if (insert_sz<0) insert_sz=0;
  else if (insert_sz>128) insert_sz=128;

  if (insert_sz>0) memset(s,' ',insert_sz);
  for(x=0;x<str->GetLength();x++)
  {
    char *p = (char *)str->Get();
    if (p[x] == '\t')
    {
      p[x] = ' ';
      str->Insert(s,x+1,insert_sz);
      x+=insert_sz;
    }
  }
}

void WDL_CursesEditor::loadLines(FILE *fh)
{
  int crcnt = 0;
  int tabstate = 0;
  int tab_cnv_size=5;
  int rdcnt=0;
  for (;;)
  {
    WDL_FastString *fs = NULL;

    for (;;)
    {
      char line[4096];
      line[0]=0;
      fgets(line,sizeof(line),fh);
      if (!line[0]) break;

      if (!fs) fs = new WDL_FastString(line);
      else fs->Append(line);

      if (fs->Get()[fs->GetLength()-1] == '\n' || fs->GetLength() > 32*1024*1024) break;
    }
    if (!fs) break;

    if (!rdcnt++)
    {
      if ((unsigned char)fs->Get()[0] == 0xef && 
          (unsigned char)fs->Get()[1] == 0xbb && 
          (unsigned char)fs->Get()[2] == 0xbf)
      {
        // remove BOM (could track it, but currently EEL/etc don't support reading it anyway)
        fs->DeleteSub(0,3);
        if (!fs->GetLength()) break;
      }
    }

    while (fs->GetLength()>0)
    {
      char c = fs->Get()[fs->GetLength()-1];
      if (c == '\r') crcnt++;
      else if (c != '\n') break;
      fs->DeleteSub(fs->GetLength()-1,1);
    }
    m_text.Add(fs);

    if (tabstate>=0)
    {
      const char *p = fs->Get();
      if (*p == '\t' && !tabstate) tabstate=1; 
      while (*p == '\t') p++;

      int spacecnt=0;
      while (*p == ' ') { p++; spacecnt++; }
      if (*p == '\t' || spacecnt>7) tabstate=-1; // tab after space, or more than 7 spaces = dont try to preserve tabs
      else if (spacecnt+1 > tab_cnv_size) tab_cnv_size = spacecnt+1;
    }
  }
  if (tabstate>0)
  {
    m_indent_size=tab_cnv_size;
    m_write_leading_tabs=tab_cnv_size;
  }
  else
  {
    m_write_leading_tabs=0;
  }

  int x;
  for (x=0;x<m_text.GetSize();x++)
  {
    WDL_FastString *s = m_text.Get(x);
    if (s)
      ReplaceTabs(s,m_indent_size);
  }
  m_newline_mode=crcnt > m_text.GetSize()/2; // more than half of lines have crlf, then use crlf

  saveUndoState();
  m_clean_undopos=m_undoStack_pos;
  updateLastModTime();
}

void WDL_CursesEditor::draw_status_state()
{
  int paney[2], paneh[2];
  const int pane_divy=GetPaneDims(paney, paneh);

  attrset(COLOR_BOTTOMLINE);
  bkgdset(COLOR_BOTTOMLINE);

  int line=LINES-1;
  const char* whichpane="";
  if (m_pane_div > 0.0 && m_pane_div < 1.0)
  {
    whichpane=(!m_curpane ? "Upper pane: " : "Lower pane: ");
    line=m_top_margin+pane_divy;
    move(line, 0);
    clrtoeol();
  }

  char str[512];
  const int pane_offs = m_paneoffs_y[!!m_curpane];
  snprintf(str, sizeof(str), "%sLine %d/%d [%d-%d] Col %d [%s]%s",
    whichpane,
    m_curs_y+1, m_text.GetSize(),
    pane_offs+1, wdl_min(pane_offs+1+paneh[!!m_curpane],m_text.GetSize()),
    m_curs_x+1,
    (s_overwrite ? "OVR" : "INS"), (m_clean_undopos == m_undoStack_pos ? "" : "*"));

  int len=strlen(str);
  int x=COLS-len-1;
  if (!*whichpane)
  {
    if (len < m_status_lastlen)
    {
      int xpos = COLS-m_status_lastlen-1;
      if (xpos<0) xpos=0;
      move(line,xpos);
      while (xpos++ < x) addstr(" ");
    }
    m_status_lastlen = len;
  }
  else 
  {
    m_status_lastlen=0;
  }

  mvaddnstr(line, x, str, len);
  clrtoeol();

  attrset(0);
  bkgdset(0);  

  const int col=m_curs_x-m_offs_x;
  line=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
  if (line >= paney[m_curpane] && line < paney[m_curpane]+paneh[m_curpane]) move(line, col);
}

void WDL_CursesEditor::setCursor(int isVscroll, double ycenter)
{
  int maxx=m_text.Get(m_curs_y) ? m_text.Get(m_curs_y)->GetLength() : 0;

  if (isVscroll)
  {
    if (m_want_x >= 0) m_curs_x=m_want_x;
  }
  else
  {
    if (m_curs_x < maxx) m_want_x=-1;
  }

  if(m_curs_x>maxx)
  {
    if (isVscroll) m_want_x=m_curs_x;
    m_curs_x=maxx;
  }

  int redraw=0;

  if (m_curs_x < m_offs_x) 
  { 
    redraw=1; 
    m_offs_x=m_curs_x; 
  }
  else 
  {
    const int mw = COLS-3;
    if (m_curs_x >= m_offs_x + mw)
    { 
      m_offs_x=m_curs_x-mw+1; 
      redraw=1; 
    }
  }

  int paney[2], paneh[2];
  GetPaneDims(paney, paneh);
  int y;
  if (ycenter >= 0.0 && ycenter < 1.0)
  {
    y=(int)((double)paneh[m_curpane]*ycenter);
    m_paneoffs_y[m_curpane]=m_curs_y-y;
    if (m_paneoffs_y[m_curpane] < 0)
    {
      y += m_paneoffs_y[m_curpane];
      m_paneoffs_y[m_curpane]=0;
    }
    redraw=1;
  }
  else
  {
    y=m_curs_y-m_paneoffs_y[m_curpane];
    if (y < 0) 
    {
      m_paneoffs_y[m_curpane] += y;
      y=0;
      redraw=1;
    }
    else if (y >= paneh[m_curpane]) 
    {
      m_paneoffs_y[m_curpane] += y-paneh[m_curpane]+1;
      y=paneh[m_curpane]-1;
      redraw=1;
    }
  }

  if (redraw) draw();

  draw_status_state();

  y += paney[m_curpane];
  move(y, m_curs_x-m_offs_x);
}

void WDL_CursesEditor::setCursorIfVisible()
{
  if (WDL_NOT_NORMALLY(m_curpane != 0 && m_curpane != 1)) return;
  int paney[2], paneh[2];
  GetPaneDims(paney, paneh);
  int y=m_curs_y-m_paneoffs_y[m_curpane];
  if (y >= 0 && y < paneh[m_curpane])
    setCursor();
}

void WDL_CursesEditor::draw_message(const char *str)
{
  if (!CURSES_INSTANCE) return;
  
  int l=strlen(str);
  if (l && m_ui_state == UI_STATE_NORMAL) m_ui_state=UI_STATE_MESSAGE;
  if (l > COLS-2) l=COLS-2;
  if (str[0]) 
  {
    attrset(COLOR_MESSAGE);
    bkgdset(COLOR_MESSAGE);
  }
  mvaddnstr(LINES-(m_bottom_margin>1?2:1),0,str,l);
  clrtoeol();
  if (str[0])
  {
    attrset(0);
    bkgdset(0);
  }   

  int paney[2], paneh[2];
  GetPaneDims(paney, paneh);

  const int col=m_curs_x-m_offs_x;
  int line=m_curs_y+paney[m_curpane]-m_paneoffs_y[m_curpane];
  if (line >= paney[m_curpane] && line < paney[m_curpane]+paneh[m_curpane]) move(line, col);
}


void WDL_CursesEditor::draw_line_highlight(int y, const char *p, int *c_comment_state, int line_n)
{
  attrset(A_NORMAL);
  mvaddstr(y,0,p + WDL_utf8_charpos_to_bytepos(p,m_offs_x));
  clrtoeol();
}

void WDL_CursesEditor::getselectregion(int &minx, int &miny, int &maxx, int &maxy)
{
    if (m_select_y2 < m_select_y1)
    {
      miny=m_select_y2; maxy=m_select_y1;
      minx=m_select_x2; maxx=m_select_x1;
    }
    else if (m_select_y1 < m_select_y2)
    {
      miny=m_select_y1; maxy=m_select_y2;
      minx=m_select_x1; maxx=m_select_x2;
    }
    else
    {
      miny=maxy=m_select_y1;
      minx=wdl_min(m_select_x1,m_select_x2);
      maxx=wdl_max(m_select_x1,m_select_x2);
    }
}

void WDL_CursesEditor::doDrawString(int y, int line_n, const char *p, int *c_comment_state)
{
  draw_line_highlight(y,p,c_comment_state, line_n);

  if (m_selecting)
  {
    int miny,maxy,minx,maxx;
    getselectregion(minx,miny,maxx,maxy);
   
    if (line_n >= miny && line_n <= maxy && (miny != maxy || minx < maxx))
    {
      minx-=m_offs_x;
      maxx-=m_offs_x;

      const int cols = COLS;

      if (line_n > miny) minx=0;
      if (line_n < maxy) maxx=cols;

      if (minx<0)minx=0;
      if (minx > cols) minx=cols;
      if (maxx > cols) maxx=cols;

      if (maxx > minx)
      {
        attrset(COLOR_SELECTION);
        p += WDL_utf8_charpos_to_bytepos(p,m_offs_x+minx);
        mvaddnstr(y,minx, p, WDL_utf8_charpos_to_bytepos(p,maxx-minx));
        attrset(A_NORMAL);
      }
      else if (maxx==minx && !*p)
      {
        attrset(COLOR_SELECTION);
        mvaddstr(y,minx," ");
        attrset(A_NORMAL);
      }
    }
  }
}

int WDL_CursesEditor::GetCommentStateForLineStart(int line) // pass current line/col, updates with previous interesting point, returns true if start of comment, or false if end of previous comment
{
  return 0;
}


void WDL_CursesEditor::draw(int lineidx)
{
  if (m_top_margin != 0) m_top_margin = GetTabCount()>1 ? 2 : 1;

  int paney[2], paneh[2];
  const int pane_divy=GetPaneDims(paney, paneh);

#ifdef WDL_IS_FAKE_CURSES
  if (!m_cursesCtx) return;

  CURSES_INSTANCE->offs_y[0]=m_paneoffs_y[0];
  CURSES_INSTANCE->offs_y[1]=m_paneoffs_y[1];
  CURSES_INSTANCE->div_y=pane_divy;
  CURSES_INSTANCE->tot_y=m_text.GetSize();

  CURSES_INSTANCE->scrollbar_topmargin = m_top_margin;
  CURSES_INSTANCE->scrollbar_botmargin = m_bottom_margin;
#endif

  attrset(A_NORMAL);

  if (lineidx >= 0)
  {
    int comment_state = GetCommentStateForLineStart(lineidx);
    WDL_FastString *s=m_text.Get(lineidx);
    if (s)
    {
      int y=lineidx-m_paneoffs_y[0];
      if (y >= 0 && y < paneh[0])
      {
        doDrawString(paney[0]+y, lineidx, s->Get(), &comment_state);
      } 
      y=lineidx-m_paneoffs_y[1];
      if (y >= 0 && y < paneh[1])
      {
        doDrawString(paney[1]+y, lineidx, s->Get(), &comment_state);
      }
    }
    return;
  }

  __curses_invalidatefull((win32CursesCtx*)m_cursesCtx,false);

  draw_top_line();

  attrset(A_NORMAL);
  bkgdset(A_NORMAL);

  move(m_top_margin,0);
  clrtoeol();

  m_status_lastlen=0;

  int pane, i;
  for (pane=0; pane < 2; ++pane)
  {
    int ln=m_paneoffs_y[pane];
    int y=paney[pane];
    int h=paneh[pane];

    int comment_state=GetCommentStateForLineStart(ln);
 
    for(i=0; i < h; ++i, ++ln, ++y)
    { 
      WDL_FastString *s=m_text.Get(ln);
      if (!s) 
      {
        move(y,0);
        clrtoeol();
      }
      else
      {
        doDrawString(y,ln,s->Get(),&comment_state);
      }
    }
  }

  attrset(COLOR_BOTTOMLINE);
  bkgdset(COLOR_BOTTOMLINE);

  if (m_bottom_margin>0)
  {
    move(LINES-1, 0);
#define BOLD(x) { attrset(COLOR_BOTTOMLINE|A_BOLD); addstr(x); attrset(COLOR_BOTTOMLINE&~A_BOLD); }
    if (m_selecting) 
    {
      mvaddstr(LINES-1,0,"SELECTING  ESC:cancel " CONTROL_KEY_NAME "+(");
      BOLD("C"); addstr("opy ");
      BOLD("X"); addstr(":cut ");
      BOLD("V"); addstr(":paste)");
    }
    else 
    {
      mvaddstr(LINES-1, 0, CONTROL_KEY_NAME "+(");

      if (m_pane_div <= 0.0 || m_pane_div >= 1.0) 
      {
        BOLD("P"); addstr("ane ");
      }
      else
      {
        BOLD("O"); addstr("therpane ");
        addstr("no"); BOLD("P"); addstr("anes ");
      }
      BOLD("F"); addstr("ind/");
      BOLD("R"); addstr("eplace ");
      draw_bottom_line();
      addstr(")");
    }
#undef BOLD
    clrtoeol();
  }

  attrset(0);
  bkgdset(0);

  __curses_invalidatefull((win32CursesCtx*)m_cursesCtx,true);
}

void WDL_CursesEditor::draw_bottom_line()
{
  // implementers add key commands here
}

static const char *countLeadingTabs(const char *p, int *ntabs, int tabsz)
{
  if (tabsz>0) while (*p)
  {
    int i;
    for (i=0;i<tabsz;i++) if (p[i]!=' ') return p;
    p+=tabsz;
    (*ntabs) += 1;
  }
  return p;
}

int WDL_CursesEditor::updateFile()
{
  FILE *fp=fopenUTF8(m_filename.Get(),"wb");
  if (!fp) return 1;
  int x;
  for (x = 0; x < m_text.GetSize(); x ++)
  {
    WDL_FastString *s = m_text.Get(x);
    if (s)
    {
      int tabcnt=0;
      const char *p = countLeadingTabs(s->Get(),&tabcnt,m_write_leading_tabs);
      while (tabcnt-->0) fputc('\t',fp);
      fwrite(p,1,strlen(p),fp);
      if (m_newline_mode==1) fputc('\r',fp);
      fputc('\n',fp);
    }
  }
  fclose(fp);
  sync();

  updateLastModTime();
  m_clean_undopos = m_undoStack_pos;

  return 0;
}

void WDL_CursesEditor::updateLastModTime()
{
  struct stat srcstat;
  if (!statUTF8(m_filename.Get(), &srcstat))
  {
#ifndef __APPLE__
    m_filelastmod = srcstat.st_mtime;
#else
    m_filelastmod = srcstat.st_mtimespec.tv_sec;
#endif
  }
  else
  {
    m_filelastmod = 0;
  }
}

void WDL_CursesEditor::indentSelect(int amt)
{
  if (m_selecting)  // remove selected text
  {
    int miny,maxy,minx,maxx;
    int x;
    getselectregion(minx,miny,maxx,maxy);
    if (maxy >= miny)
    {
      m_select_x1 = 0;
      m_select_y1 = miny;
      m_select_y2 = maxy;
      m_select_x2 = maxx;
      if (maxx<1 && maxy>miny) maxy--; // exclude empty final line

      if (m_curs_y >= miny && m_curs_y <=maxy)
      {
        m_curs_x += amt;
        if (m_curs_x<0)m_curs_x=0;
      }

      if (amt<0)
      {
        int minspc=-amt;
        for (x = miny; x <= maxy; x ++)
        {
          WDL_FastString *s=m_text.Get(x);
          if (s)
          {
            int a=0;
            while (a<minspc && s->Get()[a]== ' ') a++;
            minspc = a;
          }
        }
        if (minspc>0 && minspc < -amt) amt = -minspc;
      }

      for (x = miny; x <= maxy; x ++)
      {
        WDL_FastString *s=m_text.Get(x);
        if (s)
        {
          if (amt>0)
          {
            int a;
            for(a=0;a<amt;a+=16)
              s->Insert("                  ",0,wdl_min(amt-a,16));
          }
          else if (amt<0)
          {
            int a=0;
            while (a<-amt && s->Get()[a]== ' ') a++;
            s->DeleteSub(0,a);
          }
        }
        if (x==m_select_y2) m_select_x2 = s ? s->GetLength() : 0;
      }
    }
  }
}

void WDL_CursesEditor::removeSelect()
{
  if (m_selecting)  // remove selected text
  {
    int miny,maxy,minx,maxx;
    int x;
    getselectregion(minx,miny,maxx,maxy);
    m_curs_x = minx;
    m_curs_y = miny;
    if (m_curs_y < 0) m_curs_y=0;
          
      if (minx != maxx|| miny != maxy) 
      {
        int fht=0,lht=0;
        for (x = miny; x <= maxy; x ++)
        {
          WDL_FastString *s=m_text.Get(x);
          if (s)
          {
            const int sx=x == miny ? WDL_utf8_charpos_to_bytepos(s->Get(),minx) : 0;
            const int ex=x == maxy ? WDL_utf8_charpos_to_bytepos(s->Get(),maxx) : s->GetLength();

            if (x != maxy && sx == 0 && ex == s->GetLength()) // remove entire line
            {
              m_text.Delete(x,true);
              if (x==miny) miny--;
              x--;
              maxy--;
            }
            else { if (x==miny) fht=1; if (x == maxy) lht=1; s->DeleteSub(sx,ex-sx); }
          }
        }
        if (fht && lht && miny+1 == maxy)
        {
          m_text.Get(miny)->Append(m_text.Get(maxy)->Get());
          m_text.Delete(maxy,true);
        }

      }
      m_selecting=0;
      if (m_curs_y >= m_text.GetSize())
      {
        m_curs_y = m_text.GetSize();
        m_text.Add(new WDL_FastString);
      }
    }

}

static const char *skip_indent(const char *tstr, int skipsz)
{
  while (*tstr == ' ' && skipsz-->0) tstr++;
  return tstr;
}

static WDL_FastString *newIndentedFastString(const char *tstr, int indent_to_pos)
{
  WDL_FastString *s=new WDL_FastString;
  if (indent_to_pos>=0) 
  {
    int x;
    for (x=0;x<indent_to_pos;x++) s->Append(" ");
  }
  s->Append(tstr);
  return s;

}

void WDL_CursesEditor::highlight_line(int line)
{ 
  if (line >= 0 && line <= m_text.GetSize())
  {
    bool nosel=false;
    if (line == m_text.GetSize()) { line--; nosel=true; }

    m_curs_x=0;
    m_curs_y=line;

    WDL_FastString* s=m_text.Get(line);
    if (s && s->GetLength())
    {
      if (nosel) m_curs_x = WDL_utf8_get_charlen(s->Get());
      else
      {
        m_select_x1=0;
        m_select_x2=WDL_utf8_get_charlen(s->Get());
        m_select_y1=m_select_y2=m_curs_y;
        m_selecting=1;
      }
      draw();
    }

    setCursor();
  }
}

void WDL_CursesEditor::GoToLine(int line, bool dosel)
{
  WDL_FastString *fs = m_text.Get(line);
  m_curs_y=line;
  m_select_x1=0;
  m_curs_x = m_select_x2 = WDL_NORMALLY(fs) ? WDL_utf8_get_charlen(fs->Get()) : 0;
  m_select_y1=m_select_y2=line;
  m_selecting=dosel?1:0;
  setCursor(0,0.25);
}

// tweak tokens to make them more useful in searching
static void tweak_tok(const char *tok, const char **np, int *len)
{
  int l = *len;
  if (WDL_NOT_NORMALLY(*np != tok+l)) return;
  if (WDL_NOT_NORMALLY(l < 1)) return;

  if (l == 1)
  {
    if ((tok[0] == '=' || tok[0] == '!' || tok[0] == '<' || tok[0] == '>') && tok[1] == '=')
    {
      l = (tok[0] == '=' || tok[0] == '!') && tok[2] == '=' ? 3 : 2;
      *np = tok + l;
      *len = l;
    }
  }
  else if (*tok == '.')
  {
    *len = 1;
    *np = tok + 1;
  }
  else
  {
    const char *p = tok;
    while (++p < tok+l)
    {
      if (*p == '.')
      {
        *len = (int) (p-tok);
        *np = p;
        break;
      }
    }
  }
}

int WDL_CursesEditor::search_line(const char *str, const WDL_FastString *line, int startx, bool backwards, int *match_len) // returns offset of next match, or -1 if none
{
  const char *p = line->Get();
  const int linelen = line->GetLength();
  const int srchlen = (int) strlen(str);

  if (s_search_mode == 4 || s_search_mode == 5)
  {
    const char *lptr = p;
    int best_match = -1, best_match_len = 0, lstate = 0;
    for (;;)
    {
      int llen=0;
      const char *ltok = sh_tokenize(&lptr, p + linelen, &llen,&lstate);
      if (!ltok) break;
      if (!lstate) tweak_tok(ltok,&lptr,&llen);
      const int ltok_offs = (int) (ltok - p);
      if (backwards && ltok_offs > startx) break;

      // see if the sequence of tokens at ltok matches
      const char *aptr = str, *bptr = ltok;
      int astate=0, bstate=lstate;
      int last_endtok_offs = ltok_offs;
      for (;;)
      {
        int alen=0, blen=0;
        const char *atok = sh_tokenize(&aptr, str+srchlen, &alen,&astate);

        if (!atok) // end of search term
        {
          if (backwards || ltok_offs >= startx)
          {
            best_match = ltok_offs;
            best_match_len = last_endtok_offs - ltok_offs;
          }
          break;
        }

        const char *btok = sh_tokenize(&bptr, p+linelen, &blen, &bstate);
        if (!btok) break; // end of line without match

        if (!astate) tweak_tok(atok,&aptr,&alen);
        if (!bstate) tweak_tok(btok,&bptr,&blen);

        if (alen != blen ||
            ((s_search_mode&1) ? strncmp(atok,btok,alen) : strnicmp(atok,btok,alen)))
          break;

        last_endtok_offs = (int) (btok + blen - p);
      }
      if (!backwards && best_match>=0) break;
    }
    if (match_len) *match_len = best_match_len;
    return best_match;
  }
  if (match_len) *match_len = srchlen;

  const int dstartx = backwards?-1:1;
  for (; backwards ? (startx>=0) : (startx <= linelen-srchlen); startx+=dstartx)
    if ((s_search_mode&1) ? !strncmp(p+startx,str,srchlen) : !strnicmp(p+startx,str,srchlen))
    {
      if (!(s_search_mode&2)) return startx;
      if ((startx==0 || p[startx-1]<0 || !isalnum(p[startx-1])) &&
          (p[startx+srchlen]<=0 || !isalnum(p[startx+srchlen]))) return startx;
    }
  return -1;
}

static const char *ellipsify(const char *str, char *buf, int bufsz)
{
  int str_len = (int) strlen(str);
  if (str_len < bufsz-8 || bufsz < 8) return str;
  int l1 = 0;
  while (l1 < bufsz/2)
  {
    if (WDL_NOT_NORMALLY(!str[l1])) return str;
    int sz=wdl_utf8_parsechar(str+l1,NULL);
    l1+=sz;
  }
  int l2 = l1;
  while (l1 + (str_len - l2) > bufsz-4)
  {
    if (WDL_NOT_NORMALLY(!str[l2])) return str;
    int sz=wdl_utf8_parsechar(str+l2,NULL);
    l2+=sz;
  }
  snprintf(buf,bufsz,"%.*s...%s",l1,str,str+l2);
  return buf;
}

void WDL_CursesEditor::runSearch(bool backwards, bool replaceAll)
{
  char buf[512];
  buf[0]=0;
  if (replaceAll && m_search_string.GetLength())
  {
    preSaveUndoState();
    int cnt = 0, linecnt = 0;
    const int numlines = m_text.GetSize();
    for (int line = 0; line < numlines; line ++)
    {
      WDL_FastString *tl = m_text.Get(line);
      if (WDL_NOT_NORMALLY(!tl)) continue;

      bool repl = false;
      int startx = 0;

      while (startx < tl->GetLength())
      {
        int matchlen=0;
        int bytepos = search_line(m_search_string.Get(),tl, startx, false, &matchlen);
        if (bytepos < 0) break;

        int tbp, cursx_bytepos = -1, selx1_bytepos=-1, selx2_bytepos=-1;
#define PRECHK_PAIR(py, px, bpv) \
        if ((py) == line && (tbp=WDL_utf8_charpos_to_bytepos(tl->Get(),(px))) >= bytepos) { \
          if (tbp < bytepos + matchlen) (px) = WDL_utf8_bytepos_to_charpos(tl->Get(),bytepos); \
          else (bpv) = tbp; \
        }

        PRECHK_PAIR(m_curs_y,m_curs_x,cursx_bytepos)
        PRECHK_PAIR(m_select_y1,m_select_x1,selx1_bytepos)
        PRECHK_PAIR(m_select_y2,m_select_x2,selx2_bytepos)

        tl->DeleteSub(bytepos,matchlen);
        tl->Insert(m_replace_string.Get(),bytepos);

#define POSTCHK_PAIR(px, bpv) \
        if ((bpv) >= 0) (px) = WDL_utf8_bytepos_to_charpos(tl->Get(),(bpv) - matchlen + m_replace_string.GetLength());
        POSTCHK_PAIR(m_curs_x,cursx_bytepos)
        POSTCHK_PAIR(m_select_x1,selx1_bytepos);
        POSTCHK_PAIR(m_select_x2,selx2_bytepos);
#undef PRECHK_PAIR
#undef POSTCHK_PAIR

        startx = bytepos + m_replace_string.GetLength();
        repl = true;
        cnt++;
      }
      if (repl) linecnt++;
    }
    if (cnt)
      snprintf(buf,sizeof(buf),"Replaced %d instance%s on %d line%s",cnt, cnt==1?"":"s", linecnt, linecnt==1?"":"s");
    saveUndoState();
  }
  else if (m_search_string.GetLength())
  {
    char elbuf[50];
    const int numlines = m_text.GetSize();
    if (numlines) for (int y = 0; y <= numlines; y ++)
    {
      int line = m_curs_y + (backwards ? -y : y), wrapflag=0;
      if (line >= numlines) { line -= numlines; wrapflag = 1; }
      else if (line < 0) { line += numlines; wrapflag = 1; }

      WDL_FastString *tl = m_text.Get(line);
      if (WDL_NOT_NORMALLY(!tl)) continue;

      const int startx = y==0 ? (backwards ? m_curs_x-1 : m_curs_x+1) : (backwards ? tl->GetLength()-1 : 0);
      if (backwards && startx<0) continue;

      int matchlen=0;
      int bytepos = search_line(m_search_string.Get(),tl, startx > 0 ? WDL_utf8_charpos_to_bytepos(tl->Get(),startx) : 0, backwards, &matchlen);
      if (bytepos >= 0)
      {
        m_select_y1=m_select_y2=m_curs_y=line;
        m_select_x1=m_curs_x=WDL_utf8_bytepos_to_charpos(tl->Get(),bytepos);
        m_select_x2=WDL_utf8_bytepos_to_charpos(tl->Get(),bytepos+matchlen);
        m_selecting=1;

        // make sure the end is on screen
        m_offs_x = 0;
        m_curs_x = wdl_max(m_select_x1,m_select_x2) + COLS/4;
        setCursor();

        m_curs_x = m_select_x1;
        snprintf(buf,sizeof(buf),"Found @ Line %d Col %d %s'%s' (Shift+)F3|" CONTROL_KEY_NAME "+G:(prev)next",m_curs_y+1,m_curs_x+1,wrapflag?"(wrapped) ":"",ellipsify(m_search_string.Get(),elbuf,sizeof(elbuf)));
        break;
      }
    }
    if (!buf[0])
      snprintf(buf,sizeof(buf),"%s '%s' not found",searchmode_desc(s_search_mode),ellipsify(m_search_string.Get(),elbuf,sizeof(elbuf)));
  }

  draw();
  setCursor();
  if (buf[0])
    draw_message(buf);
}

static int categorizeCharForWordNess(int c)
{
  if (c >= 0 && c < 256)
  {
    if (isspace(c)) return 0;
    if (isalnum(c) || c == '_') return 1;
    if (c == ';') return 2; // I prefer this, since semicolons are somewhat special
  }
  return 3;
}

#define CTRL_KEY_DOWN (GetAsyncKeyState(VK_CONTROL)&0x8000)
#define SHIFT_KEY_DOWN (GetAsyncKeyState(VK_SHIFT)&0x8000)
#define ALT_KEY_DOWN (GetAsyncKeyState(VK_MENU)&0x8000)


void WDL_CursesEditor::getLinesFromClipboard(WDL_FastString &buf, WDL_PtrList<const char> &lines)
{
#ifdef WDL_IS_FAKE_CURSES
  if (CURSES_INSTANCE)
  {
    HANDLE h;
    OpenClipboard(CURSES_INSTANCE->m_hwnd);
#ifdef CF_UNICODETEXT
    h=GetClipboardData(CF_UNICODETEXT);
    if (h)
    {
      wchar_t *t=(wchar_t *)GlobalLock(h);
      int s=(int)(GlobalSize(h)/2);
      while (s-- > 0)
      {
        char b[32];
        if (!*t) break;
        WDL_MakeUTFChar(b,*t++,sizeof(b));
        buf.Append(b);
      }

      GlobalUnlock(t);
    }

#endif
    if (!buf.GetLength())
    {
      h=GetClipboardData(CF_TEXT);
      if (h)
      {
        char *t=(char *)GlobalLock(h);
        int s=(int)(GlobalSize(h));
        buf.Set(t,s);
        GlobalUnlock(t);
      }
    }
    CloseClipboard();
  }
  else
#endif
  {
    buf.Set(s_fake_clipboard.Get());
  }

  if (buf.Get() && buf.Get()[0])
  {
    ReplaceTabs(&buf,m_indent_size);

    char *src=(char*)buf.Get();
    while (*src)
    {
      char *seek=src;
      while (*seek && *seek != '\r' && *seek != '\n') seek++;
      char hadclr=*seek;
      if (*seek) *seek++=0;
      lines.Add(src);

      if (hadclr == '\r' && *seek == '\n') seek++;

      if (hadclr && !*seek)
      {
        lines.Add("");
      }
      src=seek;
    }
  }
}

void WDL_CursesEditor::run_line_editor(int c, WDL_FastString *fs)
{
  char tmp[512];
  switch (c)
  {
    case -1: break;
    case 0:
      m_line_editor_edited=false;
      draw_top_line();
    break;
    case 27:
      draw();
      setCursor();
      // fallthrough
    case '\r': case '\n':
      m_ui_state=UI_STATE_NORMAL;
    return;
    case KEY_BACKSPACE:
      if (!fs->GetLength()) return;

      {
        const char *p = fs->Get();
        for (int x = 0;;)
        {
          int sz=wdl_utf8_parsechar(p+x,NULL);
          if (!p[x+sz])
          {
            fs->SetLen(x);
            m_line_editor_edited=true;
            break;
          }
          x+=sz;
        }
      }
    break;
    case KEY_IC:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN) return;
      WDL_FALLTHROUGH;
    case 'V'-'A'+1:
      {
        WDL_PtrList<const char> lines;
        WDL_FastString buf;
        getLinesFromClipboard(buf,lines);
        if (!lines.Get(0)) return;
        if (!m_line_editor_edited)
        {
          fs->Set("");
          m_line_editor_edited=true;
        }
        fs->Append(lines.Get(0));
      }
    break;
    case KEY_UP:
      if (m_ui_state == UI_STATE_REPLACE || m_ui_state == UI_STATE_SEARCH)
      {
        if (--s_search_mode<0) s_search_mode=5;
      }
    break;
    case KEY_DOWN:
      if (m_ui_state == UI_STATE_REPLACE || m_ui_state == UI_STATE_SEARCH)
      {
        if (++s_search_mode>5) s_search_mode=0;
      }
    break;
    default:
      if (!VALIDATE_TEXT_CHAR(c)) return;

      if (!m_line_editor_edited) fs->Set("");
      m_line_editor_edited=true;
      WDL_MakeUTFChar(tmp,c,sizeof(tmp));
      fs->Append(tmp);
    break;
  }

  const char *search_help = COLS > 70 ? " (Up/Down:mode " CONTROL_KEY_NAME "+R:replace ESC:cancel)" :
                            COLS > 40 ? "(U/D:mode " CONTROL_KEY_NAME "+R:repl)" :
                            "";
  char elbuf[50];
  switch (m_ui_state)
  {
    case UI_STATE_REPLACE:
      snprintf(tmp,sizeof(tmp),"Replace all (%s) '%s': ",searchmode_desc(s_search_mode),ellipsify(m_search_string.Get(),elbuf,sizeof(elbuf)));
    break;
    case UI_STATE_SEARCH:
      snprintf(tmp,sizeof(tmp),"Find %s%s: ",searchmode_desc(s_search_mode), search_help);
    break;
    case UI_STATE_GOTO_LINE:
      lstrcpyn_safe(tmp, "Go to line (ESC:cancel): ",sizeof(tmp));
    break;
    default:
    return;
  }

  attrset(COLOR_MESSAGE);
  bkgdset(COLOR_MESSAGE);
  mvaddstr(LINES-1,0,tmp);
  addstr(fs->Get());
  clrtoeol();
  attrset(0);
  bkgdset(0);
}

int WDL_CursesEditor::onChar(int c)
{
  switch (m_ui_state)
  {
    case UI_STATE_MESSAGE:
      m_ui_state=UI_STATE_NORMAL;
      draw();
      setCursor();
    break;
    case UI_STATE_SAVE_ON_CLOSE:
      if (c>=0 && (isalnum(c) || isprint(c) || c==27))
      {
        if (c == 27)
        {
          m_ui_state=UI_STATE_NORMAL;
          draw();
          draw_message("Cancelled close of file.");
          setCursor();
          return 0;
        }
        if (toupper(c) == 'N' || toupper(c) == 'Y')
        {
          if (toupper(c) == 'Y')
          {
            if(updateFile())
            {
              m_ui_state=UI_STATE_NORMAL;
              draw();
              draw_message("Error writing file, changes not saved!");
              setCursor();
              return 0;
            }
          }
          CloseCurrentTab();

          delete this;
          // this no longer valid, return 1 to avoid future calls in onChar()

          return 1;
        }
      }
    return 0;
    case UI_STATE_SAVE_AS_NEW:
      if (c>=0 && (isalnum(c) || isprint(c) || c==27 || c == '\r' || c=='\n'))
      {
        m_ui_state=UI_STATE_NORMAL;
        if (toupper(c) == 'N' || c == 27)
        {
          draw();
          draw_message("Cancelled create new file.");
          setCursor();
          return 0;
        }

        AddTab(m_newfn.Get());
      }
    return 0;
    case UI_STATE_SEARCH:
    case UI_STATE_REPLACE:
      {
        uiState chgstate = c == 1+'R'-'A' ? UI_STATE_REPLACE : c == 1+'F'-'A' ? UI_STATE_SEARCH : UI_STATE_NORMAL;
        if (chgstate != UI_STATE_NORMAL)
        {
          if (chgstate == UI_STATE_REPLACE && !m_search_string.GetLength()) chgstate = UI_STATE_SEARCH;
          c = m_ui_state == chgstate ? 27 : 0;
          m_ui_state = chgstate;
        }
        const bool is_replace = m_ui_state == UI_STATE_REPLACE;
        run_line_editor(c,is_replace ? &m_replace_string : &m_search_string);
        if (c == '\r' || c == '\n') runSearch(false, is_replace);
      }
    return 0;
    case UI_STATE_GOTO_LINE:
      if (c == 1+'J'-'A') c=27;
      run_line_editor(c,&s_goto_line_buf);
      if (c == '\r' || c == '\n')
      {
        const char *p = s_goto_line_buf.Get();
        while (*p == ' ' || *p == '\t') p++;
        int rel = 0;
        if (*p == '+') rel=1;
        else if (*p == '-') rel=-1;
        if (rel) p++;
        while (*p == ' ' || *p == '\t') p++;
        int a = atoi(p);
        if (a > 0)
        {
          if (rel) a = m_curs_y + a*rel;
          else a--;

          m_curs_y = wdl_clamp(a,0,m_text.GetSize());
          WDL_FastString *fs = m_text.Get(m_curs_y);
          m_curs_x = fs ? WDL_utf8_get_charlen(fs->Get()) : 0;
          m_selecting=0;
          draw();
          setCursor(0,-1.0);
        }
        else
        {
          draw();
          setCursor();
        }
      }
    return 0;
    case UI_STATE_NORMAL:
    break;
  }

  // UI_STATE_NORMAL
  WDL_ASSERT(m_ui_state == UI_STATE_NORMAL /* unhandled state? */);

  if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN && c =='W'-'A'+1)
  {
    if (GetTab(0) == this) return 0; // first in list = do nothing

    if (IsDirty())
    {
      m_ui_state=UI_STATE_SAVE_ON_CLOSE;
      attrset(COLOR_MESSAGE);
      bkgdset(COLOR_MESSAGE);
      mvaddstr(LINES-1,0,"Save file before closing (y/N)? ");
      clrtoeol();
      attrset(0);
      bkgdset(0);
    }
    else
    {
      CloseCurrentTab();

      delete this;
      // context no longer valid!
      return 1;
    }
    return 0;
  }
  if ((c==27 || c==29 || (c >= KEY_F1 && c<=KEY_F10)) && CTRL_KEY_DOWN)
  {
    int idx=c-KEY_F1;
    bool rel=true;
    if (c==27) idx=-1;
    else if (c==29) idx=1;
    else rel=false;
    SwitchTab(idx,rel);
    return 1;
  }

  if (c==KEY_DOWN || c==KEY_UP || c==KEY_PPAGE||c==KEY_NPAGE || c==KEY_RIGHT||c==KEY_LEFT||c==KEY_HOME||c==KEY_END)
  {
    if (SHIFT_KEY_DOWN)
    {
      if (!m_selecting)
      {
        m_select_x2=m_select_x1=m_curs_x; m_select_y2=m_select_y1=m_curs_y;
        m_selecting=1;
      }
    }
    else if (m_selecting)
    {
      m_selecting=0;
      if (c == KEY_LEFT || c == KEY_RIGHT || c == KEY_UP || c == KEY_DOWN)
      {
        int miny,maxy,minx,maxx;
        getselectregion(minx,miny,maxx,maxy);
        if ((m_curs_y > miny || (m_curs_y == miny && m_curs_x >= minx)) &&
            (m_curs_y < maxy || (m_curs_y == maxy && m_curs_x <= maxx)))
        {
          m_curs_x = c == KEY_LEFT || c == KEY_UP ? minx : maxx;
          m_curs_y = c == KEY_LEFT || c == KEY_UP ? miny : maxy;
          draw();
          setCursor();
          return 0;
        }
      }
      draw();
    }
  }

  switch(c)
  {
    case 'O'-'A'+1:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
      {
        if (m_pane_div <= 0.0 || m_pane_div >= 1.0)
        {
          onChar('P'-'A'+1);
        }
        if (m_pane_div > 0.0 && m_pane_div < 1.0) 
        {
          m_curpane=!m_curpane;
          draw();
          draw_status_state();
          int paney[2], paneh[2];
          GetPaneDims(paney, paneh);
          if (m_curs_y-m_paneoffs_y[m_curpane] < 0) m_curs_y=m_paneoffs_y[m_curpane];
          else if (m_curs_y-m_paneoffs_y[m_curpane] >= paneh[m_curpane]) m_curs_y=paneh[m_curpane]+m_paneoffs_y[m_curpane]-1;
          setCursor();
        }
      }
    break;
    case 'P'-'A'+1:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
      {
        if (m_pane_div <= 0.0 || m_pane_div >= 1.0) 
        {
          m_pane_div=0.5;
          m_paneoffs_y[1]=m_paneoffs_y[0];
        }
        else 
        {
          m_pane_div=1.0;
          if (m_curpane) m_paneoffs_y[0]=m_paneoffs_y[1];
          m_curpane=0;
        }
        draw();
        draw_status_state();

        int paney[2], paneh[2];
        GetPaneDims(paney, paneh);
        setCursor();
      }
    break;
    
    case 407:
    case 'Z'-'A'+1:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
      {
        if (m_undoStack_pos > 0)
        {
           m_undoStack_pos--;
           loadUndoState(m_undoStack.Get(m_undoStack_pos),1);
           draw();
           setCursor();
           char buf[512];
           snprintf(buf,sizeof(buf),"Undid action - %d items in undo buffer",m_undoStack_pos);
           draw_message(buf);
        }
        else 
        {
          draw_message("Can't Undo");
        }   
        break;
      }
    // fall through
    case 'Y'-'A'+1:
      if ((c == 'Z'-'A'+1 || !SHIFT_KEY_DOWN) && !ALT_KEY_DOWN)
      {
        if (m_undoStack_pos < m_undoStack.GetSize()-1)
        {
          m_undoStack_pos++;
          loadUndoState(m_undoStack.Get(m_undoStack_pos),0);
          draw();
          setCursor();
          char buf[512];
          snprintf(buf,sizeof(buf),"Redid action - %d items in redo buffer",m_undoStack.GetSize()-m_undoStack_pos-1);
          draw_message(buf);
        }
        else 
        {
          draw_message("Can't Redo");  
        }
      }
    break;
    case KEY_IC:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
      {
        s_overwrite=!s_overwrite;
        setCursor();
        break;
      }
      // fall through
    case 'V'-'A'+1:
      if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
      {
        // generate a m_clipboard using win32 clipboard data
        WDL_PtrList<const char> lines;
        WDL_FastString buf;
        getLinesFromClipboard(buf,lines);

        if (lines.GetSize())
        {
          removeSelect();
          // insert lines at m_curs_y,m_curs_x
          if (m_curs_y > m_text.GetSize()) m_curs_y=m_text.GetSize();
          if (m_curs_y < 0) m_curs_y=0;

          preSaveUndoState();

          do_paste_lines(lines);
          draw();
          setCursor();
          draw_message("Pasted");
          saveUndoState();
        }
        else 
        {
          setCursor();
          draw_message("Clipboard empty");
        }
      }
  break;

  case KEY_DC:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
    {
      WDL_FastString *s;
      if (m_selecting)
      {
        preSaveUndoState();
        removeSelect();
        draw();
        saveUndoState();
        setCursor();
      }
      else if ((s=m_text.Get(m_curs_y)))
      {
        const int xbyte = WDL_utf8_charpos_to_bytepos(s->Get(),m_curs_x);
        if (xbyte < s->GetLength())
        {
          preSaveUndoState();

          const int xbytesz=WDL_utf8_charpos_to_bytepos(s->Get()+xbyte,1);
          bool hadCom = LineCanAffectOtherLines(s->Get(),xbyte,xbytesz); 
          s->DeleteSub(xbyte,xbytesz);
          if (!hadCom) hadCom = LineCanAffectOtherLines(s->Get(),xbyte,0);
          draw(hadCom ? -1 : m_curs_y);
          saveUndoState();
          setCursor();
        }
        else // append next line to us
        {
          if (m_curs_y < m_text.GetSize()-1)
          {
            preSaveUndoState();

            WDL_FastString *nl=m_text.Get(m_curs_y+1);
            if (nl)
            {
              const char *p = nl->Get();
              while ((*p == ' ' || *p == '\t') && (p[1] == ' ' || p[1] == '\t')) p++;
              s->Append(p);
            }
            m_text.Delete(m_curs_y+1,true);

            draw();
            saveUndoState();
            setCursor();
          }
        }
      }
      break;
    }
    WDL_FALLTHROUGH;
  case 'C'-'A'+1:
  case 'X'-'A'+1:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN && m_selecting)
    {
      if (c!= 'C'-'A'+1) m_selecting=0;
      int miny,maxy,minx,maxx;
      int x;
      getselectregion(minx,miny,maxx,maxy);
      const char *status="";
      char statusbuf[512];

      if (minx != maxx|| miny != maxy) 
      {
        s_fake_clipboard.Set("");

        int lht=0,fht=0;
        if (c != 'C'-'A'+1) preSaveUndoState();

        for (x = miny; x <= maxy; x ++)
        {
          WDL_FastString *s=m_text.Get(x);
          if (s) 
          {
            const char *str=s->Get();
            int sx=x == miny ? WDL_utf8_charpos_to_bytepos(s->Get(),minx) : 0;
            const int ex=x == maxy ? WDL_utf8_charpos_to_bytepos(s->Get(),maxx) : s->GetLength();
      
            if (x != miny) s_fake_clipboard.Append("\r\n");

            const int oldlen = s_fake_clipboard.GetLength();
            if (x == miny && maxy > miny && sx > 0)
            {
              // first line of multi-line copy, not starting at first byte of line
              int icnt = 0;
              while (str[icnt] == ' ') icnt++;

              for (int nl = x+1; icnt > 0 && nl <= maxy; nl ++)
              {
                WDL_FastString *cs = m_text.Get(nl);
                if (cs)
                {
                  int c = 0;
                  while (c < icnt && cs->Get()[c] == ' ') c++;
                  if (c < icnt && cs->Get()[c])
                  {
                    if (nl == maxy && c >= WDL_utf8_charpos_to_bytepos(cs->Get(),maxx)) break; // last line, ignore if whitespace exceeds what we're copying anyway
                    icnt = 0;
                  }
                }
              }

              // if all later copied lines do not have non-whitespace in the first line's indentation
              if (icnt > 0)
              {
                // then we'll copy that line's indentation
                s_fake_clipboard.Append(str,icnt);
                // and skip any whitespace in our selection
                while (str[sx] == ' ') sx++;
              }
            }

            if (ex>sx) s_fake_clipboard.Append(str+sx,ex-sx);

            if (m_write_leading_tabs>0 && sx==0 && oldlen < s_fake_clipboard.GetLength())
            {
              const char *p = s_fake_clipboard.Get() + oldlen;
              int nt=0;
              const char *sp = countLeadingTabs(p,&nt,m_write_leading_tabs);
              if (nt && sp > p)
              {
                s_fake_clipboard.DeleteSub(oldlen,(int)(sp-p));
                while (nt>0) 
                {
                  int c = nt;
                  if (c > 8) c=8;
                  nt -= c;
                  s_fake_clipboard.Insert("\t\t\t\t\t\t\t\t",oldlen,c);
                }
              }
            }

            if (c != 'C'-'A'+1)
            {
              if (sx == 0 && ex == s->GetLength()) // remove entire line
              {
                m_text.Delete(x,true);
                if (x==miny) miny--;
                x--;
                maxy--;
              }
              else { if (x==miny) fht=1; if (x == maxy) lht=1; s->DeleteSub(sx,ex-sx); }
            }
          }
        }
        if (fht && lht && miny+1 == maxy)
        {
          m_text.Get(miny)->Append(m_text.Get(maxy)->Get());
          m_text.Delete(maxy,true);
        }
        if (c != 'C'-'A'+1)
        {
          m_curs_y=miny;
          if (m_curs_y < 0) m_curs_y=0;
          m_curs_x=minx;
          saveUndoState();
          snprintf(statusbuf,sizeof(statusbuf),"Cut %d bytes",s_fake_clipboard.GetLength());
        }
        else
          snprintf(statusbuf,sizeof(statusbuf),"Copied %d bytes",s_fake_clipboard.GetLength());

#ifdef WDL_IS_FAKE_CURSES
        if (CURSES_INSTANCE)
        {
#ifdef CF_UNICODETEXT
          const int l=(WDL_utf8_get_charlen(s_fake_clipboard.Get())+1)*sizeof(wchar_t);
          HANDLE h=GlobalAlloc(GMEM_MOVEABLE,l);
          wchar_t *t=(wchar_t*)GlobalLock(h);
          if (t)
          {
            WDL_MBtoWideStr(t,s_fake_clipboard.Get(),l);
            GlobalUnlock(h);
          }
          OpenClipboard(CURSES_INSTANCE->m_hwnd);
          EmptyClipboard();
          SetClipboardData(CF_UNICODETEXT,h);
#else
          int l=s_fake_clipboard.GetLength()+1;
          HANDLE h=GlobalAlloc(GMEM_MOVEABLE,l);
          void *t=GlobalLock(h);
          if (t)
          {
            memcpy(t,s_fake_clipboard.Get(),l);
            GlobalUnlock(h);
          }
          OpenClipboard(CURSES_INSTANCE->m_hwnd);
          EmptyClipboard();
          SetClipboardData(CF_TEXT,h);
#endif
          CloseClipboard();
        }
#endif

        status=statusbuf;
      }
      else status="No selection";

      draw();
      setCursor();
      draw_message(status);
    }
  break;
  case 'D'-'A'+1:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
    {
      if (m_selecting)
      {
        int miny,maxy,minx,maxx;
        getselectregion(minx,miny,maxx,maxy);

        if (minx != maxx|| miny != maxy) 
        {
          WDL_FastString dup;

          int x;
          for (x = miny; x <= maxy; x ++)
          {
            WDL_FastString *s=m_text.Get(x);
            if (s) 
            {
              const char *str=s->Get();
              const int sx=x == miny ? WDL_utf8_charpos_to_bytepos(str,minx) : 0;
              const int ex=x == maxy ? WDL_utf8_charpos_to_bytepos(str,maxx) : s->GetLength();
              if (dup.GetLength()) dup.Append("\n");
              dup.Append(ex-sx?str+sx:"",ex-sx);
            }
          }

          if (dup.GetLength())
          {
            preSaveUndoState();
            WDL_PtrList<const char> lines;
            char *p = (char *)dup.Get();
            for (;;)
            {
              const char *basep = p;
              while (*p && *p != '\n') p++;
              lines.Add(basep);
              if (!*p) break;
              *p++=0;
            }
            m_curs_x=maxx;
            m_curs_y=maxy;
            do_paste_lines(lines);
            m_curs_x=maxx;
            m_curs_y=maxy;

            draw();
            setCursor();
            draw_message("Duplicated selection");
            saveUndoState();
          }
        }
      }
      else if (m_text.Get(m_curs_y))
      {
        preSaveUndoState();
        m_text.Insert(m_curs_y, new WDL_FastString(m_text.Get(m_curs_y)));
        draw();
        setCursor();
        draw_message("Duplicated line");
        saveUndoState();
      }
    }
  break;

  case 'A'-'A'+1:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
    {
      m_selecting=1;
      m_select_x1=0;
      m_select_y1=0;
      m_select_y2=m_text.GetSize()-1;
      m_select_x2=0;
      if (m_text.Get(m_select_y2))
        m_select_x2=m_text.Get(m_select_y2)->GetLength();
      draw();
      setCursor();
    }
  break;
  case 27:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN && m_selecting)
    {
      m_selecting=0;
      draw();
      setCursor();
      break;
    }
  break;
  case KEY_F3:
  case 'G'-'A'+1:
    if (!ALT_KEY_DOWN && m_search_string.GetLength())
    {
      runSearch(SHIFT_KEY_DOWN != 0, false);
      return 0;
    }
  WDL_FALLTHROUGH; // fall through
  case 'R'-'A'+1:
  case 'F'-'A'+1:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
    {
      if (m_selecting && m_select_y1==m_select_y2)
      {
        WDL_FastString* s=m_text.Get(m_select_y1);
        if (s)
        {
          const char* p=s->Get();
          int xlo=wdl_min(m_select_x1, m_select_x2);
          int xhi=wdl_max(m_select_x1, m_select_x2);
          xlo = WDL_utf8_charpos_to_bytepos(p,xlo);
          xhi = WDL_utf8_charpos_to_bytepos(p,xhi);
          if (xhi > xlo)
          {
            m_search_string.Set(p+xlo, xhi-xlo);
          }
        }
      }
      if (c == 'R'-'A'+1 && !m_search_string.GetLength())
      {
        draw_message("Use " CONTROL_KEY_NAME "+F first, or run " CONTROL_KEY_NAME "+R from Find prompt");
      }
      else
      {
        m_ui_state=c == 'R'-'A'+1 ? UI_STATE_REPLACE : UI_STATE_SEARCH;
        run_line_editor(0,m_ui_state == UI_STATE_REPLACE ? &m_replace_string : &m_search_string);
      }
    }
  break;
  case KEY_DOWN:
    {
      if (CTRL_KEY_DOWN)
      {
        int paney[2], paneh[2];
        GetPaneDims(paney, paneh);
        int maxscroll=m_text.GetSize()-paneh[m_curpane]+4;
        if (m_paneoffs_y[m_curpane] < maxscroll-1)
        {
          m_paneoffs_y[m_curpane]++;
          if (m_curs_y < m_paneoffs_y[m_curpane]) m_curs_y=m_paneoffs_y[m_curpane];
          draw();
        }
      }
      else
      {
        m_curs_y++;
        if (m_curs_y>=m_text.GetSize()) m_curs_y=m_text.GetSize()-1;
        if (m_curs_y < 0) m_curs_y=0;
      }
      if (m_selecting) { setCursor(1); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      setCursor(1);
    }
  break;
  case KEY_UP:
    {
      if (CTRL_KEY_DOWN)
      {
        if (m_paneoffs_y[m_curpane] > 0)
        {
          int paney[2], paneh[2];
          GetPaneDims(paney, paneh);
          m_paneoffs_y[m_curpane]--;
          if (m_curs_y >  m_paneoffs_y[m_curpane]+paneh[m_curpane]-1) m_curs_y = m_paneoffs_y[m_curpane]+paneh[m_curpane]-1;
          if (m_curs_y < 0) m_curs_y=0;
          draw();
        }
      }
      else
      {
        if(m_curs_y>0) m_curs_y--;
      }
      if (m_selecting) { setCursor(1); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      setCursor(1);
    }
  break;
  case KEY_PPAGE:
    {
      if (m_curs_y > m_paneoffs_y[m_curpane])
      {
        m_curs_y=m_paneoffs_y[m_curpane];
        if (m_curs_y < 0) m_curs_y=0;
      }
      else 
      {
        int paney[2], paneh[2];
        GetPaneDims(paney, paneh);
        m_curs_y -= paneh[m_curpane];
        if (m_curs_y < 0) m_curs_y=0;
        m_paneoffs_y[m_curpane]=m_curs_y;
      }
      if (m_selecting) { setCursor(1); m_select_x2=m_curs_x; m_select_y2=m_curs_y; }
      draw();
      setCursor(1);
    }
  break; 
  case KEY_NPAGE:
    {
      int paney[2], paneh[2]; 
      GetPaneDims(paney, paneh);
      if (m_curs_y >= m_paneoffs_y[m_curpane]+paneh[m_curpane]-1) m_paneoffs_y[m_curpane]=m_curs_y-1;
      m_curs_y = m_paneoffs_y[m_curpane]+paneh[m_curpane]-1;
      if (m_curs_y >= m_text.GetSize()) m_curs_y=m_text.GetSize()-1;
      if (m_curs_y < 0) m_curs_y=0;
      if (m_selecting) { setCursor(1); m_select_x2=m_curs_x; m_select_y2=m_curs_y; }
      draw();
      setCursor(1);
    }
  break;
  case KEY_RIGHT:
    {
      if (1) // wrap across lines
      {
        WDL_FastString *s = m_text.Get(m_curs_y);
        if (s && m_curs_x >= WDL_utf8_get_charlen(s->Get()) && m_curs_y < m_text.GetSize()) { m_curs_y++; m_curs_x = -1; }
      }

      if(m_curs_x<0) 
      {
        m_curs_x=0;
      }
      else
      {
        if (CTRL_KEY_DOWN)
        {
          WDL_FastString *s = m_text.Get(m_curs_y);
          if (!s) break;
          int bytepos = WDL_utf8_charpos_to_bytepos(s->Get(),m_curs_x);

          if (bytepos >= s->GetLength()) break;
          int lastType = categorizeCharForWordNess(s->Get()[bytepos++]);
          while (bytepos < s->GetLength())
          {
            int thisType = categorizeCharForWordNess(s->Get()[bytepos]);
            if (thisType != lastType && thisType != 0) break;
            lastType=thisType;
            bytepos++;
          }
          m_curs_x = WDL_utf8_bytepos_to_charpos(s->Get(),bytepos);
        }
        else 
        {
          m_curs_x++;
        }
      }
      if (m_selecting) { setCursor(); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      setCursor();
    }
  break;
  case KEY_LEFT:
    {
      bool doMove=true;
      if (1) // wrap across lines
      {
        WDL_FastString *s = m_text.Get(m_curs_y);
        if (s && m_curs_y>0 && m_curs_x == 0) 
        { 
          s = m_text.Get(--m_curs_y);
          if (s) 
          {
            m_curs_x = WDL_utf8_get_charlen(s->Get());
            doMove=false;
          }
        }
      }

      if(m_curs_x>0 && doMove) 
      {
        if (CTRL_KEY_DOWN)
        {
          WDL_FastString *s = m_text.Get(m_curs_y);
          if (!s) break;
          int bytepos = WDL_utf8_charpos_to_bytepos(s->Get(),m_curs_x);
          if (bytepos > s->GetLength()) bytepos = s->GetLength();
          bytepos--;

          int lastType = categorizeCharForWordNess(s->Get()[bytepos--]);
          while (bytepos >= 0)
          {
            int thisType = categorizeCharForWordNess(s->Get()[bytepos]);
            if (thisType != lastType && lastType != 0) break;
            lastType=thisType;
            bytepos--;
          }
          m_curs_x = WDL_utf8_bytepos_to_charpos(s->Get(),bytepos+1);
        }
        else 
        {
          m_curs_x--;
        }
      }
      if (m_selecting) { setCursor(); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      setCursor();
    }
  break;
  case KEY_HOME:
    {
      const int old_x = m_curs_x;
      m_curs_x=0;
      if (!CTRL_KEY_DOWN)
      {
        const WDL_FastString *ln = m_text.Get(m_curs_y);
        if (ln)
        {
          int i = 0;
          while (ln->Get()[i] == ' ' || ln->Get()[i] == '\t') i++;
          if (ln->Get()[i] && i != old_x) m_curs_x = i;
        }
      }
      else m_curs_y=0;

      if (m_selecting) { setCursor(); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      m_want_x=-1; // clear in case we're already at the start of the line
      setCursor();
    }
  break;
  case KEY_END:
    {
      if (CTRL_KEY_DOWN) m_curs_y=wdl_max(m_text.GetSize()-1,0);
      const WDL_FastString *ln = m_text.Get(m_curs_y);
      if (ln) m_curs_x=WDL_utf8_get_charlen(ln->Get());
      if (m_selecting) { setCursor(); m_select_x2=m_curs_x; m_select_y2=m_curs_y; draw(); }
      m_want_x=-1; // clear in case we're already at the end of the line
      setCursor();
    }
  break;
  case KEY_BACKSPACE: // backspace, baby
    if (m_selecting)
    {
      preSaveUndoState();
      removeSelect();
      draw();
      saveUndoState();
      setCursor();
    }
    else if (m_curs_x > 0)
    {
      WDL_FastString *tl=m_text.Get(m_curs_y);
      if (tl)
      {
        preSaveUndoState();

        int del_sz=1;
        if (m_indent_size > 0 && !(m_curs_x % m_indent_size))
        {
          const char *p = tl->Get();
          int i=0;
          while (i<m_curs_x && p[i]== ' ') i++;
          if (i == m_curs_x && m_curs_x>=m_indent_size)
          {
            del_sz=m_indent_size;
          }
        }

        const int xbyte = WDL_utf8_charpos_to_bytepos(tl->Get(),m_curs_x - del_sz);
        const int xbytesz=WDL_utf8_charpos_to_bytepos(tl->Get()+xbyte,del_sz);

        bool hadCom = LineCanAffectOtherLines(tl->Get(), xbyte,xbytesz);
        tl->DeleteSub(xbyte,xbytesz);
        m_curs_x-=del_sz;

        if (!hadCom) hadCom = LineCanAffectOtherLines(tl->Get(),xbyte,0);
        draw(hadCom?-1:m_curs_y);
        saveUndoState();
        setCursor();
      }
    }
    else // append current line to previous line
    {
      WDL_FastString *fl=m_text.Get(m_curs_y-1), *tl=m_text.Get(m_curs_y);
      if (!tl) 
      {
        m_curs_y--;
        if (fl) m_curs_x=WDL_utf8_get_charlen(fl->Get());
        draw();
        saveUndoState();
        setCursor();
      }
      else if (fl)
      {
        preSaveUndoState();
        m_curs_x=WDL_utf8_get_charlen(fl->Get());

        // if tl is all whitespace, don't bother appending to the previous line
        const char *p = tl->Get();
        while (*p == ' ' || *p == '\t') p++;
        if (*p) fl->Append(tl->Get());

        m_text.Delete(m_curs_y--,true);
        draw();
        saveUndoState();
        setCursor();
      }
    }
  break;
  case 'L'-'A'+1:
    draw();
    setCursor();
  break;
  case 'J'-'A'+1:
    if (!SHIFT_KEY_DOWN && !ALT_KEY_DOWN)
    {
      s_goto_line_buf.Set("");
      m_ui_state=UI_STATE_GOTO_LINE;
      run_line_editor(0,&s_goto_line_buf);
    }
  break;
  case '\r': //KEY_ENTER:
    //insert newline
    preSaveUndoState();

    if (m_selecting) { removeSelect(); draw(); setCursor(); }
    if (m_curs_y >= m_text.GetSize())
    {
      m_curs_y=m_text.GetSize();
      m_text.Add(new WDL_FastString);
    }
    if (s_overwrite)
    {
      WDL_FastString *s = m_text.Get(m_curs_y);
      int plen=0;
      const char *pb=NULL;
      if (s)
      {
        pb = s->Get();
        while (plen < m_curs_x && (pb[plen]== ' ' || pb[plen] == '\t')) plen++;
      }
      if (++m_curs_y >= m_text.GetSize())
      {
        m_curs_y = m_text.GetSize();
        WDL_FastString *ns=new WDL_FastString;
        if (plen>0) ns->Set(pb,plen);
        m_text.Insert(m_curs_y,ns);
      }
      s = m_text.Get(m_curs_y);
      if (s && plen > s->GetLength()) plen=s->GetLength();
      m_curs_x=s ? WDL_utf8_bytepos_to_charpos(s->Get(),plen) : plen;
    }
    else 
    {
      WDL_FastString *s = m_text.Get(m_curs_y);
      if (s)
      {
        int bytepos = WDL_utf8_charpos_to_bytepos(s->Get(),m_curs_x);
        if (CTRL_KEY_DOWN || bytepos > s->GetLength()) bytepos = s->GetLength();
        WDL_FastString *nl = new WDL_FastString();
        int plen=0;
        const char *pb = s->Get();
        while (pb[plen]== ' ' || pb[plen] == '\t') plen++;

        if (plen>0) nl->Set(pb,plen);

        const char *insert = pb+bytepos;
        while (*insert == ' ' || *insert == '\t') insert++;
        nl->Append(insert);
        m_text.Insert(++m_curs_y,nl);
        s->SetLen(bytepos);
        m_curs_x=WDL_utf8_bytepos_to_charpos(nl->Get(),plen);
      }
    }
    m_offs_x=0;

    draw();
    saveUndoState();
    setCursor();
  break;
  case '\t':
    if (m_selecting)
    {
      preSaveUndoState();

      bool isRev = !!(GetAsyncKeyState(VK_SHIFT)&0x8000);
      indentSelect(isRev?-m_indent_size:m_indent_size);
      // indent selection:
      draw();
      setCursor();
      saveUndoState();
      break;
    }
    else if (GetAsyncKeyState(VK_SHIFT)&0x8000)
    {
      if (m_curs_x > 0)
      {
        WDL_FastString *tl=m_text.Get(m_curs_y);
        if (tl)
        {
          int del_pos = m_curs_x, del_sz;

          for (del_sz = 0; del_sz < m_curs_x && tl->Get()[del_sz] == ' '; del_sz++);

          if (del_sz < m_curs_x && tl->Get()[WDL_utf8_charpos_to_bytepos(tl->Get(),m_curs_x - 1)] == ' ')
          {
            // spaces before cursor but not at start of line
            for (del_sz = 1;
                m_curs_x - del_sz - 1 >= 0 &&
                del_sz < wdl_max(m_indent_size,1) &&
                tl->Get()[WDL_utf8_charpos_to_bytepos(tl->Get(),m_curs_x - del_sz - 1)] == ' ';
                del_sz++);
          }
          else if (del_sz > 0)
          {
            // adjust leading indentation
            int rem = 1;
            if (m_indent_size > 0)
            {
              rem = del_sz % m_indent_size;
              if (!rem) rem = m_indent_size;
            }
            if (del_sz > rem) del_sz = rem;
            del_pos = del_sz;
          }

          if (del_sz > 0)
          {
            preSaveUndoState();
            const int xbyte = WDL_utf8_charpos_to_bytepos(tl->Get(),del_pos - del_sz);
            const int xbytesz=WDL_utf8_charpos_to_bytepos(tl->Get()+xbyte,del_sz);

            bool hadCom = LineCanAffectOtherLines(tl->Get(), xbyte,xbytesz);
            tl->DeleteSub(xbyte,xbytesz);
            m_curs_x-=del_sz;

            if (!hadCom) hadCom = LineCanAffectOtherLines(tl->Get(),xbyte,0);
            draw(hadCom?-1:m_curs_y);
            saveUndoState();
            setCursor();
          }
        }
      }
      break;
    }
    WDL_FALLTHROUGH;
  default:
    //insert char
    if(VALIDATE_TEXT_CHAR(c))
    { 
      preSaveUndoState();

      if (m_selecting) { removeSelect(); draw(); setCursor(); }
      if (!m_text.Get(m_curs_y)) m_text.Insert(m_curs_y,new WDL_FastString);

      WDL_FastString *ss;
      if ((ss=m_text.Get(m_curs_y)))
      {
        char str[64];
        int slen=1,slen_bytes=1;
        if (c == '\t') 
        {
          slen = wdl_min(m_indent_size,64);
          if (slen<1) slen=1;
          
          const int partial = m_curs_x % slen;
          if (partial) slen -= partial;

          int x; 
          for(x=0;x<slen;x++) str[x]=' ';
          slen_bytes=slen;
        }
#ifdef __APPLE__
        else if (c == 'n' && !SHIFT_KEY_DOWN && ALT_KEY_DOWN) 
        {
          str[0]='~';
        }
#endif
        else
        {
          slen_bytes = WDL_MakeUTFChar(str,c,32);
          str[slen_bytes]=0;
        }


        const int xbyte = WDL_utf8_charpos_to_bytepos(ss->Get(),m_curs_x);

        bool hadCom = LineCanAffectOtherLines(ss->Get(),xbyte,0);
        if (s_overwrite)
        {
          const int xbytesz_del=WDL_utf8_charpos_to_bytepos(ss->Get()+xbyte,slen);
          if (!hadCom) hadCom = LineCanAffectOtherLines(ss->Get(),xbyte,xbytesz_del);
          ss->DeleteSub(xbyte,xbytesz_del);
        }

        ss->Insert(str,xbyte,slen_bytes);
        if (!hadCom) hadCom = LineCanAffectOtherLines(ss->Get(),xbyte,slen_bytes);

        m_curs_x += slen;

        draw(hadCom ? -1 : m_curs_y);
      }
      saveUndoState();
      setCursor();
    }
    break;
  }
  return 0;
}

void WDL_CursesEditor::preSaveUndoState() 
{
  editUndoRec *rec= m_undoStack.Get(m_undoStack_pos);
  if (rec)
  {
    rec->m_offs_x[1]=m_offs_x;
    rec->m_curs_x[1]=m_curs_x;
    rec->m_curs_y[1]=m_curs_y;
    rec->m_curpaneoffs_y[1]=m_paneoffs_y[m_curpane];
  }
}
void WDL_CursesEditor::saveUndoState() 
{
  editUndoRec *rec=new editUndoRec;
  rec->m_offs_x[1]=rec->m_offs_x[0]=m_offs_x;
  rec->m_curs_x[1]=rec->m_curs_x[0]=m_curs_x;
  rec->m_curs_y[1]=rec->m_curs_y[0]=m_curs_y;
  rec->m_curpaneoffs_y[1]=rec->m_curpaneoffs_y[0]=m_paneoffs_y[m_curpane];

  editUndoRec *lrec[5];
  lrec[0] = m_undoStack.Get(m_undoStack_pos);
  lrec[1] = m_undoStack.Get(m_undoStack_pos+1);
  lrec[2] = m_undoStack.Get(m_undoStack_pos-1);
  lrec[3] = m_undoStack.Get(m_undoStack_pos-2);
  lrec[4] = m_undoStack.Get(m_undoStack_pos-3);

  int x;
  for (x = 0; x < m_text.GetSize(); x ++)
  {
    refcntString *ns = NULL;

    const WDL_FastString *s=m_text.Get(x);
    const int s_len=s?s->GetLength():0;
    
    if (s_len>0)
    {
      const char *cs=s?s->Get():"";
      int w;
      for(w=0;w<5 && !ns;w++)
      {
        if (lrec[w])
        {
          int y;
          for (y=0; y<15; y ++)
          {
            refcntString *a = lrec[w]->m_htext.Get(x + ((y&1) ? -(y+1)/2 : y/2));
            if (a && a->getStrLen() == s_len && !strcmp(a->getStr(),cs))
            {
              ns = a;
              break;
            }
          }
        }
      }

      if (!ns) ns = new refcntString(cs,s_len);

      ns->AddRef();
    }
    else
    {
      // ns is NULL, blank line
    }

    rec->m_htext.Add(ns);
  }

  for (x = m_undoStack.GetSize()-1; x > m_undoStack_pos; x --)
  {
    m_undoStack.Delete(x,true);
  }
  if (m_clean_undopos > m_undoStack_pos) 
    m_clean_undopos=-1;
  
  if (m_undoStack.GetSize() > m_max_undo_states) 
  {
    for (x = 1; x < m_undoStack.GetSize()/2; x += 2)
    {
      if (x != m_clean_undopos)// don't delete our preferred special (last saved) one
      {
        m_undoStack.Delete(x,true);
        if (m_clean_undopos > x) m_clean_undopos--;
        x--;
      }
    }

    m_undoStack_pos = m_undoStack.GetSize()-1;
  }
  m_undoStack_pos++;
  m_undoStack.Add(rec);
}

void WDL_CursesEditor::loadUndoState(editUndoRec *rec, int idx)
{
  int x;
  m_text.Empty(true);
  for (x = 0; x < rec->m_htext.GetSize(); x ++)
  {
    refcntString *s=rec->m_htext.Get(x);
    m_text.Add(new WDL_FastString(s?s->getStr():""));
  }

  m_offs_x=rec->m_offs_x[idx];
  m_curs_x=rec->m_curs_x[idx];
  m_curs_y=rec->m_curs_y[idx];

  m_paneoffs_y[m_curpane]=rec->m_curpaneoffs_y[idx];
  m_selecting=false;
}


void WDL_CursesEditor::RunEditor()
{
  WDL_DestroyCheck chk(&destroy_check);

  int x;
  for(x=0;x<16;x++)
  {
    if (!chk.isOK() || !CURSES_INSTANCE) break;

    int thischar = getch();
    if (thischar==ERR) break;

    if (onChar(thischar)) break;
  }
}

void WDL_CursesEditor::draw_top_line()
{
  int ypos=0;
  if (m_top_margin > 1)
  {
    int xpos=0;
    int x;
    move(ypos++,0);
    const int cnt= GetTabCount();
    int tsz=16;
    // this is duplicated in onMouseMessage
    if (cnt>0) tsz=COLS/cnt;
    if (tsz>128)tsz=128;
    if (tsz<12) tsz=12;

    for (x= 0; x < cnt && xpos < COLS; x ++)
    {
      WDL_CursesEditor *ed = GetTab(x);
      if (ed)
      {
        char buf[128 + 8];
        memset(buf,' ',tsz);
        const char *p = WDL_get_filepart(ed->GetFileName());
        const int lp=strlen(p);
        int skip=0;        
        if (x<9) 
        { 
          if (tsz>16)
          {
            const char *s = "<" CONTROL_KEY_NAME "+";
            memcpy(buf,s,skip = (int)strlen(s));
          }
          buf[skip++]='F'; 
          buf[skip++] = '1'+x; 
          buf[skip++] = '>';
          skip++;
        }
        memcpy(buf+skip,p,wdl_min(tsz-1-skip,lp));
        buf[tsz]=0;
        int l = tsz;
        if (l > COLS-xpos) l = COLS-xpos;
        if (ed == this)
        {
          attrset(SYNTAX_HIGHLIGHT2|A_BOLD);
        }
        else
        {
          attrset(A_NORMAL);
        }
        addnstr(buf,l);
        xpos += l;
      }
    }
    if (xpos < COLS) clrtoeol();
  }
  attrset(COLOR_TOPLINE|A_BOLD);
  bkgdset(COLOR_TOPLINE);
  const char *p=GetFileName();
  move(ypos,0);
  if (COLS>4)
  {
    const int pl = (int) strlen(p);
    if (pl > COLS-1 && COLS > 4)
    {
      addstr("...");
      p+=pl - (COLS-1) + 4;
    }
    addstr(p);
  }
  clrtoeol();
}

void WDL_CursesEditor::OpenFileInTab(const char *fnp)
{
  if (!fnp[0]) return;

  FILE *fp = fopen(fnp,"rb");
  if (!fp)
  {
    WDL_FastString s(fnp);
    m_newfn.Set(fnp);

    if (COLS > 25)
    {
      int allowed = COLS-25;
      if (s.GetLength()>allowed)
      {
        s.DeleteSub(0,s.GetLength() - allowed + 3);
        s.Insert("...",0);
      }
      s.Insert("Create new file '",0);
      s.Append("' (Y/n)? ");
    }
    else
      s.Set("Create new file (Y/n)? ");

    m_ui_state=UI_STATE_SAVE_AS_NEW;
    attrset(COLOR_MESSAGE);
    bkgdset(COLOR_MESSAGE);
    mvaddstr(LINES-1,0,s.Get());
    clrtoeol();
    attrset(0);
    bkgdset(0);
  }
  else
  {
    fclose(fp);
    int x;
    for (x=0;x<GetTabCount();x++)
    {
      WDL_CursesEditor *e = GetTab(x);
      if (e && !stricmp(e->GetFileName(),fnp))
      {
        SwitchTab(x,false);
        return;
      }
    }
    AddTab(fnp);
  }
}

void WDL_CursesEditor::do_paste_lines(WDL_PtrList<const char> &lines)
{
  WDL_FastString poststr;
  int x;
  int indent_to_pos = 0;
  int skip_source_indent=0; // number of characters of whitespace to (potentially) ignore when pasting

  for (x = 0; x < lines.GetSize(); x ++)
  {
    WDL_FastString *str=m_text.Get(m_curs_y);
    const char *tstr=lines.Get(x);
    if (!tstr) tstr="";
    if (!x)
    {
      if (str)
      {
        int bytepos = WDL_utf8_charpos_to_bytepos(str->Get(),m_curs_x);

        if (bytepos < 0) bytepos=0;
        int tmp=str->GetLength();
        if (bytepos > tmp) bytepos=tmp;

        poststr.Set(str->Get()+bytepos);
        str->SetLen(bytepos);

        if (bytepos > 0 && lines.GetSize()>1)
        {
          indent_to_pos = 0;
          while (str->Get()[indent_to_pos] == ' ') indent_to_pos++;

          skip_source_indent=1024;
          for (int i = 0; skip_source_indent > 0 && i < lines.GetSize(); i ++)
          {
            int a=0;
            const char *p = lines.Get(i);
            while (a < skip_source_indent && p[a] == ' ') a++;
            if (a < skip_source_indent && p[a]) skip_source_indent=a;
          }
        }

        str->Append(skip_indent(tstr,skip_source_indent));
      }
      else
      {
        m_text.Insert(m_curs_y,(str=new WDL_FastString(skip_indent(tstr,skip_source_indent))));
      }

      if (lines.GetSize() > 1)
      {
        m_curs_y++;
      }
      else
      {
        m_curs_x = WDL_utf8_get_charlen(str->Get());
        str->Append(poststr.Get());
      }
    }
    else if (x == lines.GetSize()-1)
    {
      WDL_FastString *s=newIndentedFastString(skip_indent(tstr,skip_source_indent),indent_to_pos);
      m_curs_x = WDL_utf8_get_charlen(s->Get());
      s->Append(poststr.Get());
      m_text.Insert(m_curs_y,s);
    }
    else
    {
      m_text.Insert(m_curs_y,newIndentedFastString(skip_indent(tstr,skip_source_indent),indent_to_pos));
      m_curs_y++;
    }
  }
}
