// This software is distributed under the terms of the MIT License.
// Copyright (c) 2018, 2019 molelord
// All rights reserved.

import processing.sound.*;

import javax.swing.JFrame;
import java.awt.MouseInfo;
import java.awt.PointerInfo;
import java.awt.Point;

import java.awt.Robot;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.*;

import java.time.*;
import java.time.format.TextStyle;

import java.util.Locale;

// 8x10 dots
static final short[][] OcrNumberTable = {
  {0x3C,0x76,0x63,0x43,0x43,0x43,0x43,0x63,0x66,0x3E}, // 0
  {0x08,0x3C,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3E}, // 1
  {0x3E,0x66,0x03,0x03,0x02,0x06,0x0C,0x18,0x38,0x7F}, // 2
  {0x7E,0x66,0x03,0x02,0x0E,0x1E,0x03,0x03,0x47,0x7E}, // 3
  {0x06,0x0E,0x1E,0x26,0x26,0x46,0x7F,0x7F,0x06,0x06}, // 4
  {0x3F,0x70,0x60,0x60,0x7E,0x07,0x03,0x03,0x47,0x7E}, // 5
  {0x1E,0x38,0x20,0x74,0x7F,0x63,0x63,0x63,0x73,0x3E}, // 6
  {0x7F,0x03,0x03,0x06,0x06,0x0C,0x0C,0x18,0x18,0x30}, // 7
  {0x3E,0x77,0x43,0x62,0x3E,0x3E,0x43,0x43,0x63,0x7E}, // 8
  {0x3C,0x76,0x63,0x43,0x63,0x7F,0x3F,0x02,0x06,0x3C}, // 9
  {0,0,0,0,0,0,0,0,0,0}, // Empty 
};

// 10x14 dots
static final short[][] OcrNumber2Table = {
  {0x070,0x1FC,0x3DE,0x38E,0x386,0x307,0x307,0x307,0x307,0x307,0x386,0x38E,0x3FE,0x1FC}, // 0
  {0x000,0x038,0x0F8,0x1F8,0x1F8,0x038,0x038,0x038,0x038,0x038,0x038,0x038,0x078,0x1FE}, // 1
  {0x0F8,0x1FE,0x19E,0x00E,0x00E,0x00E,0x00E,0x01C,0x03C,0x078,0x0E0,0x1E0,0x3FF,0x3FF}, // 2
  {0x0F8,0x3FE,0x39E,0x00E,0x00E,0x00E,0x07C,0x07C,0x00E,0x006,0x007,0x20E,0x3BE,0x3FC}, // 3
  {0x000,0x03C,0x03C,0x07C,0x0CC,0x08C,0x18C,0x30C,0x39E,0x3FF,0x3FF,0x00C,0x00C,0x00C}, // 4
  {0x000,0x1FE,0x1FE,0x180,0x180,0x1C0,0x1FE,0x1FE,0x00F,0x007,0x007,0x20E,0x3BE,0x3FC}, // 5
  {0x03C,0x0FE,0x1E0,0x3C0,0x380,0x380,0x3FE,0x38E,0x387,0x387,0x387,0x387,0x3CE,0x1FC}, // 6
  {0x000,0x3FF,0x3FF,0x00E,0x00E,0x01C,0x01C,0x038,0x038,0x070,0x0F0,0x0E0,0x0E0,0x1C0}, // 7
  {0x078,0x1FE,0x38F,0x387,0x387,0x3C6,0x1FC,0x0FE,0x39F,0x307,0x307,0x387,0x3CF,0x1FE}, // 8
  {0x0F0,0x1FC,0x39E,0x30E,0x306,0x307,0x387,0x3DF,0x1FE,0x00E,0x00E,0x01C,0x37C,0x3F8}, // 9
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0}, // Empty 
};

// 8x10 dots
static final short[][] OcrNumber3Table = {
  {0x7C,0xC6,0xC7,0xC7,0xC7,0xC7,0xC7,0xC6,0x7E,0x38}, // 0
  {0x18,0x78,0x78,0x18,0x18,0x18,0x18,0x18,0x3C,0x7E}, // 1
  {0xFC,0x8E,0x06,0x06,0x0C,0x1C,0x30,0x70,0xFF,0xFF}, // 2
  {0xFE,0x8E,0x06,0x0C,0x3C,0x06,0x06,0x06,0xFE,0x78}, // 3
  {0x0C,0x1C,0x3C,0x64,0xC4,0x8E,0xFF,0x0E,0x04,0x04}, // 4
  {0xFE,0xE0,0xE0,0xE0,0xFE,0x07,0x07,0x87,0xFE,0x78}, // 5
  {0x3E,0x60,0xC0,0xDC,0xEE,0xC7,0xC7,0xE7,0x7E,0x3C}, // 6
  {0xFF,0x07,0x06,0x0E,0x0C,0x1C,0x18,0x30,0x70,0x60}, // 7
  {0x7E,0xC6,0xC6,0xE4,0x7C,0xCE,0xC7,0xC7,0xFE,0x38}, // 8
  {0x7C,0xC6,0xC7,0xC7,0xE7,0x7F,0x06,0x06,0x7C,0x70}, // 9
  {0,0,0,0,0,0,0,0,0,0}, // Empty 
};

