import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import processing.awt.PSurfaceAWT; 
import processing.sound.*; 
import javax.swing.JFrame; 
import java.awt.MouseInfo; 
import java.awt.PointerInfo; 
import java.awt.Point; 
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; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class AbilityTimer extends PApplet {

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









final String AppName       = "AbilityTimer";
final String AppVersion    = "0.61";
final String SaveFileName  = "save.csv";
final String ChimeFileName = "chime.mp3";

ArrayList<GuiItem> timerModeItems     = new ArrayList<GuiItem>();
ArrayList<GuiItem> checklistModeItems = new ArrayList<GuiItem>();
ArrayList<Mission> missions           = new ArrayList<Mission>();

public void settings() {
  size(Glbl.W, Glbl.H);
}

public void setup(){
  surface.setAlwaysOnTop(true);
  
  PSurfaceAWT.SmoothCanvas smoothCanvas;
  smoothCanvas = (PSurfaceAWT.SmoothCanvas)surface.getNative();
  JFrame jframe;
  jframe = (JFrame)smoothCanvas.getFrame();
  jframe.dispose();
  jframe.setUndecorated(true);
  jframe.setVisible(true);

  surface.setResizable(true);
  
  smooth();

  TimerDisplay tmdisp = new TimerDisplay(20, 5);
  timerModeItems.add(tmdisp);

  CloseButton cbutton_t = new CloseButton(
    Glbl.TimerW-CloseButton.W, 0);
  timerModeItems.add(cbutton_t);

  MinimizeButton mbutton_t = new MinimizeButton(
    Glbl.TimerW-CloseButton.W-MinimizeButton.W, 0);
  timerModeItems.add(mbutton_t);

  TimerBar tmbar = new TimerBar(180, 5, 5, tmdisp);
  checklistModeItems.add(tmbar);

  PFont font12 = loadFont("mplus-2p-bold-12.vlw");
  PFont font24 = loadFont("mplus-2m-bold-24.vlw");
  Glbl.setInstances(surface, jframe, font12, font24,
    new SoundFile(this, ChimeFileName)
  );

  Table chkTbl  = loadTable(SaveFileName, "header");
  Table itemTbl = loadTable("Items.csv", "header");
  ZoneId zoneid = ZoneId.of("America/Los_Angeles");
  int i = 0;
  for (TableRow itemRow : itemTbl.rows()) {
    String kind = itemRow.getString("kind");
    if (kind.equals("mission")) {
      String name  = itemRow.getString("name");
      int    items = itemRow.getInt("value");
      int    value = 0;
      if (chkTbl != null && (i < chkTbl.getRowCount())) {
        TableRow chkRow = chkTbl.getRow(i);
        if (chkRow != null) { 
          value = chkRow.getInt("value");
          i++;
        }
      }
      Mission m = new Mission(name, items, value);
      checklistModeItems.add(m);
      missions.add(m);
    } else if (kind.equals("service")) {
      String name  = itemRow.getString("name");
      if (name.equals("DMM")) {
        zoneid = ZoneId.of("Asia/Tokyo");
      }
    }
  }
  WallClock wallc = new WallClock(zoneid);
  checklistModeItems.add(wallc);
  checklistModeItems.add(new TimerBarLabel(tmbar));

  CloseButton closeb = new CloseButton(
    Glbl.W-CloseButton.W, 0);
  checklistModeItems.add(closeb);

  MinimizeButton minib = new MinimizeButton(
    Glbl.W-CloseButton.W-MinimizeButton.W, 0);
  checklistModeItems.add(minib);

  DrawerButton drawerb = new DrawerButton(
    Glbl.W-DrawerButton.W, Glbl.H/2-DrawerButton.H+7,
    AppName, AppVersion);
  checklistModeItems.add(drawerb);
}

long prevEpochSecond = 0;
int  toggle = 0;

public void draw(){
  Glbl.setBgSelected(true);
  
  boolean changed = (Glbl.prevMode != Glbl.mode);
  Glbl.prevMode = Glbl.mode;

  if (Glbl.isTimerMode()) {
    background(224);
    if (changed) {
      Glbl.changeSize();
    }
    for (GuiItem item : timerModeItems) {
      item.render();
    }
  } else {
    boolean mustRedraw = false;
    long currentEpochSecond = Instant.now().getEpochSecond();

    boolean passage0p5sec = false;
    int mil = millis() % 1000;
    if (toggle == 0) {
      if (0 <= mil && mil <= 499) {
        toggle = 1;
        passage0p5sec = true;
      }
    } else {
      if (500 <= mil && mil <= 999) {
        toggle = 0;
        passage0p5sec = true;
      }
    }

    if (mousePressed) {
      mustRedraw = true;
    } else if (pmouseX != mouseX || pmouseY != mouseY) {
      mustRedraw = true;
    } else if (Glbl.isLastOneMinute && currentEpochSecond != prevEpochSecond) {
      mustRedraw = true;
    } else if (Glbl.scouterEnabled && passage0p5sec) {
      mustRedraw = true;
    } else if (currentEpochSecond/60 != prevEpochSecond/60) {
      mustRedraw = true;
    }
    prevEpochSecond = currentEpochSecond;

    if (changed) {
      Glbl.changeSize();
      // Even if true was assigned to mustRedraw here, drawing did not occur.
      // Therefore, by assigning 0 to prevEpochSecond, next draw() will redraw it.
      prevEpochSecond = 0;
    }
    if (mustRedraw) {
      background(224);
      for (GuiItem item : checklistModeItems) {
        item.render();
      }
    }
  }
}
 
int prevMouseX = 0;
int prevMouseY = 0;

public void mousePressed(){
  prevMouseX = mouseX;
  prevMouseY = mouseY;
  if (Glbl.isTimerMode()) {
    for (GuiItem item : timerModeItems) {
      item.press();
    }
  } else {
    for (GuiItem item : checklistModeItems) {
      item.press();
    }
  }
}

public void mouseReleased() {
}

public void mouseDragged() {
  if (Glbl.isBgSelected()) {
    Point mouse = MouseInfo.getPointerInfo().getLocation();
    surface.setLocation(mouse.x - prevMouseX, mouse.y - prevMouseY - 0);
  }
}

public void exit() {
  Table tbl = new Table();
  tbl.addColumn("value");
  for (Mission m : missions) {
    TableRow row = tbl.addRow();
    row.setInt("value", m.getValue());
  }
  saveTable(tbl, SaveFileName);
  super.exit();
}
// This software is distributed under the terms of the MIT License.
// Copyright (c) 2018, 2019 molelord
// All rights reserved.


















// 8x10 dots
static final short[][] OcrNumber = {
  {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
  {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // Empty 
};

public static int getR(int c) {
  return (c>>16) & 0xFF;
}
public static int getG(int c) {
  return (c>>8) & 0xFF;
}
public 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 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;

  public static void setInstances(PSurface _ps, JFrame _jf,
    PFont _f12, PFont _f24, SoundFile _chime) {
    ps     = _ps;
    jf     = _jf;
    font12 = _f12;
    font24 = _f24;
    chime  = _chime;
  }
  public static boolean isTimerMode() {
    return mode == TimerMode ? true : false;
  }
  public 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);
      }
    }
  }
  public static void setBgSelected(boolean value) {
    bgSelected = value;
  }
  public static boolean isBgSelected() {
    return bgSelected;
  }
  public static int countHotbits(int value, int width) {
    int result = 0;
    while (value > 0) {
      if ((value & 1) == 1) {
        result++;
      }
      value >>>= 1;
    }
    return result;
  }
  
  // -1:failed  0to9:number 10:empty
  public static int ocrImage(PImage img) {
    img.filter(THRESHOLD, 0.8f);

    short[] line = new short[OcrNumberH];
    for (int py = 0; py < OcrNumberH; py++) {
      line[py] = 0;
      for (int px = 0; px < OcrNumberW; px++) {
        int c = img.pixels[OcrNumberW*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], 2) + " : " + binary(line[py], 8));
      }
    }

    int   minDiff    = OcrNumberW*OcrNumberH;
    int   minDiffNum = 0;
    int[] totalDiff  = new int[OcrNumber.length];
    for (int num = 0; num < OcrNumber.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 < OcrNumberH; i++) {
          int tmp = (OcrNumber[num][i]<<sft) ^ line[i];
          if (tmp != 0) {
            int bits = countHotbits(tmp, OcrNumberW);
            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) {
        println("OCR num:" + nf(num) +
          " diff:" + nf(totalDiff[num]));
      }
    }

    // Allow even a slight difference.
    if (minDiff < 25) {
      if (Glbl.DebugOcr) {
        println("OCR o num:" + nf(minDiffNum) +
          " diff:" + nf(totalDiff[minDiffNum]));
      }
      return minDiffNum;
    }
    if (Glbl.DebugOcr) {
      println("OCR x:");
    }
    return -1;
  }
  public 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);
  }
  public void render() {
  }
  public void press() {
  }
  public 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;
  }
  public void start(int seconds) {
    endTime = millis() + 1000*seconds;
    Glbl.mode = Glbl.TimerMode;
  }
  public 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);
    }
  }
  public 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;
  }
  public 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);
    }
  }
  public int computeSeconds() {
    int pos = mouseY/2;
    if (pos < 50) {
      // do nothing
    } else {
      pos = 50 + (pos - 50)/4*5;
    }
    return pos;
  }
  public void press() {
    if (isOver()) {
      td.start(computeSeconds());
    }
  }
}

