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.Point; 
import java.time.*; 
import java.util.Locale; 
import java.time.format.TextStyle; 

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 version = "0.44";
final String saveFileName = "save.csv";
final int checklistModeWidth  = 240;
final int checklistModeHeight = 320;
final int timerModeWidth      = 130;
final int timerModeHeight     = 100;

boolean isBackgroundSelected = true;
JFrame jframe;

static class Helper {
  public static String oxStr(int v) {
    String zeros = "";
    if (v < 10) {
      zeros = "0";
    }
    return zeros + str(v);
  }
  public static String ooxStr(int v) {
    String zeros = "";
    if (v < 10) {
      zeros = "00";
    } else if (v < 100) {
      zeros = "0";
    }
    return zeros + str(v);
  }
}

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 = jframe.getX();
    int windowY = jframe.getY(); 
    int maxX = windowX + (timerMode ? timerModeWidth  : checklistModeWidth);
    int maxY = windowY + (timerMode ? timerModeHeight : checklistModeHeight);
    Point mouse = MouseInfo.getPointerInfo().getLocation();
    boolean rc = false;
    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;
      isBackgroundSelected = rc ? false : isBackgroundSelected;
    }
    return rc;
  }
}

PFont font12;
PFont font24;
SoundFile chime;

// true : Timer / false : Checklist
boolean timerMode     = false;
boolean prevTimerMode = true;

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

class TimerBar extends GuiItem {
  int seconds;
  TimerDisplay td;
  TimerBar(int _seconds, int _x, int _y, TimerDisplay _td){
    super(_x, _y, 64, 310);
    seconds = _seconds;
    td = _td;
  }
  public void render(){
    stroke(224);
    strokeWeight(1);
    fill(255); // Left side
    triangle(x,y+5, x,y+itemHeight, x+itemWidth-1,y+itemHeight-1);
    fill(240); // Right side
    triangle(x,y, x+itemWidth,y,  x+itemWidth-1,y+itemHeight-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(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;
  Checkbox(int _x, int _y, boolean _checked) {
    super(_x, _y, 20, 20);
    checked = _checked;
  }
  public void render() {
    stroke(0); // color of box's flame
    strokeWeight(1);
    fill(isOver()?224:255); // color of box
    rect(x, y, itemWidth-1, itemHeight-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(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 {
  CloseButton(int _x, int _y){
    super(_x, _y, 20, 20);
  }
  public void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, itemWidth-1, itemHeight-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();
    }
  }
}

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;
  WallClock(ZoneId _zid) {
    super(checklistModeWidth - 82 - 1, checklistModeHeight - 20 - 1, 82, 20);
    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;
    for (int i = 0; i < 3; i++) {
      int gemHour = gemTimeTable[(w-1)*3 + i];
      if (gemHour <= nowHour && nowHour < gemHour + 50) {
        isGemTime = 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, itemWidth-1, itemHeight-1);

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

    if (isOver()) {
      putGemQuestTable(currentTime);
    } else {
      // put Timezone
      textFont(font12);
      textSize(12);
      fill(0);
      textAlign(RIGHT);
      text(zid.toString(), x-2, checklistModeHeight-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, checklistModeWidth-9, 201);
    fill(255);
    rect(5, 85, checklistModeWidth-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(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 += ">" + Helper.oxStr(lh) + ":" + Helper.oxStr(lm) + "<";
          } else {
            msg += " " + Helper.oxStr(lh) + ":" + Helper.oxStr(lm) + " ";
          }
        }
      }
      textFont(font24);
      textSize(12);
      fill(0);
      text(msg, 15, msgY);
      msgY += msgDeltaY;
    }
    textFont(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);
  }
}

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

public void settings() {
  size(checklistModeWidth, checklistModeHeight);
}

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

  surface.setResizable(true);
  
  
  TimerDisplay tmdisp = new TimerDisplay(30, 5);
  timerModeItems.add(tmdisp);
  CloseButton cbutton_t = new CloseButton(timerModeWidth-20, 0);
  timerModeItems.add(cbutton_t);

  TimerBar tmbar = new TimerBar(180, 5, 5, tmdisp);
  memoModeItems.add(tmbar);
  font12 = loadFont("mplus-2p-bold-12.vlw");
  font24 = loadFont("mplus-2m-bold-24.vlw");
  chime = new SoundFile(this, "chime.mp3");

  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);
      memoModeItems.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);
  memoModeItems.add(wallc);
  memoModeItems.add(new TimerBarLabel(tmbar));
  CloseButton closeb = new CloseButton(checklistModeWidth-20, 0);
  memoModeItems.add(closeb);
}

public void draw(){
  background(224);
  isBackgroundSelected = true;
  
  boolean changed = (prevTimerMode != timerMode);
  prevTimerMode = timerMode;

  if (timerMode) {
    if (changed) {
      jframe.setOpacity(.5f);
      surface.setSize(timerModeWidth, timerModeHeight);
    }
    for (GuiItem item : timerModeItems) {
      item.render();
    }
  } else {
    if (changed) {
      jframe.setOpacity(.75f);
      surface.setSize(checklistModeWidth, checklistModeHeight);
    }
    for (GuiItem item : memoModeItems) {
      item.render();
    }
  }
}
 
int prevMouseX = 0;
int prevMouseY = 0;

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

public void mouseReleased() {
}

public void mouseDragged() {
  if (isBackgroundSelected) {
    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();
}
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "AbilityTimer" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
