// Hyperbolic Rogue -- dialogs
// Copyright (C) 2011-2018 Zeno Rogue, see 'hyper.cpp' for details

/** \file dialogs.cpp
 *  \brief Implementation of various generic dialogs and elements of dialog windows
 */

#include "hyper.h"
namespace hr {

EX const char* COLORBAR = "###";

EX namespace dialog {

#if HDR
  #define IFM(x) (mousing?"":x)

  static const int DONT_SHOW = 16;

  enum tDialogItem {diTitle, diItem, diBreak, diHelp, diInfo, diIntSlider, diSlider, diBigItem, diKeyboard};

  struct item {
    tDialogItem type;
    string body;
    string value;
    int key;
    color_t color, colorv, colork, colors, colorc;
    int scale;
    double param;
    int p1, p2, p3;
    int position;
    };
  
  struct scaler {
    ld (*direct) (ld);
    ld (*inverse) (ld);
    bool positive;
    };

  static inline ld identity_f(ld x) { return x; }
  
  const static scaler identity = {identity_f, identity_f, false};
  const static scaler logarithmic = {log, exp, true};
  const static scaler asinhic = {asinh, sinh, false};
  const static scaler asinhic100 = {[] (ld x) { return asinh(x*100); }, [] (ld x) { return sinh(x)/100; }, false};
 
  struct numberEditor {
    ld *editwhat;
    string s;
    ld vmin, vmax, step, dft;
    string title, help;
    scaler sc;
    int *intval; ld intbuf;
    bool animatable;
    };

  extern numberEditor ne;

  inline void scaleLog() { ne.sc = logarithmic; }
  inline void scaleSinh() { ne.sc = asinhic; }
  inline void scaleSinh100() { ne.sc = asinhic100; }  
#endif

  EX color_t dialogcolor = 0xC0C0C0;

  EX void addBack() {
    addItem(XLAT("go back"), SDLK_ESCAPE);
    }

  EX void addHelp() {
    addItem(XLAT("help"), SDLK_F1);
    }

  EX namespace zoom {
    int zoomf = 1, shiftx, shifty;
    bool zoomoff = false;

    void nozoom() {
      zoomf = 1; shiftx = 0; shifty = 0; zoomoff = false;
      }
    
    void initzoom() {
      zoomf = 3;
      shiftx = -2*mousex;
      if(mousex < vid.xres / 6) shiftx = 0;
      if(mousex > vid.xres * 5 / 6) shiftx = -2 * vid.xres;
      shifty = -2*mousey;
      if(mousey < vid.yres / 6) shifty = 0;
      if(mousey > vid.yres * 5 / 6) shifty = -2 * vid.yres;
      }
    
    void stopzoom() { zoomoff = true; }

    EX bool displayfr(int x, int y, int b, int size, const string &s, color_t color, int align) {
      return hr::displayfr(x * zoomf + shiftx, y * zoomf + shifty, b, size * zoomf, s, color, align);
      }
  
    EX bool displayfr_highlight(int x, int y, int b, int size, const string &s, color_t color, int align, int hicolor IS(0xFFFF00)) {
      bool clicked = hr::displayfr(x * zoomf + shiftx, y * zoomf + shifty, b, size * zoomf, s, color, align);
      if(clicked) hr::displayfr(x * zoomf + shiftx, y * zoomf + shifty, b, size * zoomf, s, hicolor, align);
      return clicked;
      }
    EX }

#if CAP_MENUSCALING && CAP_SDL
  EX void handleZooming(SDL_Event &ev) {
    using namespace zoom;
    if(zoomoff || !(cmode & sm::ZOOMABLE)) { 
      nozoom(); return; 
      }
    if(ev.type == SDL_MOUSEBUTTONDOWN) initzoom();
    if(ev.type == SDL_MOUSEBUTTONUP && zoomf > 1) stopzoom();
    }
#endif
#if !(CAP_MENUSCALING && CAP_SDL)
  EX void handleZooming(SDL_Event &ev) {}
#endif
  
  EX vector<item> items;
  
  EX item& lastItem() { return items[items.size() - 1]; }

  EX item& titleItem() { return items[0]; }
  
  EX map<int, reaction_t> key_actions;
  
  EX void add_key_action(int key, const reaction_t& action) {
    key_actions[key] = action;
    }
  
  EX void add_key_action_adjust(int& key, const reaction_t& action) {
    while(key_actions.count(key)) key++;
    add_key_action(key, action);
    }
  
  EX void add_action(const reaction_t& action) {
    add_key_action_adjust(lastItem().key, action);
    }
  
  EX void add_action_push(const reaction_t& action) { add_action([action] { pushScreen(action); }); }

  EX void handler(int sym, int uni) {
    dialog::handleNavigation(sym, uni);
    if(doexiton(sym, uni)) popScreen();
    }

  EX void init() {
    items.clear();
    key_actions.clear(); 
    keyhandler = dialog::handler;
    }
  
  EX string keyname(int k) {
    if(k == 0) return "";
    if(k == SDLK_ESCAPE) return "Esc";
    if(k == SDLK_F5) return "F5";
    if(k == SDLK_F10) return "F10";
    if(k == SDLK_F1) return "F1";
    if(k == SDLK_HOME) return "Home";
    if(k == SDLK_BACKSPACE) return "Backspace";
    if(k == SDLK_RETURN) return "Enter";
    if(k == 32) return "space";
    if(k >= 1 && k <= 26) { string s = "Ctrl+"; s += (k+64); return s; }
    if(k < 128) { string s; s += k; return s; }
    return "?";
    }

  EX void addSlider(double d1, double d2, double d3, int key) {
    item it;
    it.type = diSlider;
    it.color = dialogcolor;
    it.scale = 100;
    it.key = key;
    it.param = (d2-d1) / (d3-d1);
    items.push_back(it);
    }
  
  EX void addIntSlider(int d1, int d2, int d3, int key) {
    item it;
    it.type = diIntSlider;
    it.color = dialogcolor;
    it.scale = 100;
    it.key = key;
    it.p1 = (d2-d1);
    it.p2 = (d3-d1);
    items.push_back(it);
    }
  
  EX void addSelItem(string body, string value, int key) {
    item it;
    it.type = diItem;
    it.body = body;
    it.value = value;
    it.key = key;
    it.color = dialogcolor;
    it.colork = 0x808080;
    it.colorv = 0x80A040;
    it.colorc = 0xFFD500;
    it.colors = 0xFF8000;
    if(value == ONOFF(true)) it.colorv = 0x40FF40;
    if(value == ONOFF(false)) it.colorv = 0xC04040;
    it.scale = 100;
    items.push_back(it);
    }

  EX void addBoolItem(string body, bool value, int key) {
    addSelItem(body, ONOFF(value), key);
    }
  
  EX int displaycolor(color_t col) {
    int c = col >> 8;
    if(!c) return 0x181818;
    return c;
    }

  EX void addKeyboardItem(string keys) {
    item it;
    it.type = diKeyboard;
    it.body = keys;
    it.color = dialogcolor;
    it.colors = 0xFF8000;
    it.scale = 150;
    items.push_back(it);
    }

  EX void addColorItem(string body, int value, int key) {
    item it;
    it.type = diItem;
    it.body = body;
    it.value = COLORBAR;
    it.key = key;
    it.color = it.colorv = displaycolor(value);
    it.colors = it.color ^ 0x404040;
    it.colorc = it.color ^ 0x808080;
    it.colork = 0x808080;
    it.scale = 100;
    items.push_back(it);
    }

  EX void addHelp(string body) {
    item it;
    it.type = diHelp;
    it.body = body;
    it.scale = 100;

    if(isize(body) >= 500) it.scale = 70;

    items.push_back(it);
    }

  EX void addInfo(string body, color_t color IS(dialogcolor)) {
    item it;
    it.type = diInfo;
    it.body = body;
    it.color = color;
    it.scale = 100;
    items.push_back(it);
    }

  EX void addItem(string body, int key) {
    item it;
    it.type = diItem;
    it.body = body;
    it.key = key;
    it.color = dialogcolor;
    it.colork = 0x808080;
    it.colors = 0xFFD500;
    it.colorc = 0xFF8000;
    it.scale = 100;
    items.push_back(it);
    }

  EX void addBigItem(string body, int key) {
    item it;
    it.type = diBigItem;
    it.body = body;
    it.key = key;
    it.color = 0xC06000;
    it.colors = 0xFFD500;
    it.colorc = 0xFF8000;
    it.scale = 150;
    items.push_back(it);
    }

  EX int addBreak(int val) {
    item it;
    it.type = diBreak;
    it.scale = val;
    items.push_back(it);
    return items.size()-1;
    }
  
  EX void addTitle(string body, color_t color, int scale) {
    item it;
    it.type = diTitle;
    it.body = body;
    it.color = color;
    it.scale = scale;
    items.push_back(it);
    }

  EX void init(string title, color_t color IS(0xE8E8E8), int scale IS(150), int brk IS(60)) { 
    init();
    addTitle(title, color, scale);
    addBreak(brk);
    }

  EX int dcenter, dwidth;

  EX int dialogflags;

  EX int displayLong(string str, int siz, int y, bool measure) {

    int last = 0;
    int lastspace = 0;
    
    int xs, xo;
    if(current_display->sidescreen)
      xs = dwidth - vid.fsize*2, xo = vid.yres + vid.fsize;
    else
      xs = vid.xres * 618/1000, xo = vid.xres * 186/1000;
    
    for(int i=0; i<=isize(str); i++) {
      int ls = 0;
      int prev = last;
      if(str[i] == ' ') lastspace = i;
      if(textwidth(siz, str.substr(last, i-last)) > xs) {
        if(lastspace == last) ls = i-1, last = i-1;
        else ls = lastspace, last = ls+1;
        }
      if(str[i] == 10 || i == isize(str)) ls = i, last = i+1;
      if(ls) {
        if(!measure) displayfr(xo, y, 2, siz, str.substr(prev, ls-prev), dialogcolor, 0);
        if(ls == prev) y += siz/2;
        else y += siz;
        lastspace = last;
        }
      
      }
    
    y += siz/2;
    return y;
    }

  EX int tothei, dialogwidth, dfsize, dfspace, leftwidth, rightwidth, innerwidth, itemx, keyx, valuex;
  
  EX string highlight_text;
  
  EX void measure() {
    tothei = 0;
    dialogwidth = 0;
    innerwidth = 0;
    int N = items.size();
    for(int i=0; i<N; i++) {
      if(items[i].type == diHelp)
        tothei += displayLong(items[i].body, dfsize * items[i].scale / 100, 0, true);
      else {
        tothei += dfspace * items[i].scale / 100;
        if(items[i].type == diItem) 
          innerwidth = max(innerwidth, textwidth(dfsize * items[i].scale / 100, items[i].body));
        if(items[i].type == diTitle || items[i].type == diInfo || items[i].type == diBigItem)
          dialogwidth = max(dialogwidth, textwidth(dfsize * items[i].scale / 100, items[i].body) * 10/9);
        }
      }
    
    leftwidth = ISMOBILE ? 0 : textwidth(dfsize, "MMMMM") + dfsize/2;
    rightwidth = textwidth(dfsize, "MMMMMMMM") + dfsize/2;
    
    int fwidth = innerwidth + leftwidth + rightwidth;
    dialogwidth = max(dialogwidth, fwidth);
    itemx  = dcenter - fwidth / 2 + leftwidth;
    keyx   = dcenter - fwidth / 2 + leftwidth - dfsize/2;
    valuex = dcenter - fwidth / 2 + leftwidth + innerwidth + dfsize/2;
    }

  EX purehookset hooks_display_dialog;
  
  EX void display() {
    callhooks(hooks_display_dialog);
    int N = items.size();
    dfsize = vid.fsize;
    #if ISMOBILE || ISPANDORA
    dfsize *= 3;
    #endif
    dfspace = dfsize * 5/4;
    
    dcenter = vid.xres/2;
    dwidth = vid.xres;

    if(current_display->sidescreen) {
      dwidth = vid.xres - vid.yres;
      dcenter = vid.xres - dwidth / 2;
      }
    
    measure();
    
    while(tothei > vid.yres - 5 * vid.fsize) {
      int adfsize = int(dfsize * sqrt((vid.yres - 5. * vid.fsize) / tothei));
      if(adfsize < dfsize-1) dfsize = adfsize + 1;
      else dfsize--;
      dfspace = dfsize * 5/4;
      measure();
      }
    while(dialogwidth > dwidth) {
      int adfsize = int(dfsize * sqrt(vid.xres * 1. / dialogwidth));
      if(adfsize < dfsize-1) dfsize = adfsize + 1;
      else dfsize--; // keep dfspace
      measure();
      }
    
    tothei = (vid.yres - tothei) / 2;
    
    for(int i=0; i<N; i++) {
      item& I = items[i];

      if(I.type == diHelp) {
        tothei = displayLong(items[i].body, dfsize * items[i].scale / 100, tothei, false);
        continue;
        }
        
      int top = tothei;
      tothei += dfspace * I.scale / 100;
      int mid = (top + tothei) / 2;
      I.position = mid;
      if(I.type == diTitle || I.type == diInfo) {
        bool xthis = (mousey >= top && mousey < tothei && I.key);
        displayfr(dcenter, mid, 2, dfsize * I.scale/100, I.body, I.color, 8);
        if(xthis) getcstat = I.key;
        }
      else if(I.type == diItem || I.type == diBigItem) {
        bool xthis = (mousey >= top && mousey < tothei);
        if(cmode & sm::DIALOG_STRICT_X)
          xthis = xthis && (mousex >= dcenter - dialogwidth/2 && mousex <= dcenter + dialogwidth/2);
#if ISMOBILE
        if(xthis && mousepressed) 
          I.color = I.colorc;
#else
        if(xthis && mousemoved) {
          highlight_text = I.body;
          mousemoved = false;
          }
        if(highlight_text == I.body) {
          I.color = (xthis&&mousepressed&&actonrelease) ? I.colorc : I.colors;
          }
#endif        
        if(I.type == diBigItem) {
          displayfr(dcenter, mid, 2, dfsize * I.scale/100, I.body, I.color, 8);
          }
        else {
          if(!mousing)
            displayfr(keyx, mid, 2, dfsize * I.scale/100, keyname(I.key), I.colork, 16);
          displayfr(itemx, mid, 2, dfsize * I.scale/100, I.body, I.color, 0);
          int siz = dfsize * I.scale/100;
          while(siz > 6 && textwidth(siz, I.value) >= vid.xres - valuex) siz--;
          displayfr(valuex, mid, 2, siz, I.value, I.colorv, 0);
          }
        if(xthis) getcstat = I.key;
        }      
      else if(among(I.type, diSlider, diIntSlider)) {
        bool xthis = (mousey >= top && mousey < tothei);
        int sl, sr;
        if(current_display->sidescreen)
          sl = vid.yres + vid.fsize*2, sr = vid.xres - vid.fsize*2;
        else
          sl = vid.xres/4, sr = vid.xres*3/4;
        int sw = sr-sl;
        if(I.type == diSlider) {
          displayfr(sl, mid, 2, dfsize * I.scale/100, "(", I.color, 16);
          displayfr(sl + double(sw * I.param), mid, 2, dfsize * I.scale/100, "#", I.color, 8);
          displayfr(sr, mid, 2, dfsize * I.scale/100, ")", I.color, 0);
          }
        else {
          displayfr(sl, mid, 2, dfsize * I.scale/100, "{", I.color, 16);
          if(I.p2 < sw / 4) for(int a=0; a<=I.p2; a++) if(a != I.p1)
            displayfr(sl + double(sw * a / I.p2), mid, 2, dfsize * I.scale/100, a == I.p1 ? "#" : ".", I.color, 8);
          displayfr(sl + double(sw * I.p1 / I.p2), mid, 2, dfsize * I.scale/100, "#", I.color, 8);
          displayfr(sr, mid, 2, dfsize * I.scale/100, "}", I.color, 0);
          }
        if(xthis) getcstat = I.key, inslider = true;
        }
      else if(I.type == diKeyboard) {
        int len = 0;
        for(char c: I.body) 
          if(c == ' ' || c == '\t') len += 3;
          else len++;
        int sl, sr;
        if(current_display->sidescreen)
          sl = vid.yres + vid.fsize*2, sr = vid.xres - vid.fsize*2;
        else
          sl = vid.xres/4, sr = vid.xres*3/4;
        int pos = 0;
        for(char c: I.body) {
          string s = "";
          s += c;
          int nlen = 1;
          if(c == ' ') s = "SPACE", nlen = 3;
          if(c == '\b') s = "⌫", nlen = 1;
          if(c == '\r' || c == '\n') s = "⏎", nlen = 1;
          if(c == 1) s = "←", nlen = 1;
          if(c == 2) s = "→", nlen = 1;
          if(c == 3) s = "π";
          if(c == '\t') s = "CLEAR", nlen = 3;
          int left = sl + (sr-sl) * pos / len;
          pos += nlen;
          int right = sl + (sr-sl) * pos / len;
          bool in = (mousex >= left && mousex <= right && mousey >= top && mousey < tothei);
          int xpos = (left + right) / 2;
          if(in) {
            if(c == 1) getcstat = SDLK_LEFT;
            else if(c == 2) getcstat = SDLK_RIGHT;
            else getcstat = c;
            }
          displayfr(xpos, mid, 2, dfsize * I.scale/100, s, in ? I.colors : I.color, 8);
          }
        }
      }
    }

  bool isitem(item& it) {
    return it.type == diItem || it.type == diBigItem;
    }
  
  EX void handleNavigation(int &sym, int &uni) {
    if(uni == '\n' || uni == '\r' || DIRECTIONKEY == SDLK_KP5)
      for(int i=0; i<isize(items); i++) 
        if(isitem(items[i]))
          if(items[i].body == highlight_text) {
            uni = sym = items[i].key;
            return;
            }
    if(DKEY == SDLK_PAGEDOWN) {
      for(int i=0; i<isize(items); i++)
        if(isitem(items[i]))
          highlight_text = items[i].body;
      }
    if(DKEY == SDLK_PAGEUP) {
      for(int i=0; i<isize(items); i++) 
        if(isitem(items[i])) {
          highlight_text = items[i].body;
          break;
          }
      }    
    if(DKEY == SDLK_UP) {
      string last = "";
      for(int i=0; i<isize(items); i++) 
        if(isitem(items[i]))
          last = items[i].body;
      uni = sym = 0;
      for(int i=0; i<isize(items); i++)
        if(isitem(items[i])) {
          if(items[i].body == highlight_text) {
            highlight_text = last; return;
            }
          else last = items[i].body;
          }
      highlight_text = last;
      }
    if(DKEY == SDLK_DOWN) {
      int state = 0;
      for(int i=0; i<isize(items); i++)
        if(isitem(items[i])) {
          if(state) { highlight_text = items[i].body; return; }
          else if(items[i].body == highlight_text) state = 1;
          }
      for(int i=0; i<isize(items); i++)
        if(isitem(items[i])) 
          highlight_text = items[i].body;
      uni = sym = 0;
      }
    if(key_actions.count(uni)) {
      key_actions[uni]();
      sym = uni = 0;
      return;
      }
    if(key_actions.count(sym)) {
      key_actions[sym]();
      sym = uni = 0;
      return;
      }
    }

  color_t colorhistory[10] = {
    0x202020FF, 0x800000FF, 0x008000FF, 0x000080FF, 
    0x404040FF, 0xC0C0C0FF, 0x804000FF, 0xC0C000FF,
    0x408040FF, 0xFFD500FF
    }, lch;
  
  EX color_t *palette;
  
  int colorp = 0;
  
  color_t *colorPointer;
  
  EX void handleKeyColor(int sym, int uni) {
    unsigned& color = *colorPointer;
    int shift = colorAlpha ? 0 : 8;

    if(uni >= 'A' && uni <= 'D') {
      int x = (mousex - (dcenter-dwidth/4)) * 510 / dwidth;
      if(x < 0) x = 0;
      if(x > 255) x = 255;
      part(color, uni - 'A') = x;
      }
    else if(uni == ' ' || uni == '\n' || uni == '\r') {
      bool inHistory = false;
      for(int i=0; i<10; i++) if(colorhistory[i] == (color << shift))
        inHistory = true;
      if(!inHistory) { colorhistory[lch] = (color << shift); lch++; lch %= 10; }
      popScreen();
      if(reaction) reaction();
      if(reaction_final) reaction_final();
      }
    else if(uni >= '0' && uni <= '9') {
      color = colorhistory[uni - '0'] >> shift;
      }
    else if(palette && uni >= 'a' && uni < 'a'+(int) palette[0]) {
      color = palette[1 + uni - 'a'] >> shift;
      }
    else if(DKEY == SDLK_DOWN) {
      colorp = (colorp-1) & 3;
      }
    else if(DKEY == SDLK_UP) {
      colorp = (colorp+1) & 3;
      }
    else if(DKEY == SDLK_LEFT) {
      unsigned char* pts = (unsigned char*) &color;
      pts[colorp] -= abs(shiftmul) < .6 ? 1 : 17;
      }
    else if(DKEY == SDLK_RIGHT) {
      unsigned char* pts = (unsigned char*) &color;
      pts[colorp] += abs(shiftmul) < .6 ? 1 : 17;
      }
    else if(doexiton(sym, uni)) {
      popScreen();
      if(reaction_final) reaction_final();
      }
    }
  
  EX bool colorAlpha;
  
  EX void drawColorDialog() {
    cmode = sm::NUMBER | dialogflags;
    if(cmode & sm::SIDE) gamescreen(0);

    dcenter = vid.xres/2;
    dwidth = vid.xres;

    if(current_display->sidescreen) {
      dwidth = vid.xres - vid.yres;
      dcenter = vid.xres - dwidth / 2;
      }

    color_t color = *colorPointer;
    
    int ash = 8;
    
    for(int j=0; j<10; j++) {
      int x = dcenter + vid.fsize * 2 * (j-5);
      int y = vid.yres / 2- 5 * vid.fsize;
      
      string s0 = ""; s0 += ('0'+j);

      vid.fsize *= 2;
      displayColorButton(x, y, s0, '0'+j, 0, 0, colorhistory[j] >> ash);
      vid.fsize /= 2;
      }
    
    if(palette) {
      int q = palette[0]; 
      for(int i=0; i<q; i++) {
        int x = dcenter + vid.fsize * (2 * i-q);
        int y = vid.yres / 2- 7 * vid.fsize;
        string s0 = ""; s0 += ('a'+i);
        vid.fsize *= 2;
        displayColorButton(x, y, s0, 'a'+i, 0, 0, palette[i+1] >> ash);
        vid.fsize /= 2;
        }
      }

    for(int i=0; i<4; i++) {
      int y = vid.yres / 2 + (2-i) * vid.fsize * 2;
      if(i == 3 && !colorAlpha) continue;
      
      color_t col = ((i==colorp) && !mousing) ? 0xFFD500 : dialogcolor;

      displayColorButton(dcenter - dwidth/4, y, "(", 0, 16, 0, col);
      string rgt = ") "; rgt += "ABGR" [i+(colorAlpha?0:1)];
      displayColorButton(dcenter + dwidth/4, y, rgt, 0, 0, 0, col);
      displayColorButton(dcenter - dwidth/4 + dwidth * part(color, i) / 510, y, "#", 0, 8, 0, col);
      
      if(mousey >= y - vid.fsize && mousey < y + vid.fsize)
        getcstat = 'A' + i, inslider = true;
      }
    
    displayColorButton(dcenter, vid.yres/2+vid.fsize * 6, XLAT("select this color") + " : " + itsh(color), ' ', 8, 0, color >> (colorAlpha ? ash : 0));

    if(extra_options) extra_options();
    
    keyhandler = handleKeyColor;
    }
  
  EX void openColorDialog(unsigned int& col, unsigned int *pal IS(palette)) {
    colorPointer = &col; palette = pal;
    colorAlpha = true;
    dialogflags = 0;
    pushScreen(drawColorDialog);
    reaction = reaction_t();
    extra_options = reaction_t();
    }
  
  EX numberEditor ne;
  
  EX bool editingDetail() {
    return ne.editwhat == &vid.highdetail || ne.editwhat == &vid.middetail;
    }
  
  int ldtoint(ld x) {
    if(x > 0) return int(x+.5);
    else return int(x-.5);
    }
  
  EX string disp(ld x) { 
    if(dialogflags & sm::HEXEDIT) return "0x" + itsh((unsigned long long)(x));
    else if(ne.intval) return its(ldtoint(x)); 
    else return fts(x); }

  EX reaction_t reaction;
  EX reaction_t reaction_final;
  
  EX reaction_t extra_options;
  
  EX void apply_slider() {
    if(ne.intval) *ne.intval = ldtoint(*ne.editwhat);
    if(reaction) reaction();
    if(ne.intval) *ne.editwhat = *ne.intval;
    ne.s = disp(*ne.editwhat);
    #if CAP_ANIMATIONS
    anims::deanimate(*ne.editwhat);
    #endif
    }
  
  EX void use_hexeditor() {
    dialogflags |= sm::HEXEDIT;
    ne.s = disp(*ne.editwhat);
    }
  
  EX void apply_edit() {
    try {
      exp_parser ep;
      ep.s = ne.s;    
      ld x = ep.rparse();
      if(ne.sc.positive && x <= 0) return;
      *ne.editwhat = x;
      if(ne.intval) *ne.intval = ldtoint(*ne.editwhat);
      #if CAP_ANIMATIONS
      if(ne.animatable) anims::animate_parameter(*ne.editwhat, ne.s, reaction ? reaction : reaction_final);    
      #endif
      if(reaction) reaction();
      }
    catch(hr_parse_exception&) { 
      }
    }

  EX void bound_low(ld val) {
    auto r = reaction;
    reaction = [r, val] () {
      if(*ne.editwhat < val) {
        *ne.editwhat = val;
        if(ne.intval) *ne.intval = ldtoint(*ne.editwhat);
        }
      if(r) r();
      };
    }

  EX void bound_up(ld val) {
    auto r = reaction;
    reaction = [r, val] () {
      if(*ne.editwhat > val) {
        *ne.editwhat = val;
        if(ne.intval) *ne.intval = ldtoint(*ne.editwhat);
        }
      if(r) r();
      };
    }

  EX int numberdark;

  EX void formula_keyboard(bool lr) {
    addKeyboardItem("1234567890");
    addKeyboardItem("=+-*/^()\x3");
    addKeyboardItem("qwertyuiop");
    addKeyboardItem("asdfghjkl");
    addKeyboardItem("zxcvbnm,.\b");
    addKeyboardItem(lr ? " \t\x1\x2" : " \t");
    }
  
  EX void drawNumberDialog() {
    cmode = sm::NUMBER | dialogflags;
    if(numberdark < DONT_SHOW)
    gamescreen(numberdark);
    init(ne.title);
    addInfo(ne.s);
    if(ne.intval && ne.sc.direct == &identity_f)
      addIntSlider(int(ne.vmin), int(*ne.editwhat), int(ne.vmax), 500);
    else
      addSlider(ne.sc.direct(ne.vmin), ne.sc.direct(*ne.editwhat), ne.sc.direct(ne.vmax), 500);
    addBreak(100);
#if !ISMOBILE
    addHelp(XLAT("You can scroll with arrow keys -- Ctrl to fine-tune"));
    addBreak(100);
#endif
    
    dialog::addBack();
    addSelItem(XLAT("default value"), disp(ne.dft), SDLK_HOME);

    addBreak(100);
    
    if(ne.help != "") {
      addHelp(ne.help);
      // bool scal = !ISMOBILE && !ISPANDORA && isize(ne.help) > 160;
      // if(scal) lastItem().scale = 30;
      }

    if(extra_options) extra_options();
    
    addBreak(100);
    formula_keyboard(false);
    
    display();
    
    keyhandler = [] (int sym, int uni) {
      handleNavigation(sym, uni);
      if((uni >= '0' && uni <= '9') || among(uni, '.', '+', '-', '*', '/', '^', '(', ')', ',', 3) || (uni >= 'a' && uni <= 'z')) {
        if(uni == 3) ne.s += "pi";
        else ne.s += uni;
        apply_edit();
        }
      else if(uni == '\b' || uni == '\t') {
        ne.s = ne.s. substr(0, isize(ne.s)-1);
        sscanf(ne.s.c_str(), LDF, ne.editwhat);
        apply_edit();
        }
  #if !ISMOBILE
      else if(DKEY == SDLK_RIGHT) {
        if(ne.intval && abs(shiftmul) < .6)
          (*ne.editwhat)++;
        else
          *ne.editwhat = ne.sc.inverse(ne.sc.direct(*ne.editwhat) + shiftmul * ne.step);
        if(abs(*ne.editwhat) < ne.step * 1e-6 && !ne.intval) *ne.editwhat = 0;
        apply_slider();
        }
      else if(DKEY == SDLK_LEFT) {
        if(ne.intval && abs(shiftmul) < .6)
          (*ne.editwhat)--;
        else
          *ne.editwhat = ne.sc.inverse(ne.sc.direct(*ne.editwhat) - shiftmul * ne.step);
        if(abs(*ne.editwhat) < ne.step * 1e-6 && !ne.intval) *ne.editwhat = 0;
        apply_slider();
        }
  #endif
      else if(sym == SDLK_HOME) {
        *ne.editwhat = ne.dft;
        apply_slider();
        }
      else if(uni == 500) {
        int sl, sr;
        if(current_display->sidescreen)
          sl = vid.yres + vid.fsize*2, sr = vid.xres - vid.fsize*2;
        else
          sl = vid.xres/4, sr = vid.xres*3/4;
        ld d = (mousex - sl + .0) / (sr-sl);
        ld val = ne.sc.inverse(d * (ne.sc.direct(ne.vmax) - ne.sc.direct(ne.vmin)) + ne.sc.direct(ne.vmin));
        ld nextval = ne.sc.inverse((mousex + 1. - sl) / (sr - sl) * (ne.sc.direct(ne.vmax) - ne.sc.direct(ne.vmin)) + ne.sc.direct(ne.vmin));
        ld dif = abs(val - nextval);
        if(dif > 1e-6) {
          ld mul = 1;
          while(dif < 10) dif *= 10, mul *= 10;
          val *= mul;
          val = floor(val + 0.5);
          val /= mul;
          }
        *ne.editwhat = val;
          
        apply_slider();
        }
      else if(doexiton(sym, uni)) { popScreen(); if(reaction_final) reaction_final(); }
      };
    }  

  int nlpage = 1;
  int wheelshift = 0;
  
  EX int handlePage(int& nl, int& nlm, int perpage) {
    nlm = nl;
    int onl = nl;
    int ret = 0;
    if(nlpage) {
      nl = nlm = perpage; 
      if(nlpage == 2) ret = nlm;
      int w = wheelshift;
      int realw = 0;
      while(w<0 && ret) {
        ret--; w++; realw--;
        }
      while(w>0 && ret+perpage < onl) {
        ret++; w--; realw++;
        }
      wheelshift = realw;
      if(ret+nl > onl) nl = onl-ret;
      }
    return ret;
    }
  
  EX void displayPageButtons(int i, bool pages) {
    int i0 = vid.yres - vid.fsize;
    int xr = vid.xres / 80;
    if(pages) if(displayfrZH(xr*8, i0, 1, vid.fsize, IFM("1 - ") + XLAT("page") + " 1", nlpage == 1 ? 0xD8D8C0 : dialogcolor, 8))
      getcstat = '1';
    if(pages) if(displayfrZH(xr*24, i0, 1, vid.fsize, IFM("2 - ") + XLAT("page") + " 2", nlpage == 1 ? 0xD8D8C0 : dialogcolor, 8))
      getcstat = '2';
    if(pages) if(displayfrZH(xr*40, i0, 1, vid.fsize, IFM("3 - ") + XLAT("all"), nlpage == 1 ? 0xD8D8C0 : dialogcolor, 8))
      getcstat = '3';
    if(i&1) if(displayfrZH(xr*56, i0, 1, vid.fsize, IFM(keyname(SDLK_ESCAPE) + " - ") + XLAT("go back"), dialogcolor, 8))
      getcstat = '0';
    if(i&2) if(displayfrZH(xr*72, i0, 1, vid.fsize, IFM("F1 - ") + XLAT("help"), dialogcolor, 8))
      getcstat = SDLK_F1;
    }
  
  EX bool handlePageButtons(int uni) {
    if(uni == '1') nlpage = 1, wheelshift = 0;
    else if(uni == '2') nlpage = 2, wheelshift = 0;
    else if(uni == '3') nlpage = 0, wheelshift = 0;
    else if(uni == PSEUDOKEY_WHEELUP) wheelshift--;
    else if(uni == PSEUDOKEY_WHEELDOWN) wheelshift++;
    else return false;
    return true;
    }
  
  EX void editNumber(ld& x, ld vmin, ld vmax, ld step, ld dft, string title, string help) {
    ne.editwhat = &x;
    ne.s = disp(x);
    ne.vmin = vmin;
    ne.vmax = vmax;
    ne.step = step;
    ne.dft = dft;
    ne.title = title;
    ne.help = help;
    ne.sc = identity;
    ne.intval = NULL;
    dialogflags = 0;
    if(cmode & sm::SIDE) dialogflags |= sm::MAYDARK | sm::SIDE;
    cmode |= sm::NUMBER;
    pushScreen(drawNumberDialog);
    reaction = reaction_t();
    reaction_final = reaction_t();
    extra_options = reaction_t();
    numberdark = 0;
    ne.animatable = true;
    #if CAP_ANIMATIONS
    anims::get_parameter_animation(x, ne.s);
    #endif
    }

  EX void editNumber(int& x, int vmin, int vmax, ld step, int dft, string title, string help) {
    editNumber(ne.intbuf, vmin, vmax, step, dft, title, help);
    ne.intbuf = x; ne.intval = &x; ne.s = its(x);
    ne.animatable = false;
    }
  
  EX void helpToEdit(int& x, int vmin, int vmax, int step, int dft) {
    popScreen();
    string title = "help";
    if(help[0] == '@') {
      int iv = help.find("\t");
      int id = help.find("\n");
      title = help.substr(iv+1, id-iv-1);
      help = help.substr(id+1);
      }
    editNumber(x, vmin, vmax, step, dft, title, help);
    }
  
  //-- choose file dialog--

  #define CDIR dialogcolor
  #define CFILE forecolor
  
  bool filecmp(const pair<string,color_t> &f1, const pair<string,color_t> &f2) {
    if(f1.first == "../") return true;
    if(f2.first == "../") return false;
    if(f1.second != f2.second)
      return f1.second == CDIR;
    return f1.first < f2.first;
    }
  
  string filecaption, cfileext;
  string *cfileptr;
  bool editext = false;
  
  bool_reaction_t file_action;
  
  void handleKeyFile(int sym, int uni);

  EX void drawFileDialog() {
    displayfr(vid.xres/2, 30 + vid.fsize, 2, vid.fsize, 
      filecaption, forecolor, 8);
      
    string& cfile = *cfileptr;

    displayfr(vid.xres/2, 34 + vid.fsize * 2, 2, vid.fsize, 
      cfile, 0xFFFF00, 8);
    
    displayColorButton(vid.xres*1/4, 38+vid.fsize * 3, 
      XLAT("F4 = extension"), SDLK_F4, 8, 0, editext ? 0xFF00FF : 0xFFFF00, editext ? 0x800080 : 0x808000);
    displayButton(vid.xres*2/4, 38+vid.fsize * 3, 
      XLAT("Enter = choose"), SDLK_RETURN, 8);
    displayButton(vid.xres*3/4, 38+vid.fsize * 3, 
      XLAT("Esc = cancel"), SDLK_ESCAPE, 8);

    v.clear();
    
    DIR           *d;
    struct dirent *dir;
    
    string where = ".";
    for(int i=0; i<isize(cfile); i++)
      if(cfile[i] == '/' || cfile[i] == '\\')
        where = cfile.substr(0, i+1);
    
    d = opendir(where.c_str());
    if (d) {
      while ((dir = readdir(d)) != NULL) {
        string s = dir->d_name;
        if(s != ".." && s[0] == '.') ;
        else if(isize(s) > 4 && s.substr(isize(s)-4) == cfileext)
          v.push_back(make_pair(s, CFILE));
        else if(dir->d_type & DT_DIR)
          v.push_back(make_pair(s+"/", CDIR));
        }
      closedir(d);
      }
    sort(v.begin(), v.end(), filecmp);

    int q = v.size();
    int percolumn = (vid.yres-38) / (vid.fsize+5) - 4;
    int columns = 1 + (q-1) / percolumn;
      
    for(int i=0; i<q; i++) {
      int x = 16 + (vid.xres * (i/percolumn)) / columns;
      int y = 42 + vid.fsize * 4 + (vid.fsize+5) * (i % percolumn);
        
      displayColorButton(x, y, v[i].first, 1000 + i, 0, 0, v[i].second, 0xFFFF00);
      }

    keyhandler = handleKeyFile;
    }
  
  EX void handleKeyFile(int sym, int uni) {
    string& s(*cfileptr);
    int i = isize(s) - (editext?0:4);
    
    if(sym == SDLK_ESCAPE) {
      popScreen();
      }
    else if(sym == SDLK_RETURN || sym == SDLK_KP_ENTER) {
      // we pop and re-push, in case if action opens something
      popScreen();
      if(!file_action()) pushScreen(drawFileDialog);
      }
    else if(sym == SDLK_F4) {
      editext = !editext;
      }
    else if(sym == SDLK_BACKSPACE && i) {
      s.erase(i-1, 1);
      }
    else if(uni >= 32 && uni < 127) {
      s.insert(i, s0 + char(uni));
      }
    else if(uni >= 1000 && uni <= 1000+isize(v)) {
      string where = "", what = s, whereparent = "../";
      for(int i=0; i<isize(s); i++)
        if(s[i] == '/') {
          if(i >= 2 && s.substr(i-2,3) == "../")
            whereparent = s.substr(0, i+1) + "../";
          else
            whereparent = where;
          where = s.substr(0, i+1), what = s.substr(i+1);
          }
      int i = uni - 1000;
      if(v[i].first == "../") {
        s = whereparent + what;
        }
      else if(v[i].second == CDIR)
        s = where + v[i].first + what;
      else
        s = where + v[i].first;
      }
    return;
    }

  EX void openFileDialog(string& filename, string fcap, string ext, bool_reaction_t action) {
    cfileptr = &filename;
    filecaption = fcap;
    cfileext = ext;
    file_action = action;
    pushScreen(dialog::drawFileDialog);
    }
  
  // infix/v/vpush

  EX string infix;
  
  EX bool hasInfix(const string &s) {
    if(infix == "") return true;
    string t = "";
    for(int i=0; i<isize(s); i++) {
      char c = s[i];
      char tt = 0;
      if(c >= 'a' && c <= 'z') tt += c - 32;
      else if(c >= 'A' && c <= 'Z') tt += c;
      else if(c == '@') tt += c;
      if(tt) t += tt;
      }
    return t.find(infix) != string::npos;
    }
  
  EX bool editInfix(int uni) {
    if(uni >= 'A' && uni <= 'Z') infix += uni;
    else if(uni >= 'a' && uni <= 'z') infix += uni-32;
    else if(infix != "" && uni == 8) infix = infix.substr(0, isize(infix)-1);
    else if(infix != "" && uni != 0) infix = "";
    else return false;
    return true;
    }
    
  EX vector<pair<string, color_t> > v;  

  EX void vpush(color_t color, const char *name) {
    string s = XLATN(name);
    if(!hasInfix(s)) return;
    dialog::v.push_back(make_pair(s, color));
    }
  
  int editpos = 0;
  EX string *edited_string;

  EX string view_edited_string() {
    string cs = *edited_string;
    if(editpos < 0) editpos = 0;
    if(editpos > isize(cs)) editpos = isize(cs);
    cs.insert(editpos, "°");
    return cs;
    }    
  
  EX void start_editing(string& s) {
    edited_string = &s;
    editpos = isize(s);
    }
  
  EX string editchecker(int sym, int uni) {
    if(uni >= 32 && uni < 127) return string("") + char(uni);
    return "";
    }

  EX bool handle_edit_string(int sym, int uni, function<string(int, int)> checker IS(editchecker)) {
    auto& es = *edited_string;
    string u2;
    if(DKEY == SDLK_LEFT) editpos--;
    else if(DKEY == SDLK_RIGHT) editpos++;
    else if(uni == 8) {
      if(editpos == 0) return true;
      es.replace(editpos-1, 1, "");
      editpos--;
      }
    else if((u2 = checker(sym, uni)) != "") {
      for(char c: u2) {
        es.insert(editpos, 1, c);
        editpos ++;
        }
      }
    else return false;
    return true;
    }
  
  EX void string_edit_dialog() {
    cmode = sm::NUMBER | dialogflags;
    if(numberdark < DONT_SHOW)
    gamescreen(numberdark);
    init(ne.title);
    addInfo(view_edited_string());
    addBreak(100);
    formula_keyboard(true);
    addBreak(100);
    dialog::addBack();
    addBreak(100);
    
    if(ne.help != "") {
      addHelp(ne.help);
      }

    if(extra_options) extra_options();
    
    display();
    
    keyhandler = [] (int sym, int uni) {
      handleNavigation(sym, uni);
      if(handle_edit_string(sym, uni)) ;
      else if(doexiton(sym, uni)) {
        popScreen();
        if(reaction_final) reaction_final();
        }
      };
    }  

  EX void edit_string(string& s, string title, string help) {
    start_editing(s);
    ne.title = title;
    ne.help = help;
    dialogflags = 0;
    if(cmode & sm::SIDE) dialogflags |= sm::MAYDARK | sm::SIDE;
    cmode |= sm::NUMBER;
    pushScreen(string_edit_dialog);
    reaction = reaction_t();
    extra_options = reaction_t();
    numberdark = 0;
    }

  EX void confirm_dialog(const string& text, const reaction_t& act) {
    gamescreen(1);
    dialog::addBreak(250);
    dialog::init(XLAT("WARNING"), 0xFF0000, 150, 100);
    dialog::addHelp(text);
    dialog::addItem(XLAT("YES"), 'y');
    auto yes = [act] () { popScreen(); act(); };
    dialog::add_action(yes);
    dialog::add_key_action(SDLK_RETURN, yes);
    dialog::addItem(XLAT("NO"), 'n');
    dialog::add_action([] () { popScreen(); });
    dialog::display();
    }

  EX void addBoolItem_action(const string& s, bool& b, char c) { 
    dialog::addBoolItem(s, b, c);
    dialog::add_action([&b] { b = !b; });
    }

  EX void addBoolItem_action_neg(const string& s, bool& b, char c) { 
    dialog::addBoolItem(s, !b, c);
    dialog::add_action([&b] { b = !b; });
    }

  EX bool cheat_forbidden() {
    if(tactic::on && !cheater) {
      addMessage(XLAT("Not available in the pure tactics mode!"));
      return true;
      }
    if(daily::on) {
      addMessage(XLAT("Not available in the daily challenge!"));
      return true;
      }
    return false;
    }

  #if HDR

  template<class T> void addBoolItem_choice(const string&  s, T& b, T val, char c) {
    addBoolItem(s, b == val, c);
    add_action([&b, val] { b = val; });
    }

  inline void cheat_if_confirmed(const reaction_t& act) {
    if(cheat_forbidden())
      return;
    if(needConfirmationEvenIfSaved()) pushScreen([act] () { confirm_dialog(XLAT("This will enable the cheat mode, making this game ineligible for scoring. Are you sure?"), act); });
    else act();
    }

  inline void do_if_confirmed(const reaction_t& act) {
    if(needConfirmationEvenIfSaved()) pushScreen([act] () { confirm_dialog(XLAT("This will end your current game and start a new one. Are you sure?"), act); });
    else act();
    }

  inline void push_confirm_dialog(const reaction_t& act, const string& s) {
    pushScreen([act, s] () { confirm_dialog(s, act); });
    }

  inline reaction_t add_confirmation(const reaction_t& act) {
    return [act] { do_if_confirmed(act); };
    }
  #endif

  }

}