// 8x8 dots
static final short[][] OcrVTable = {
  /*
  {0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x3E,0x00}, // L
  {0x00,0x90,0xB0,0xB0,0xB0,0xB0,0xB0,0xB0,0xBE,0x80}, // L(selected)
  */
  {0x00,0x22,0x22,0x36,0x14,0x1C,0x1C,0x00}, // v
  {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // Empty 
};

static int getR(int c) {
  return (c>>16) & 0xFF;
}
static int getG(int c) {
  return (c>>8) & 0xFF;
}
static int getB(int c) {
  return c & 0xFF;
}

static class Glbl {
  static final int ChecklistMode = 0;
  static final int TimerMode     = 1;
  static final int W      = 240;
  static final int H      = 320;
  static final int TimerW = 130;
  static final int TimerH = 100;
  static final int ScouterW = 600;
  static final int OcrNumberW  =  8;
  static final int OcrNumberH  = 10;
  static final int OcrNumber2W = 10;
  static final int OcrNumber2H = 14;
  static final int OcrNumber3W =  8;
  static final int OcrNumber3H = 10;
  static final int OcrVW       =  8;
  static final int OcrVH       =  8;
  static final int OcrEnhLv    =  0;
  static final int OcrEnhNext  =  1;
  static final int OcrInfoLv   =  2;
  static final int OcrInfoNext =  3;

  static int     mode     = ChecklistMode;
  static int     prevMode = TimerMode;
  static boolean isLastOneMinute = false;
  static boolean scouterEnabled  = false;
  static boolean bgSelected      = false;

  static PSurface  ps;
  static JFrame    jf;
  static PFont     font12;
  static PFont     font24;
  static SoundFile chime;
  static boolean   DebugOcr = false;
  static boolean   DebugHp  = false;

  static void setInstances(PSurface _ps, JFrame _jf,
    PFont _f12, PFont _f24, SoundFile _chime) {
    ps     = _ps;
    jf     = _jf;
    font12 = _f12;
    font24 = _f24;
    chime  = _chime;
  }
  static boolean isTimerMode() {
    return mode == TimerMode ? true : false;
  }
  static void changeSize() {
    if (isTimerMode()) {
      jf.setOpacity(0.5f);
      ps.setSize(TimerW, TimerH);
    } else {
      if (scouterEnabled) {
        jf.setOpacity(1.0f);
        ps.setSize(W + ScouterW, H);
      } else {
        jf.setOpacity(0.75f);
        ps.setSize(W, H);
      }
    }
  }
  static void setBgSelected(boolean value) {
    bgSelected = value;
  }
  static boolean isBgSelected() {
    return bgSelected;
  }
  static int countHotbits(int value, int width) {
    int result = 0;
    while (value > 0) {
      if ((value & 1) == 1) {
        result++;
      }
      value >>>= 1;
    }
    return result;
  }
  
  static int ocrImage(PImage img,
    final short[][] table, final int tableW, final int tableH,
    final float BinarizationThreshold, final int errorThreshold) {
    img.filter(THRESHOLD, BinarizationThreshold);

    short[] line = new short[tableH];
    for (int py = 0; py < tableH; py++) {
      line[py] = 0;
      for (int px = 0; px < tableW; px++) {
        int c = img.pixels[tableW*py + px];
        c = getG(c);
        line[py] <<= 1;
        line[py] |= (c > 0) ? 1 : 0;
      }
      if (Glbl.DebugOcr) {
        println("y" + nf(py,2) + ": " +
          hex(line[py], 3) + " : " + binary(line[py], tableW));
      }
    }

    if (Glbl.DebugOcr) {
      print("OCR diff ");
    }

    int   minDiff    = tableW*tableH;
    int   minDiffNum = 0;
    int[] totalDiff  = new int[table.length];
    for (int num = 0; num < table.length; num++) {

      // Perform a normal comparison and a comparison shifted by 1 dot,
      // and the better one is adopted.
      int[] diff = new int[2];
      for (int sft = 0; sft < diff.length; sft++) {
        for (int i = 0; i < tableH; i++) {
          int tmp = (table[num][i]<<sft) ^ line[i];
          if (tmp != 0) {
            int bits = countHotbits(tmp, tableW);
            if (bits >= 3) {
              // If the difference is large, the evaluation is
              // greatly reduced.
              bits *= 2;
            }
            diff[sft] += bits;
          }
        }
      }
      totalDiff[num] = (diff[0] < diff[1]) ? diff[0] : diff[1];

      if (totalDiff[num] < minDiff) {
        minDiff    = totalDiff[num];
        minDiffNum = num;
      }
      if (Glbl.DebugOcr) {
        print(nf(num) + ":" + nf(totalDiff[num]) + " ");
      }
    }
    if (Glbl.DebugOcr) println("");

    // Allow even a slight difference.
    if (minDiff < errorThreshold) {
      if (Glbl.DebugOcr) {
        println("OCR success " + nf(minDiffNum) +
          " diff:" + nf(totalDiff[minDiffNum]));
      }
      return minDiffNum;
    }
    if (Glbl.DebugOcr) {
      println("OCR fail:");
    }
    return -1;
  }
  // -1:failed 0to9:number 10:empty
  static int ocrNumber(PImage img, int kind) {
    int result = -1;
    if (kind == OcrEnhLv || kind == OcrEnhNext) {
      result = ocrImage(img, OcrNumberTable, OcrNumberW, OcrNumberH,
        0.8f, 25);
    } else if (kind == OcrInfoLv) {
      result = ocrImage(img, OcrNumber2Table, OcrNumber2W, OcrNumber2H,
        0.8f, 15);
    } else if (kind == OcrInfoNext) {
      result = ocrImage(img, OcrNumber3Table, OcrNumber3W, OcrNumber3H,
        0.8f, 25);
    }
    return result;
  }
  // false:failed true:v
  static boolean ocrV(PImage img) {
    int rc = ocrImage(img, OcrVTable, OcrVW, OcrVH,
      0.8f, 20);
    return (rc == 0);
  }
  static int calculateExpToMax(int maxLevel, int level, int nextExp) {
    final short[] AccessoryExpTable = {
    //       0   1   2   3   4   5   6   7   8   9
    /* 0*/   0, 10, 15, 20, 25, 30, 35, 40, 45, 50,
    /*10*/  55, 60, 65, 70, 75, 80, 85, 90, 95,100,
    /*20*/ 120,140,160,180,200,220,240,260,280,300,
    /*30*/ 320,340,360,380,400,420,440,460,480,500,
    /*40*/ 520,540,560,580,600,620,640,660,680,700,
    };

    int result = 0;
    if (level == maxLevel) {
      // do nothing
    } else {
      for (int i = level+1; i <= maxLevel-1; i++) {
        result += AccessoryExpTable[i];
      }
      result += nextExp;
    }
    return result;
  }
}

class GuiItem {
  int x;
  int y;
  int itemWidth;
  int itemHeight;
  GuiItem(int _x, int _y, int _itemWidth, int _itemHeight) {
    x = _x;
    y = _y;
    itemWidth  = _itemWidth;
    itemHeight = _itemHeight;
  }
  GuiItem() {
    this(0, 0, 0, 0);
  }
  void render() {
  }
  void press() {
  }
  boolean isOver() {
    int windowX = Glbl.jf.getX();
    int windowY = Glbl.jf.getY(); 
    int maxX = windowX + Glbl.W;
    int maxY = windowY + Glbl.H;
    boolean rc = false;
    PointerInfo info = MouseInfo.getPointerInfo();
    // #26 When returning from sleep, PointerInfo may become null.
    if (info != null) {
      Point mouse = info.getLocation();
      if (windowX<=mouse.x && mouse.x<maxX && windowY<=mouse.y && mouse.y<maxY) {
        int mx = mouseX;
        int my = mouseY;
        rc = x<=mx && mx<x+itemWidth && y<=my && my<y+itemHeight;
      }
    }
    if (rc) {
      Glbl.setBgSelected(false);
    }
    return rc;
  }
}

class TimerDisplay extends GuiItem {
  int endTime;
  static final int W = 70;
  static final int H = 30;
  TimerDisplay(int _x, int _y) {
    super(_x, _y, W, H);
    endTime = 0;
  }
  void start(int seconds) {
    endTime = millis() + 1000*seconds;
    Glbl.mode = Glbl.TimerMode;
  }
  void render() {
    int currentTime = millis();
    int remainTime = endTime - currentTime;
    if (remainTime <= 0) {
      Glbl.mode = Glbl.ChecklistMode;
      Glbl.chime.play();
    } else {
      strokeWeight(2); // Fixed a bug of #5
      stroke(128);     // Fixed a bug of #5
      fill(isOver() ? 64 : 0);
      rect(x, y, W-1, H-1);
      String msg = "000.0";
      fill(0, 255, 0);
      int sec     = remainTime / 1000;
      int decimal = (remainTime / 100) % 10;
      msg = nf(sec, 3) + "." + str(decimal);
      textFont(Glbl.font24);
      textSize(24);
      text(msg, x+5, y+22);
    }
  }
  void press(){
    if (isOver()) {
      Glbl.mode = Glbl.ChecklistMode;
    }
  }
}

class TimerBar extends GuiItem {
  int seconds;
  TimerDisplay td;
  static final int W =  64;
  static final int H = 310;
  TimerBar(int _seconds, int _x, int _y, TimerDisplay _td){
    super(_x, _y, W, H);
    seconds = _seconds;
    td = _td;
  }
  void render(){
    stroke(224);
    strokeWeight(1);
    fill(255); // Left side
    triangle(x,y+5, x,y+H, x+W-1,y+H-1);
    fill(240); // Right side
    triangle(x,y, x+W,y,  x+W-1,y+H-1);

    if (isOver()) {
      fill(0, 255, 0);
      triangle(x,y, x,mouseY, x+mouseY/5,mouseY);
    }
  }
  int computeSeconds() {
    int pos = mouseY/2;
    if (pos < 50) {
      // do nothing
    } else {
      pos = 50 + (pos - 50)/4*5;
    }
    return pos;
  }
  void press() {
    if (isOver()) {
      td.start(computeSeconds());
    }
  }
}

class TimerBarLabel extends GuiItem {
  TimerBar tb;
  TimerBarLabel(TimerBar _tb) {
    super();
    tb = _tb;
  }
  void render() {
    if (tb.isOver()) {
      String msg = str(tb.computeSeconds()) + "sec";
      int y = mouseY;
      if (y < 20) {
        y = 20;
      }
      textFont(Glbl.font24);
      textSize(24);
      fill(64);
      text(msg, tb.x+15+mouseY/5+2, y+2); // shadow
      fill(0, 255, 0);
      text(msg, tb.x+15+mouseY/5, y);
    }
  }
}

// Thanks to https://forum.processing.org/two/discussion/4849/checkbox
class Checkbox extends GuiItem {
  boolean checked;
  static final int W = 20;
  static final int H = 20;
  Checkbox(int _x, int _y, boolean _checked) {
    super(_x, _y, W, H);
    checked = _checked;
  }
  void render() {
    stroke(0); // color of box's flame
    strokeWeight(1);
    fill(isOver()?224:255); // color of box
    rect(x, y, W-1, H-1);
    if (checked) {
      stroke(255, 0, 0); // color of v
      strokeWeight(2);
      line(x+2, y+10, x+10, y+15);
      line(x+10, y+15, x+17, y+3);
    }
  }
  void press() {
    if (isOver()) {
      checked=!checked;
    }
  }
  boolean get() {
    return checked;
  }
  void set() {
    checked = true;
  }
  void reset() {
    checked = false;
  }
}

int next_y = 5;
class Mission extends GuiItem {
  String name;
  Checkbox[] boxes;
  int y;
  final int delta_y = 29;
  Mission(String _name, int _items, int _value) {
    super();
    name  = _name;
    boxes = new Checkbox[_items];
    y = next_y;
    next_y += delta_y;
    for (int i=0; i< boxes.length; i++) {
      boxes[i] = new Checkbox(115 + 25*i, y, (i < _value));
    }
  }
  void render() {
    boolean isCurrent = false;
    for (Checkbox box : boxes) {
      box.render();
      isCurrent = isCurrent ? true : box.isOver();
    }
    textFont(Glbl.font12);
    textSize(12);
    if (isCurrent) {
      fill(128);
      text(name, 5, y+15);
      fill(0);
      text(name, 5-1, y+15-1);
    } else {
      fill(0);
      text(name, 5, y+15);
    }
  }
  void press(){
    for (int i=0; i< boxes.length; i++) {
      boxes[i].press();

      // Chain reaction
      if (boxes[i].get() == true) {
        for (int j = i-1; j >= 0; j--) {
          boxes[j].set();
        }
      } else {
        for (int j = i+1; j < boxes.length; j++) {
          boxes[j].reset();
        }
      }
    }
  }
  int getValue() {
    int i;
    for (i=boxes.length-1; i>=0; i--) {
      if (boxes[i].get() == true) {
        break;
      }
    }
    return i+1;
  }
}

class CloseButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  CloseButton(int _x, int _y){
    super(_x, _y, W, H);
  }
  void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of x
    strokeWeight(2);
    line(x+3, y+3, x+16, y+16);
    line(x+3, y+16, x+16, y+3);
  }
  void press(){
    if (isOver()) {
      exit();
    }
  }
}

class MinimizeButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  MinimizeButton(int _x, int _y){
    super(_x, _y, W, H);
  }
  void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of _
    strokeWeight(2);
    line(x+3, y+12, x+16, y+12);
  }
  void press() {
    if (isOver()) {
      Glbl.jf.setExtendedState(
        Glbl.jf.getExtendedState() | JFrame.ICONIFIED);
    }
  }
}

final float[] dmmGemTimeTable = {
  12.0, 20.0, -1,   // Mon
  12.5, 21.0, -1,   // Tue
  -1,   18.0, 21.0, // Wed
  -1,   19.0, 22.0, // Tur
  -1,   20.0, 23.0, // Fri
  12.0, 18.0, 22.0, // Sat
  12.5, 19.0, 23.0, // Sun
};
final float[] nutakuGemTimeTable = {
  12.0, 19.0, -1,   // Mon
  12.5, 19.5, -1,   // Tue
  -1,   18.0, 22.5, // Wed
  -1,   19.0, 23.0, // Tur
  -1,   19.5, 23.5, // Fri
  12.0, 18.0, 22.0, // Sat
  12.5, 19.0, 23.0, // Sun
};
class WallClock extends GuiItem {
  ZoneId zid;
  int[] gemTimeTable;
  static final int W = 82;
  static final int H = 20;
  WallClock(ZoneId _zid) {
    super(Glbl.W - W - 1, Glbl.H - H - 1, W, H);
    zid = _zid;
    gemTimeTable = new int[dmmGemTimeTable.length];
    float[] tbl = dmmGemTimeTable; // "Asia/Tokyo"
    if (zid.equals(ZoneId.of("America/Los_Angeles"))) {
      tbl = nutakuGemTimeTable;
    }
    for (int i = 0; i < dmmGemTimeTable.length; i++) {
      gemTimeTable[i] = int(tbl[i]*100);
    }
  }
  void render() {
    Instant currentTime = Instant.now();
    ZonedDateTime zoneTime = currentTime.atZone(zid);

    int w      = zoneTime.getDayOfWeek().getValue();
    int hour   = zoneTime.getHour();
    int minute = zoneTime.getMinute();
    boolean isGemTime = false;
    int nowHour = hour*100 + minute*100/60;
    Glbl.isLastOneMinute = false;
    for (int i = 0; i < 3; i++) {
      int gemHour = gemTimeTable[(w-1)*3 + i];
      if (gemHour <= nowHour && nowHour < gemHour + 50) {
        isGemTime = true;
        if (minute == 29 || minute == 59) {
          Glbl.isLastOneMinute = true;
        }
        break;
      }
    }

    if (isOver()) {
      zoneTime = currentTime.atZone(ZoneId.systemDefault());
      hour   = zoneTime.getHour();
      minute = zoneTime.getMinute();
    }

    strokeWeight(2);
    stroke(128);
    if (isGemTime) {
      fill(255,255,0);
    } else {
      fill(0);
    }
    rect(x, y, W-1, H-1);

    if (isGemTime) {
      fill(0);
    } else {
      fill(0,255,0);
    }
    textFont(Glbl.font24);
    textSize(12);
    String dow = zoneTime.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US);
    int    day = zoneTime.getDayOfMonth();
    String msg = dow;
    if (!Glbl.isLastOneMinute) {
      msg += " " + ((day<10) ? " " : "") + str(day);
      msg += " " + nf(hour, 2) + ":" + nf(minute, 2);
      text(msg, x+4, y+14);
    } else {
      int second = zoneTime.getSecond();
      msg += " " + nf(hour, 2) + ":" + nf(minute, 2) + ":" + nf(second, 2);
      text(msg, x+4, y+14);
    }