class TimerBarLabel extends GuiItem {
  TimerBar tb;
  TimerBarLabel(TimerBar _tb) {
    super();
    tb = _tb;
  }
  public 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;
  }
  public 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);
    }
  }
  public void press() {
    if (isOver()) {
      checked=!checked;
    }
  }
  public boolean get() {
    return checked;
  }
  public void set() {
    checked = true;
  }
  public 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));
    }
  }
  public 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);
    }
  }
  public 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();
        }
      }
    }
  }
  public 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);
  }
  public 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);
  }
  public 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);
  }
  public 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);
  }
  public void press() {
    if (isOver()) {
      Glbl.jf.setExtendedState(
        Glbl.jf.getExtendedState() | JFrame.ICONIFIED);
    }
  }
}

final float[] dmmGemTimeTable = {
  12.0f, 20.0f, -1,   // Mon
  12.5f, 21.0f, -1,   // Tue
  -1,   18.0f, 21.0f, // Wed
  -1,   19.0f, 22.0f, // Tur
  -1,   20.0f, 23.0f, // Fri
  12.0f, 18.0f, 22.0f, // Sat
  12.5f, 19.0f, 23.0f, // Sun
};
final float[] nutakuGemTimeTable = {
  12.0f, 19.0f, -1,   // Mon
  12.5f, 19.5f, -1,   // Tue
  -1,   18.0f, 22.5f, // Wed
  -1,   19.0f, 23.0f, // Tur
  -1,   19.5f, 23.5f, // Fri
  12.0f, 18.0f, 22.0f, // Sat
  12.5f, 19.0f, 23.0f, // 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] = PApplet.parseInt(tbl[i]*100);
    }
  }
  public 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);
    }
  }
  public void press(){
  }
  public 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.5f;
    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;
  }
  public 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;
  }
  public 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;
  }
  public boolean searchHpBar() {
    final int offsetX  = 204;
    final int offsetY  =  32;
    final int hpPixels = 358;

    int c;
    int r, g, b;
    int pos = 0;
    int hp  = 0;
    for (pos = hpPixels; pos >= 0; pos--) {
      c = windowImg.pixels[windowImg.width*offsetY + offsetX + pos];
      r = getR(c);
      g = getG(c);
      b = getB(c);
      // find Hp  
      if (r > 240 && g < 40 && b < 90) {
        hp = pos * 100 / hpPixels;
        //println("hp:" + hp + " c:" + c);
      }
      if (hp > 0) break;
    }

    if (hp > 0) {
      PImage hpImg = windowImg.get(48, 10, Glbl.ScouterW, 116);
      hpImg.filter(GRAY);
      image(hpImg, Glbl.W, 0);

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

      final int originX = Glbl.W + offsetX - 48;
      final int originY = offsetY - 10;

      // current position
      line(originX + pos, originY+0,
        originX + pos, originY+12);

      // draw |______|
      line(originX, originY+4,
        originX, originY+9);
      line(originX + hpPixels-1, originY+4,
        originX + hpPixels-1, originY+9);
      line(originX, originY+9,
        originX + hpPixels-1, originY+9);

      // draw 50% marker
      line(originX + hpPixels/2, originY+3,
        originX + hpPixels/2, originY+9);

      // draw 30% marker
      line(originX + hpPixels*0.3f, originY+3,
        originX + hpPixels*0.3f, originY+9);

      textFont(Glbl.font24);
      textSize(24);
      fill(0, 255, 0);
      text(nf(hp) + "%", originX + hpPixels, originY);
    }

    return hp > 0 ? true : false;
  }
  public boolean searchAccessory() {
    final int offsetX = 687;
    final int offsetY = 103;
    final String[] rarelityStr = {"R", "SR", "SSR"};
    final int[]    rarelityMaxLv = {30, 40, 50};
    boolean found = false;

    PImage acImg = windowImg.get(
      offsetX, offsetY, 220, 349);

    // Lv
    int level = 0;
    for (int i = 0; i < 2; i++) {
      PImage numImg = acImg.get(
        196+i*(Glbl.OcrNumberW+2), 3, Glbl.OcrNumberW, Glbl.OcrNumberH);
      if (Glbl.DebugOcr) {
        image(numImg, Glbl.W + 400 + i*(Glbl.OcrNumberW+4), 120);
      }
      int num = Glbl.ocrImage(numImg);
      if (num < 0) {
        break;
      } else if (num < 10) {
        level = level*10 + num;
      }
    }
    if (level == 0) {
      PImage numImg = acImg.get(
        201, 3, Glbl.OcrNumberW, Glbl.OcrNumberH);
      if (Glbl.DebugOcr) {
        image(numImg, Glbl.W + 400, 135);
      }
      int num = Glbl.ocrImage(numImg);
      if (num < 0) {
        // do nothing
      } else if (num < 10) {
        level = num;
      }
    }

    // NEXT
    int nextExp = 0;
    for (int i = 0; i < 4; i++) {
      PImage numImg = acImg.get(
        111+i*(Glbl.OcrNumberW+2), 301, Glbl.OcrNumberW, Glbl.OcrNumberH);
      if (Glbl.DebugOcr) {
        image(numImg, Glbl.W + 400 + i*(Glbl.OcrNumberW+4), 150);
      }
      int num = Glbl.ocrImage(numImg);
      if (num < 0) {
        break;
      } else if (num < 10) {
        nextExp = nextExp*10 + num;
      }
    }

    // Rarelity
    int rarelity = 0;
    int c = acImg.pixels[acImg.width*155 + 162];
    int r, g, b;
    r = getR(c);
    g = getG(c);
    b = getB(c);
    rarelity = 0;
    if (
      0xda-1 <= r && r <= 0xda+1 &&
      0xd8-1 <= g && g <= 0xd8+1 &&
      0xdc-1 <= b && b <= 0xdc+1) {
      rarelity = 1;
    } else if (
      0xf4-1 <= r && r <= 0xf4+1 &&
      0xe7-1 <= g && g <= 0xe7+1 &&
      0xbd-1 <= b && b <= 0xbd+1) {
      rarelity = 2;
    }

    if (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 + 162, 155, Glbl.W + 175, 163);
    }

    return found;
  }
  public 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);
  }
  public 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);
    }
  }
  public void press() {
    if (isOver()) {
      if (!Glbl.scouterEnabled) {
        if (scouter.initialize()) {
          Glbl.scouterEnabled = true;
          Glbl.changeSize();
        }
      } else {
        Glbl.scouterEnabled = false;
        Glbl.changeSize();
      }
    }
  }
}
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "AbilityTimer" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