    if (isOver()) {
      putGemQuestTable(currentTime);
    } else {
      // put Timezone
      textFont(Glbl.font12);
      textSize(12);
      fill(0);
      textAlign(RIGHT);
      text(zid.toString(), x-2, Glbl.H-8);
      textAlign(LEFT);
    }
  }
  void press(){
  }
  void putGemQuestTable(Instant currentTime) {
    ZonedDateTime zoneTime = currentTime.atZone(zid);

    // Round the current time to the 15 minute boundary.
    // Because 15 minutes are the smallest unit of time difference.
    final int boundary = 15;
    int m = zoneTime.getMinute();
    if (m % boundary > 0) {
      zoneTime = zoneTime.plusMinutes(boundary - (m % boundary));
    }
    
    // draw background
    stroke(128);
    strokeWeight(1);
    fill(0);
    rect(6, 86, Glbl.W-9, 201);
    fill(255);
    rect(5, 85, Glbl.W-10, 200);
    int msgY = 100;
    final int msgDeltaY = 20;

    // build localGemTimeTable
    final int minutesPerWeek = 7*24*60;
    boolean first = true;
    // This table is 4x7 because in some areas quests can occur 4 times a day.
    int[] localGemTimeTable = new int[4*7];
    for (int i = 0; i < 4*7; i++) {
      localGemTimeTable[i] = Integer.MAX_VALUE;
    }
    for (int i = 0; i < minutesPerWeek/boundary; i++) { 
      int w      = zoneTime.getDayOfWeek().getValue();
      int hour   = zoneTime.getHour();
      int minute = zoneTime.getMinute();

      int nowHour = hour*100 + minute*100/60;
      for (int j = 0; j < 3; j++) {
        int gemHour = gemTimeTable[(w-1)*3 + j];
        if (gemHour == nowHour) {
          Instant t = Instant.from(zoneTime);
          ZonedDateTime local = t.atZone(ZoneId.systemDefault());
          int lw = local.getDayOfWeek().getValue();
          int lh = local.getHour();
          int lm = local.getMinute();
          int found = lh*100+lm*100/60;
          if (first) {
            found += 1; // Marking
            first = false;
          }
          int index = (lw-1)*4;
          int[] part = {found,
            localGemTimeTable[index+0],
            localGemTimeTable[index+1],
            localGemTimeTable[index+2],
            localGemTimeTable[index+3]};
          java.util.Arrays.sort(part);
          System.arraycopy(part, 0, localGemTimeTable, index, 4);
        }
      }
      zoneTime = zoneTime.plusMinutes(boundary);
    }

    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text("Gem Quest on your Timezone", 10, msgY);
    msgY += msgDeltaY*1.5;
    for (int w = 1; w <= 7; w++) {
      String msg = DayOfWeek.of(w).getDisplayName(TextStyle.SHORT, Locale.US);
      msg += " ";
      for (int i = 0; i < 4; i++) {
        int index = (w-1)*4 + i;
        int value = localGemTimeTable[index];
        if (value != Integer.MAX_VALUE) {
          int lh = value/100;
          int lm = (value%100)*60/100;
          if (value%10 == 1) {
            msg += ">" + nf(lh, 2) + ":" + nf(lm, 2) + "<";
          } else {
            msg += " " + nf(lh, 2) + ":" + nf(lm, 2) + " ";
          }
        }
      }
      textFont(Glbl.font24);
      textSize(12);
      fill(0);
      text(msg, 15, msgY);
      msgY += msgDeltaY;
    }
    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text(ZoneId.systemDefault().toString(), x-55, y-20);
    stroke(0);
    strokeWeight(2);
    line(x-10, y-15, x, y-3);
  }
}

class Scouter {
  Robot          bot;
  Rectangle      area;
  PImage         fullImg;
  PImage         windowImg;
  WritableRaster wr;
  String         name;
  String         ver;
  Scouter(String _name, String _ver) {
    name = _name;
    ver  = _ver;
  }
  boolean initialize() {
    boolean result = false;
    try {
      bot    = new Robot();
      area   = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
      result = true;
    } catch (java.awt.AWTException e) {
      // do nothing
    }
    return result;
  }
  boolean findKamihimeWindow() {
    // Thanks to https://junkato.jp/ja/blog/2013/01/28/processing-efficient-copy-from-bufferedimage-to-pimage/
    BufferedImage bimg = bot.createScreenCapture(area);
    if (fullImg == null) {
      fullImg = new PImage(bimg);
      DataBufferInt dbi = new DataBufferInt(
        fullImg.pixels, fullImg.pixels.length);
      wr = Raster.createWritableRaster(
        bimg.getSampleModel(), dbi, new Point(0, 0));
      fullImg.loadPixels();
    } else {
      bimg.copyData(wr);
      fullImg.updatePixels();
    }

    // find Top-Left of Game Window
    // Black, White, Black, White, Black, White, White, Black
    final int verticalPixels = 8;
    final int maxX = fullImg.width - 1;
    final int maxY = fullImg.height - verticalPixels;
    int px = 0;
    int py = 0;
    for (px = 0; px <= maxX; px++) {
      for (py = 0; py <= maxY; py++) {
        int[] g = new int[verticalPixels];
        for (int i = 0; i < verticalPixels; i++) {
          // G component is used for brightness judgment.
          int c = fullImg.pixels[fullImg.width * (py+i) + px];
          g[i] = getG(c);
        }
        if (g[0] <  40 &&
            g[1] > 200 &&
            g[2] <  40 &&
            g[3] > 200 &&
            g[4] <  40 &&
            g[5] > 200 &&
            g[6] > 200 &&
            g[7] <  40) {
          windowImg = fullImg.get(px-142, py+8, 960, 640);
          return true;
        }
      }
    }
    return false;
  }
  boolean searchHpBar() {
    //         Enemie's L      1  2-A  2-B  3-A  3-B  3-C
    //final int[] lvOffsetX = {208, 159, 459, 284, 108, 459}; // L
    final int[] lvOffsetX = {215, 165, 466, 291, 116, 466}; // v
    final int[] lvOffsetY = { 16,  31,  31,  16,  66,  66};
    final int[] hpOffsetX = {205, 155, 455, 280, 105, 455};
    final int[] hpOffsetY = { 32,  47,  47,  32,  82,  82};
    final int[] hpPixels  = {358, 177, 177, 177, 177, 177};
    int count = 0;
    boolean[] found = new boolean[lvOffsetX.length];
    int[]     pos   = new int[lvOffsetX.length];

    for (int i = 0; i < lvOffsetX.length; i++) {
      PImage lvImg = windowImg.get(
        lvOffsetX[i], lvOffsetY[i], Glbl.OcrVW, Glbl.OcrVH);
        //lvOffsetX[i], lvOffsetY[i], 25, 25);
      boolean rc = Glbl.ocrV(lvImg);
      if (rc) {
        found[i] = true;
        count++;
      }
      if (Glbl.DebugOcr) {
        image(lvImg, Glbl.W + 400 + Glbl.OcrVW*2*i, 120);
      }
    }

    int[] hp = new int[lvOffsetX.length];
    for (int i = 0; i < lvOffsetX.length; i++) {
      if (!found[i]) continue;
      for (int x = hpPixels[i]-1; x >= 0; x--) {
        int c = windowImg.pixels[windowImg.width*hpOffsetY[i] +
          hpOffsetX[i] + x];
        int r = getR(c);
        int g = getG(c);
        int b = getB(c);
        // find Hp  
        if (r > 240 && g < 40 && b < 90) {
          double tmp = (double)(x*100) / (hpPixels[i]-1);
          hp[i]  = (int)Math.ceil(tmp);
          pos[i] = x;
          if (Glbl.DebugHp) {
            println("hp:" + hp[i] + " x:" + x);
          }
          break;
        }
      }
    }

    final int deltaX = 48;
    final int deltaY = 10;

    if (count > 0) {
      PImage hpImg = windowImg.get(deltaX, deltaY, Glbl.ScouterW, 116);
      hpImg.filter(GRAY);
      image(hpImg, Glbl.W, 0);
    }

    for (int i = 0; i < lvOffsetX.length; i++) {
      if (!found[i]) continue;
      if (hp[i] > 0) {

        strokeWeight(1);

        final int orgX  = Glbl.W + hpOffsetX[i] - deltaX;
        final int orgY  = hpOffsetY[i] - deltaY;
        final int hpPix = hpPixels[i];

        if (Glbl.DebugHp) {
          stroke(255, 0, 0); // color of lines
          line(orgX, orgY, orgX + hpPix-1, orgY);
        }

        stroke(0, 255, 0); // color of lines

        // current position
        line(orgX + pos[i], orgY, orgX + pos[i], orgY+12);

        // draw |------|
        line(orgX,           orgY+4, orgX,           orgY+9);
        line(orgX,           orgY+6, orgX+hpPix-1,   orgY+6);
        line(orgX+hpPix-1,   orgY+4, orgX+hpPix-1,   orgY+9);

        // draw 50% marker
        line(orgX+hpPix/2,   orgY+4, orgX+hpPix/2,   orgY+9);

        // draw 30% marker
        line(orgX+hpPix*0.3, orgY+4, orgX+hpPix*0.3, orgY+9);

        textFont(Glbl.font12);
        textSize(12);
        fill(0);
        text(nf(hp[i]) + "%", orgX + hpPix - 25+1, orgY+3);
        fill(0, 255, 0);
        text(nf(hp[i]) + "%", orgX + hpPix - 25+0, orgY+2);
      }
    }

    return count > 0 ? true : false;
  }
  boolean searchAccessory() {
    final int[] acOffsetX = {687, 264};
    final int[] acOffsetY = {103,  15};
    final int[] acW       = {220, 350};
    final int[] acH       = {349, 300};
    final int[] rarelityX = {161,  50};
    final int[] rarelityY = {156,  32};
    final int[] levelX    = {196,  49};
    final int[] levelY    = {  3, 280};
    final int[] between   = {  2,   3};
    final int[] nextX     = {111, 301};
    final int[] nextY     = {301, 284};
    final String[] rarelityStr   = {"R", "SR", "SSR"};
    final int[]    rarelityMaxLv = {30, 40, 50};

    for (int view = 0; view < acOffsetX.length; view++) {
      PImage acImg = windowImg.get(
        acOffsetX[view], acOffsetY[view], acW[view], acH[view]);

      // Lv
      int kind = (view == 0) ? Glbl.OcrEnhLv : Glbl.OcrInfoLv;
      int w = (view == 0) ? Glbl.OcrNumberW : Glbl.OcrNumber2W;
      int h = (view == 0) ? Glbl.OcrNumberH : Glbl.OcrNumber2H;
      int level = 0;
      for (int i = 0; i < 2; i++) {
        PImage numImg = acImg.get(
          levelX[view]+i*(w+between[view]), levelY[view],
          w, h);
        if (Glbl.DebugOcr) {
          image(numImg, Glbl.W + 400 + i*(w+2+between[view]), 135 + 15*view);
        }
        int num = Glbl.ocrNumber(numImg, kind);
        if (num < 0) {
          break;
        } else if (num < 10) {
          level = level*10 + num;
        }
      }
      if (view == 0 && level == 0) {
        PImage numImg = acImg.get(
          201, 3, w, h);
        if (Glbl.DebugOcr) {
          image(numImg, Glbl.W + 400 + 2*(w+2+between[view]), 135);
        }
        int num = Glbl.ocrNumber(numImg, kind);
        if (num < 0) {
          // do nothing
        } else if (num < 10) {
          level = num;
        }
      }

      // NEXT
      kind = (view == 0) ? Glbl.OcrEnhNext : Glbl.OcrInfoNext;
      int nextExp = 0;
      for (int i = 0; i < 4; i++) {
        PImage numImg = acImg.get(
          nextX[view]+i*(Glbl.OcrNumberW+2), nextY[view],
          Glbl.OcrNumberW, Glbl.OcrNumberH);
        if (Glbl.DebugOcr) {
          image(numImg, Glbl.W + 400 + i*(Glbl.OcrNumberW+4), 170);
        }
        int num = Glbl.ocrNumber(numImg, kind);
        if (num < 0) {
          break;
        } else if (num < 10) {
          nextExp = nextExp*10 + num;
        }
      }

      // Rarelity
      int rarelity = 0;
      int c = acImg.pixels[acImg.width*rarelityY[view] + rarelityX[view]];
      int r, g, b;
      r = getR(c);
      g = getG(c);
      b = getB(c);
      rarelity = 0;
      if (
        220 <= r && r <= 250 &&
        220 <= g && g <= 250 &&
        220 <= b && b <= 250) {
        rarelity = 1;
      } else if (
        220 <= r && r <= 240 &&
        190 <= g && g <= 210 &&
        105 <= b && b <= 125) {
        rarelity = 2;
      }

      if (level == 50 || (level > 0 && nextExp > 0)) {
      //if (view == 1 || (level > 0 && nextExp > 0)) {
        acImg.filter(GRAY);
        image(acImg, Glbl.W, 0);

        int expToMax = Glbl.calculateExpToMax(
          rarelityMaxLv[rarelity], level, nextExp);

        textFont(Glbl.font24);
        textSize(24);
        fill(0);
        text(rarelityStr[rarelity], Glbl.W + 230, 30);
        text("Lv   " + nf(level),   Glbl.W + 230, 50);
        text("NEXT " + nf(nextExp), Glbl.W + 230, 70);
        text("to Lv" + nf(rarelityMaxLv[rarelity]) +
          ": " + nf(expToMax) + "exp",
          Glbl.W + 230, 110);

        if (Glbl.DebugOcr) {
          println("Rarelity icon RGB: " + nf(r) + " " + nf(g) + " " + nf(b));
          strokeWeight(1);
          stroke(0);
          line(Glbl.W + rarelityX[view], rarelityY[view],
            Glbl.W + rarelityX[view] + 10, rarelityY[view] + 10);
        }
        return true;
      }
    }

    return false;
  }
  void render() {
    boolean found = findKamihimeWindow();
    if (found) {
      found = searchHpBar();
      if (!found) {
        searchAccessory();
      }
    }
    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text(name + " version " + ver, Glbl.W+Glbl.ScouterW - 170, Glbl.H-8);
  }
}

class DrawerButton extends GuiItem {
  Scouter scouter;
  static final int W = 20;
  static final int H = 40;
  DrawerButton(int _x, int _y, String name, String ver){
    super(_x, _y, W, H);
    scouter = new Scouter(name, ver);
  }
  void render() {
    noStroke();
    fill(isOver()?64:0); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of >
    strokeWeight(2);
    if (Glbl.scouterEnabled) {
      line(x+16, y+ 3, x+ 3, y+19);
      line(x+ 3, y+20, x+16, y+36);
      scouter.render();
    } else {
      line(x+ 3, y+ 3, x+16, y+19);
      line(x+16, y+20, x+3, y+36);
    }
  }
  void press() {
    if (isOver()) {
      if (!Glbl.scouterEnabled) {
        if (scouter.initialize()) {
          Glbl.scouterEnabled = true;
          Glbl.changeSize();
        }
      } else {
        Glbl.scouterEnabled = false;
        Glbl.changeSize();
      }
    }
  }
}
