/*
 * OpenKropki
 *
 * Copyright (C) 2014-2022 Mateusz Viste
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>           /* free() */
#include <string.h>           /* memset() */
#include <time.h>
#include <SDL2/SDL_thread.h>

#include "ai.h"
#include "drv_gra.h"
#include "lang.h"
#include "langdetect.h"
#include "kropcore.h"
#include "okp.h"
#include "prefs.h"
#include "ui.h"
#include "menu.h"
#include "media/data.h"       /* contains the font data, sprites etc */

/* set to non-zero to enable debug data on captured fields */
#ifndef DEBUG_CAPTURES
  #define DEBUG_CAPTURES 0
#endif


struct rozgrywka_t {
  int scrollvector[2];
  int endgamesfxplayed;
  int kratkasize;
  int points[2]; /* remember last state of points (to detect new wins and to draw screen without re-counting while AI operates) */
  SDL_Thread *ai_thread;
  int ai_in_progress; /* set to zero when the AI thread finishes thinking */
  int ai_movex, ai_movey; /* AI result; applied on the main thread only */
  enum playertype_t p[2]; /* players definitions */
  struct kropki_game *game;
  int *aidebugfields; /* list of fields that AI wants to highlight (debug purposes), format: x1, y1, x2, y2, x3, y3, (...), -1 */
  unsigned long lastmove_anim_ms; /* ticks when last move was placed; 0 = idle */
  int kratkasize_ready; /* 1 once zoom has been chosen for the current board (kept across main-menu trips) */
};


/* used to display localized type names of players during game */
static enum msgid_t playertype2name(enum playertype_t p) {
  switch (p) {
    case PLAYER_DUMBO: return(MSG_DUMBO);
    case PLAYER_TIGRAN: return(MSG_TIGRAN);
    case PLAYER_BENDER: return(MSG_BENDER);
    case PLAYER_VIKTOR: return(MSG_VIKTOR);
    case PLAYER_HUMAN: return(MSG_HUMAN);
  }
  return(MSG_HUMAN); /* useless since I cover all possible enum cases... it's just to shut gcc warning */
}


/* translates a player type enum value into a text identifier that may be written into an OKP string */
static const char *playertype2txtid(enum playertype_t p) {
  switch (p) {
    case PLAYER_DUMBO: return("DUMBO");
    case PLAYER_TIGRAN: return("TIGRAN");
    case PLAYER_BENDER: return("BENDER");
    case PLAYER_VIKTOR: return("VIKTOR");
    case PLAYER_HUMAN:
    default:           return("HUMAN");
  }
}


/* translates a player's OKP "name" into a player type enum */
static enum playertype_t txtid2playertype(const char *s) {
  if (strcmp(s, "DUMBO") == 0) return(PLAYER_DUMBO);
  if (strcmp(s, "TIGRAN") == 0) return(PLAYER_TIGRAN);
  if (strcmp(s, "BENDER") == 0) return(PLAYER_BENDER);
  if (strcmp(s, "VIKTOR") == 0) return(PLAYER_VIKTOR);
  return(PLAYER_HUMAN);
}


static void redraw_screen(struct rozgrywka_t *r, const char *msg, int msgplayercolor, int shadebases) {
  int x, y, firstx, firsty, lastx, lasty;
  const char *playernames[2];
  struct kropki_wallelement *wallsptr;
  int winw, winh;
  int i;
  int pop_scale_pm = 1000;
  /* find coordinates of the first and last visible points so rendering can be limited to visible area */
  gra_getwinsize(&winw, &winh);
  firstx = -1 - (r->scrollvector[0] / r->kratkasize);
  firsty = -1 - (r->scrollvector[1] / r->kratkasize);
  lastx = 1 - ((r->scrollvector[0] - winw) / r->kratkasize);
  lasty = 1 - ((r->scrollvector[1] - winh) / r->kratkasize);
  if (firstx < 0) firstx = 0;
  if (firsty < 0) firsty = 0;
  if (lastx < 0) lastx = 0;
  if (lasty < 0) lasty = 0;
  if (lastx >= r->game->field_width) lastx = r->game->field_width - 1;
  if (lasty >= r->game->field_height) lasty = r->game->field_height - 1;

  /* pop-in scale for the last-placed dot (permille: 1000 = normal size) */
  if (r->lastmove_anim_ms != 0) {
    unsigned long now = ui_getticks();
    unsigned long elapsed;
    const unsigned long pop_ms = 260;
    if (now >= r->lastmove_anim_ms) {
      elapsed = now - r->lastmove_anim_ms;
    } else {
      elapsed = pop_ms; /* tick wrap: skip anim */
    }
    if (elapsed >= pop_ms) {
      r->lastmove_anim_ms = 0;
      pop_scale_pm = 1000;
    } else {
      int t = (int)(elapsed * 1000 / pop_ms); /* 0..999 */
      if (t < 550) {
        /* grow from 15% to 125% */
        pop_scale_pm = 150 + (t * (1250 - 150)) / 550;
      } else {
        /* settle from 125% to 100% */
        pop_scale_pm = 1250 - ((t - 550) * 250) / 450;
      }
    }
  }

  /* draw the (empty) playfield */
  ui_drawKartka(r->game->field_width, r->game->field_height, r->scrollvector);
  /* draw walls */
  for (wallsptr = r->game->walls; wallsptr != NULL; wallsptr = wallsptr->next) {
    if ((wallsptr->x1 < firstx) || (wallsptr->x1 > lastx) || (wallsptr->y1 < firsty) || (wallsptr->y1 > lasty)) continue; /* skip walls outside the currently visible area */
    graph_drawWall(wallsptr->x1, wallsptr->y1, wallsptr->x2, wallsptr->y2, wallsptr->player, r->scrollvector);
  }
  /* draw dots */
  for (y = firsty; y <= lasty; y++) {
    for (x = firstx; x <= lastx; x++) {
      int fieldval;
      fieldval = KROPKI_GETPOS(r->game, x, y);
      if ((fieldval & 3) != 0) {
        int activeflag = 0;
        int scale_pm = 1000;
        if ((r->game->lastmovex == x) && (r->game->lastmovey == y)) {
          activeflag = 1;
          scale_pm = pop_scale_pm;
        }
        graph_drawKropka(x, y, fieldval & 3, activeflag, scale_pm, r->scrollvector);
      }
#if DEBUG_CAPTURES != 0
      /* --- DEBUG START --- */
      if ((fieldval >> 2) & 3) { /* draw a circle around field if captured */
        uint32_t col = 0x1010ffu;
        if (((fieldval >> 2) & 3) == 2) col = 0xff0000u;
        gra_circle((x + 1) * r->kratkasize + r->scrollvector[0], (y + 1) * r->kratkasize + r->scrollvector[1], r->kratkasize * 4 / 7, col, 0xff);
        gra_circle((x + 1) * r->kratkasize + r->scrollvector[0], (y + 1) * r->kratkasize + r->scrollvector[1], r->kratkasize * 4 / 7 - 1, col, 0xff);
      }
      /* --- DEBUG STOP ---*/
#endif
    }
  }
  if (r->aidebugfields != NULL) {
    for (i = 0;; i += 2) {
      x = r->aidebugfields[i];
      if (x < 0) break;
      y = r->aidebugfields[i + 1];
      gra_circle((x + 1) * r->kratkasize + r->scrollvector[0], (y + 1) * r->kratkasize + r->scrollvector[1], r->kratkasize * 4 / 7 - 1, 0x10ff10, 0xff);
    }
  }
  /* draw shaded (captured) areas, if enabled */
  if (shadebases != 0) {
    for (y = firsty; y < lasty; y++) {
      for (x = firstx; x < lastx; x++) {
        int fieldval, capturedby;
        fieldval = KROPKI_GETPOS(r->game, x, y);
        capturedby = (fieldval >> 2) & 3;
        /* shading for captured dots */
        if (capturedby != 0) {
          /* start by drawing a patterned diamond around the dot */
          ui_shadediamond(x, y, capturedby, r->scrollvector);
          /* check right neighbor and draw a big rect there if neighbor captured */
          if ((kropki_getpos(r->game, x + 1, y) >> 2) & 3) {
            ui_shadeblock(x, y, 1, 2, capturedby, r->scrollvector);
          }
          /* check top neighbor */
          if ((kropki_getpos(r->game, x, y - 1) >> 2) & 3) {
            ui_shadeblock(x - 1, y, 2, 1, capturedby, r->scrollvector);
          }
        }
      }
    }
  }
  /* Draw scores */
  for (i = 0; i < 2; i++) {
    playernames[i] = lang_msg(playertype2name(r->p[i]));
  }
  ui_drawscores(r->points, msg, msgplayercolor, playernames);
  /* refresh the screen */
  gra_refresh();
}



#define COLORPLAYER1 0x3030FFu
#define COLORPLAYER2 0xFF0000u
#define PAPER_COLOR 0xF6F0E4u   /* cream notebook page */
#define GRID_COLOR  0xA9C0D8u   /* faint ruled-paper blue */

/* Prefer ~9 mm cells (matches old ~36 px @ 96 DPI), then shrink to fit the window. */
static int pick_default_kratkasize(int fw, int fh) {
  int winw, winh;
  int kx, ky, k_fit, k_want, k;
  int min_k, max_k;
  float dpi;
  const int fallback = 36;

  if ((fw < 1) || (fh < 1)) return(fallback);
  gra_getwinsize(&winw, &winh);
  if ((winw < 1) || (winh < 1)) return(fallback);

  dpi = gra_get_display_dpi();
  /* physical targets */
  k_want = (int)(9.0f * dpi / 25.4f + 0.5f);
  min_k = (int)(4.0f * dpi / 25.4f + 0.5f);
  max_k = (int)(14.0f * dpi / 25.4f + 0.5f);
  if (min_k < 8) min_k = 8;
  if (max_k < 48) max_k = 48;
  if (k_want < min_k) k_want = min_k;
  if (k_want > max_k) k_want = max_k;

  /* paper pixels are (fw+1)*k by (fh+1)*k -- never larger than ~88% of the window */
  kx = (winw * 88 / 100) / (fw + 1);
  ky = (winh * 88 / 100) / (fh + 1);
  k_fit = (kx < ky) ? kx : ky;
  if (k_fit < 1) k_fit = 1;

  k = (k_want < k_fit) ? k_want : k_fit;
  if (k < min_k) k = min_k;
  if (k > max_k) k = max_k;
  /* if the board still cannot fit, allow going below the physical min */
  if (k > k_fit) k = k_fit;
  if (k < 4) k = 4;
  return(k);
}


/* Pick scroll on one axis so as much of the page as possible is visible.
 * If a focus point is given (focus_set != 0), pull toward centering it,
 * but never at the cost of showing empty desk where the page could be. */
static int scroll_for_axis(int view0, int view_len, int page_len, int trim, int focus_set, int focus) {
  int page_center;
  int preferred;
  int min_s, max_s;

  page_center = view0 + (view_len - page_len) / 2 - trim;
  if (focus_set) {
    preferred = view0 + (view_len / 2) - focus;
  } else {
    preferred = page_center;
  }

  if (page_len <= view_len) {
    /* Whole page fits: centering it uniquely maximizes visible playfield. */
    return(page_center);
  }

  /* Page larger than the view: keep the view fully covered by paper, then
   * among those scrolls prefer the focus (last move) as centered as possible. */
  min_s = view0 + view_len - page_len - trim;
  max_s = view0 - trim;
  if (preferred < min_s) preferred = min_s;
  if (preferred > max_s) preferred = max_s;
  return(preferred);
}


static void centerscrollvector(struct rozgrywka_t *rozgrywka) {
  int winw, winh;
  int barh;
  int page_w, page_h;
  int usable_h;
  int trim;
  int focus_set = 0;
  int focus_x = 0, focus_y = 0;

  gra_getwinsize(&winw, &winh);
  barh = ui_getscorebarheight();
  usable_h = winh - barh;
  if (usable_h < 1) usable_h = winh;

  page_w = (rozgrywka->game->field_width + 1) * rozgrywka->kratkasize;
  page_h = (rozgrywka->game->field_height + 1) * rozgrywka->kratkasize;
  /* visual cream page is trimmed so outer margin is ~2/3 cell */
  trim = rozgrywka->kratkasize / 3;
  if (trim < 1) trim = 1;
  page_w -= 2 * trim;
  page_h -= 2 * trim;

  if (rozgrywka->game->lastplayer != 0) {
    focus_set = 1;
    focus_x = (rozgrywka->game->lastmovex + 1) * rozgrywka->kratkasize;
    focus_y = (rozgrywka->game->lastmovey + 1) * rozgrywka->kratkasize;
  }

  rozgrywka->scrollvector[0] = scroll_for_axis(0, winw, page_w, trim, focus_set, focus_x);
  rozgrywka->scrollvector[1] = scroll_for_axis(barh, usable_h, page_h, trim, focus_set, focus_y);
}


/* a wrapper around ai_play() that can be called as a thread.
 * Computes a move only -- the live game is updated on the main thread. */
static int ai_caller(void *rozgrywka) {
  char *okps;
  int movex = -1, movey = -1;
  struct rozgrywka_t *r = rozgrywka;
  unsigned long aithinkuntil;
  /* preset a timer to wait at least 0.5s so AI feels more natural and avoid sfx sounds to queue on */
  aithinkuntil = ui_getticks() + 500;
  /* prepare the okps field description */
  okps = kropki_struct2okp(r->game, NULL, NULL);
  if (okps == NULL) {
    puts("ai_caller: kropki_struct2okp() FAIL!");
    r->ai_movex = -1;
    r->ai_movey = -1;
    r->ai_in_progress = 0;
    return(0);
  }
  /* run A.I. */
  printf("calling AI: '%s'\n", okps);
  switch (r->p[r->game->nextplayer - 1]) {
    case PLAYER_DUMBO:
      ai_dumbo(okps, &movex, &movey);
      break;
    case PLAYER_TIGRAN:
      ai_tigran(okps, &movex, &movey, &(r->aidebugfields));
      break;
    case PLAYER_BENDER:
      ai_bender(okps, &movex, &movey);
      break;
    case PLAYER_VIKTOR:
      ai_viktor(okps, &movex, &movey);
      break;
    default:
      printf("ERROR: UNEXPECTED PLAYER TYPE\n");
      break;
  }
  free(okps);
  /* artificial delay (if AI was too fast) */
  for (;;) {
    if (ui_getticks() >= aithinkuntil) break;
    if (ui_getticks() + 4000 < aithinkuntil) break; /* wrap protection */
    ui_delay(10);
  }
  /* publish answer for the main thread (do not touch r->game here) */
  printf("AI answer: %d.%d\n", movex, movey);
  r->ai_movex = movex;
  r->ai_movey = movey;
  r->ai_in_progress = 0;
  return(0);
}


/* wait for AI thread to terminate, if it is pending and displays a message
 * about this, unless nomsg is non-zero. */
static void finish_ai_thread(struct rozgrywka_t *r) {
  if ((r != NULL) && (r->ai_thread != NULL)) {
    ui_changecursor(UI_CURSOR_WAIT);
    if (r->ai_in_progress != 0) ui_throwmsg(lang_msg(MSG_WAIT_FOR_AI_ENGINE));
    SDL_WaitThread(r->ai_thread, NULL);
    ui_changecursor(UI_CURSOR_NORMAL);
    r->ai_thread = NULL;
  }
}


/* Apply a pending AI move on the main thread. Returns the player who moved (1/2), or 0. */
static int apply_pending_ai_move(struct rozgrywka_t *r) {
  int mx, my, who;
  if (r == NULL || r->game == NULL) return(0);
  mx = r->ai_movex;
  my = r->ai_movey;
  r->ai_movex = -1;
  r->ai_movey = -1;
  if ((mx < 0) || (my < 0) || (r->game->nextplayer < 1)) return(0);
  who = r->game->nextplayer;
  if (kropki_setpos(r->game, mx, my, who, 0) != 0) return(0);
  return(who);
}


static void rozgrywka_init(struct rozgrywka_t *r) {
  memset(r, 0, sizeof(struct rozgrywka_t));
  r->kratkasize = 36; /* placeholder; real default set when a board exists */
  r->ai_movex = -1;
  r->ai_movey = -1;
  /* reset AIs that need a reset */
  ai_tigran_reset();
  ai_viktor_reset();
}


static void resume_save(const struct rozgrywka_t *r) {
  char *okps;
  const struct kropki_game *g = r->game;

  okps = kropki_struct2okp(g, playertype2txtid(r->p[0]), playertype2txtid(r->p[1]));
  if (okps == NULL) {
    printf("resume_save fail: kropki_struct2okp() returned NULL!\n");
  } else {
    prefs_save("resume.okp", okps);
    free(okps);
  }
}


static void resume_load(struct rozgrywka_t *r) {
  char *okps;
  char playername1[64];
  char playername2[64];
  if (r->game != NULL) {
    printf("ERROR: resume_load() called while r->game != NULL\n");
    return;
  }
  okps = prefs_load("resume.okp");
  if (okps != NULL) r->game = kropki_okp2struct(okps, playername1, playername2, sizeof(playername1));
  free(okps);
  /* */
  if (r->game == NULL) {
    return;
  }
  printf("loaded a resume state of a %dx%d game, lastplayer=%d, nextplayer=%d, scores=%d:%d playablefields=%d\n", r->game->field_width, r->game->field_height, r->game->lastplayer, r->game->nextplayer, r->game->score[0], r->game->score[1], r->game->playablefields);
  /* ignore the resume state if game is no longer playable */
  if (r->game->nextplayer == 0) {
    printf("resume state appears to be an ended game -> ignoring it\n");
    kropki_freegame(r->game);
    r->game = NULL;
  } else { /* else copy the scores */
    r->p[0] = txtid2playertype(playername1);
    r->p[1] = txtid2playertype(playername2);
    r->points[0] = r->game->score[0];
    r->points[1] = r->game->score[1];
  }
}


int main(void) {
  struct rozgrywka_t rozgrywka;
  int exitflag = 0;
  int sound;
  int shadebases;
  int lang;

  ui_init();

  /* fetch preferences */
  {
    lang = prefs_loadint("language", LANG_ERR);
    if ((lang >= LANG_ERR) || (lang < 0)) {
      lang = langdetect();
      if (lang == LANG_ERR) lang = LANG_EN;
    }
    sound = prefs_loadint("sound", 1);
    shadebases = prefs_loadint("shadebases", 1);
  }

  /* init language module */
  lang_init(lang);

  /* seed the random generator, AIs often rely on rand() */
  srand(time(NULL));

  rozgrywka_init(&rozgrywka);

  /* load the last "resume" gamestate (if available) */
  resume_load(&rozgrywka);

  while (exitflag == 0) {
    int menupos;

    menupos = menu_main(rozgrywka.game != NULL, sound);

    if ((menupos == MAINMENU_QUIT) || (menupos == UI_MENU_BACK) || (menupos == UI_MENU_EXIT)) {
      break;
    } else if (menupos == MAINMENU_NEWGAME) {
      int w, h, choice, playsize = 6;
      int i;
      enum playertype_t p[2] = {PLAYER_HUMAN, PLAYER_VIKTOR};
      /* playfield size menu */
      choice = menu_playconf(sound, &playsize, p);
      /* translate playfield size to width and height */
      if (playsize == 4) { /* A4 */
        w = 58;
        h = 42;
      } else if (playsize == 5) {
        w = 42;
        h = 28;
      } else if (playsize == 6) {
        w = 28;
        h = 20;
      } else if (playsize == 7) {
        w = 20;
        h = 14;
      }
      if (choice == UI_MENU_EXIT) {
        exitflag = 1;
        continue;
      } else if (choice == UI_MENU_BACK) {
        continue;
      } else if ((choice != 150) && (choice != 151)) { /* ignore anything that is not a play action */
        continue;
      }
      /* make sure no AI thread is running before freeing a previous game! */
      finish_ai_thread(&rozgrywka);
      rozgrywka.ai_movex = -1;
      rozgrywka.ai_movey = -1;
      kropki_freegame(rozgrywka.game);
      /* */
      rozgrywka_init(&rozgrywka);
      rozgrywka.game = kropki_newgame(w, h, 1 + choice - 150);
      for (i = 0; i < 2; i++) {
        rozgrywka.p[i] = p[i];
      }
    } else if (menupos == MAINMENU_SETTINGS) {
      for (;;) {
        int choice = menu_settings(sound, shadebases, lang);
        if (choice == UI_MENU_EXIT) {
          exitflag = 1;
          break;
        } else if (choice == SETMENU_LANG) {
          int choice = menu_lang(sound);
          if (choice == UI_MENU_EXIT) {
            exitflag = 1;
          } else if (choice < LANG_ERR) {
            lang = choice;
            lang_init(lang);
            prefs_saveint("language", lang);
          }
          continue;
        } else if (choice == SETMENU_SOUND) {
          sound ^= 1;
          prefs_saveint("sound", sound);
          continue;
        } else if (choice == SETMENU_SHADEBASES) {
          shadebases ^= 1;
          prefs_saveint("shadebases", shadebases);
          continue;
        }
        break;
      }
      continue;
    } else if (menupos == MAINMENU_ABOUT) {
      int choice = menu_about(sound);
      if (choice == UI_MENU_EXIT) exitflag = 1;
      continue;
    } else if (menupos == MAINMENU_RESUME) {
      /* nothing to do */
    } else {
      /* abnormal choice, let's play it safe and ignore it */
      continue;
    }
    /* new game (or cold resume): pick a smart default zoom; otherwise keep the
     * zoom the player already had when they left for the main menu */
    if (!rozgrywka.kratkasize_ready) {
      rozgrywka.kratkasize = pick_default_kratkasize(rozgrywka.game->field_width, rozgrywka.game->field_height);
      rozgrywka.kratkasize_ready = 1;
    }
    centerscrollvector(&rozgrywka);

    if (ui_setparams(rozgrywka.game->field_width, rozgrywka.game->field_height, rozgrywka.kratkasize, 2, GRID_COLOR, COLORPLAYER1, COLORPLAYER2, PAPER_COLOR, rozgrywka.kratkasize/10, rozgrywka.kratkasize/4, 0xb5540cu) != 0) return(0);

    {
    int resume_dirty = 0;
    struct ui_event_t pending_event;
    int have_pending_event = 0;

    while (exitflag == 0) {
      struct ui_event_t event;
      const char *msg = NULL;
      char msgcustom[256];
      int msgplayercolor = 0; /* 0=no player  1=player A  2=player B */
      unsigned long frame_start;

      /* flush a deferred resume save once the pop-in animation is done */
      if (resume_dirty && (rozgrywka.lastmove_anim_ms == 0)) {
        resume_save(&rozgrywka);
        resume_dirty = 0;
      }

      /* if AI finished thinking, join and apply its move on this thread */
      if ((rozgrywka.ai_thread != NULL) && (rozgrywka.ai_in_progress == 0)) {
        int who;
        finish_ai_thread(&rozgrywka);
        who = apply_pending_ai_move(&rozgrywka);
        if (who > 0) {
          if (sound) {
            if (who == 1) {
              gra_playwavgz(sfx_pop1_wav_gz, sfx_pop1_wav_gz_len);
            } else {
              gra_playwavgz(sfx_pop3_wav_gz, sfx_pop3_wav_gz_len);
            }
          }
          /* start the pop clock after slow work so early frames aren't skipped */
          rozgrywka.lastmove_anim_ms = ui_getticks();
          resume_dirty = 1;
        }
      }

      /* if points changed, play the 'collected' sound (check only if no AI processes the playfield) */
      if (rozgrywka.ai_thread == NULL) {
        if ((rozgrywka.game->score[0] != rozgrywka.points[0]) || (rozgrywka.game->score[1] != rozgrywka.points[1])) {
          if (sound) gra_playwavgz(sfx_collect_wav_gz, sfx_collect_wav_gz_len);
          rozgrywka.points[0] = rozgrywka.game->score[0];
          rozgrywka.points[1] = rozgrywka.game->score[1];
        }
      }
      /* check whether the game ended (do not check if AI thinks) */
      if ((rozgrywka.ai_thread == NULL) && (rozgrywka.game->nextplayer == 0)) {
        int winnerid = -1;
        if (rozgrywka.points[0] > rozgrywka.points[1]) {
          winnerid = 0;
        } else if (rozgrywka.points[0] < rozgrywka.points[1]) {
          winnerid = 1;
        } else { /* remis */
          if ((rozgrywka.endgamesfxplayed == 0) && (sound)) gra_playwavgz(sfx_death_wav_gz, sfx_death_wav_gz_len);
          msgplayercolor = 0;
          msg = lang_msg(MSG_NOBWINS);
        }
        if (winnerid >= 0) {
          snprintf(msgcustom, sizeof(msgcustom), lang_msg(MSG_HEWINS), lang_msg(playertype2name(rozgrywka.p[winnerid])));
          msg = msgcustom;
          if (rozgrywka.p[winnerid] == PLAYER_HUMAN) {
            if ((rozgrywka.endgamesfxplayed == 0) && (sound)) gra_playwavgz(sfx_winfantasia_wav_gz, sfx_winfantasia_wav_gz_len);
          } else {
            if ((rozgrywka.endgamesfxplayed == 0) && (sound)) gra_playwavgz(sfx_death_wav_gz, sfx_death_wav_gz_len);
          }
          msgplayercolor = winnerid + 1;
        }
        rozgrywka.endgamesfxplayed = 1; /* used to avoid playing 'win' sfx many times */
      } else if (rozgrywka.game->nextplayer > 0) { /* game is in progress: display who moves now */
        msgplayercolor = rozgrywka.game->nextplayer;
        if (rozgrywka.p[rozgrywka.game->nextplayer - 1] == PLAYER_HUMAN) {
          msg = lang_msg(MSG_YOURTURN);
        } else {
          msg = lang_msg(MSG_AITHINKS);
        }
      }
      frame_start = ui_getticks();
      redraw_screen(&rozgrywka, msg, msgplayercolor, shadebases);

      /* is it time for AI to play? (nextplayer is 0 when the game is over) */
      if ((rozgrywka.ai_thread == NULL)
       && (rozgrywka.game->nextplayer >= 1)
       && (rozgrywka.p[rozgrywka.game->nextplayer - 1] != PLAYER_HUMAN)) {
        rozgrywka.ai_movex = -1;
        rozgrywka.ai_movey = -1;
        rozgrywka.ai_in_progress = 1; /* this is reset by the thread once job is done */
        rozgrywka.ai_thread = SDL_CreateThread(ai_caller, "openkropki_ai", &rozgrywka);
      }

      /* wait for an event; during pop-in, pace ~60fps from wall clock */
      {
        int wait_ms = 100;
        if (rozgrywka.lastmove_anim_ms != 0) {
          unsigned long spent = ui_getticks() - frame_start;
          wait_ms = (spent < 16) ? (int)(16 - spent) : 0;
        }
        if (have_pending_event) {
          event = pending_event;
          have_pending_event = 0;
        } else {
          ui_getevent_to(&event, 0, wait_ms);
        }
      }

      if (event.type == Event_QUIT) {
        exitflag = 1;
        break;
      } else if ((event.type == Event_CLICK) && (rozgrywka.ai_thread == NULL)
              && (rozgrywka.game->nextplayer >= 1)
              && (rozgrywka.p[rozgrywka.game->nextplayer - 1] == PLAYER_HUMAN)
              && (event.button >= 1) && (event.button <= 2)) { /* left/right click on human turn only */
        if (event.button == 1) {
          int fieldx, fieldy;
          graph_mousemap(event.x, event.y, &fieldx, &fieldy, rozgrywka.scrollvector);
          /* does click points to a field? */
          if ((fieldx >= 0) && (fieldy >= 0)) {
            if (kropki_getpos(rozgrywka.game, fieldx, fieldy) != 0) { /* is this field actually empty? */
              if (sound) gra_playwavgz(sfx_wrong_wav_gz, sfx_wrong_wav_gz_len); /* bad move */
            } else {
              if (sound) {
                if (rozgrywka.game->nextplayer == 1) {
                  gra_playwavgz(sfx_pop1_wav_gz, sfx_pop1_wav_gz_len);
                } else {
                  gra_playwavgz(sfx_pop3_wav_gz, sfx_pop3_wav_gz_len);
                }
              }
              kropki_setpos(rozgrywka.game, fieldx, fieldy, rozgrywka.game->nextplayer, 0);
              /* start pop after sfx decode; defer disk save until anim ends */
              rozgrywka.lastmove_anim_ms = ui_getticks();
              resume_dirty = 1;
            }
          }
        } else if (event.button == 2) {   /* right click: center field around last point */
          centerscrollvector(&rozgrywka);
        }
      } else if (event.type == Event_KEYPRESS) {
        if (event.button == 0x1B) break; /* ESC */
      } else if (event.type == Event_SWEEP) {
        int winw, winh;
        gra_getwinsize(&winw, &winh);
        /* coalesce queued drag events into one scroll + one redraw */
        for (;;) {
          rozgrywka.scrollvector[0] -= event.x;
          rozgrywka.scrollvector[1] -= event.y;
          if (rozgrywka.scrollvector[0] > winw - 16) rozgrywka.scrollvector[0] = winw - 16;
          if (rozgrywka.scrollvector[1] > winh - 16) rozgrywka.scrollvector[1] = winh - 16;
          if (rozgrywka.scrollvector[0] < -16 - (rozgrywka.game->field_width * rozgrywka.kratkasize)) rozgrywka.scrollvector[0] = -16 - (rozgrywka.game->field_width * rozgrywka.kratkasize);
          if (rozgrywka.scrollvector[1] < 12 - (rozgrywka.game->field_height * rozgrywka.kratkasize)) rozgrywka.scrollvector[1] = 12 - (rozgrywka.game->field_height * rozgrywka.kratkasize);
          {
            struct ui_event_t more;
            ui_getevent_to(&more, 0, 0);
            if (more.type == Event_SWEEP) {
              event = more;
              continue;
            }
            if (more.type != Event_NONE) {
              pending_event = more;
              have_pending_event = 1;
            }
            break;
          }
        }
      } else if ((event.type == Event_SCROLL) || ((event.type == Event_CLICK) && (event.button == 3))) {
        int mouseptrx, mouseptry, oldkratkasize;
        int step = 1 + (rozgrywka.kratkasize / 32);
        /* compute mouse pointer position relative to field, in kratkasize * 1000 units */
        ui_getmousepos(&mouseptrx, &mouseptry);
        mouseptrx -= rozgrywka.scrollvector[0];
        mouseptry -= rozgrywka.scrollvector[1];
        mouseptrx *= 1000;
        mouseptry *= 1000;
        oldkratkasize = rozgrywka.kratkasize; /* remember what the kratkasize unit is */
        /* change kratkasize accordingly to the zoom event */
        if (event.type == Event_SCROLL) {
          if (event.y > 0) rozgrywka.kratkasize += step;
          if (event.y < 0) rozgrywka.kratkasize -= step;
        } else { /* else it's a middle click (reset zoom to smart default) */
          rozgrywka.kratkasize = pick_default_kratkasize(rozgrywka.game->field_width, rozgrywka.game->field_height);
        }
        /* enforce upper and lower zoom boundaries */
        if (rozgrywka.kratkasize > 512) rozgrywka.kratkasize = 512;
        if (rozgrywka.kratkasize < 4) rozgrywka.kratkasize = 4;
        printf("new kratka size: %d px\n", rozgrywka.kratkasize);
        ui_setparams(rozgrywka.game->field_width, rozgrywka.game->field_height, rozgrywka.kratkasize, 2, GRID_COLOR, COLORPLAYER1, COLORPLAYER2, PAPER_COLOR, rozgrywka.kratkasize/10, rozgrywka.kratkasize/4, 0xb5540cu);
        /* adjust scroll vectors so the cursor is still at the same place it was before */
        rozgrywka.scrollvector[0] += (((mouseptrx / rozgrywka.kratkasize) - (mouseptrx / oldkratkasize)) * rozgrywka.kratkasize) / 1000;
        rozgrywka.scrollvector[1] += (((mouseptry / rozgrywka.kratkasize) - (mouseptry / oldkratkasize)) * rozgrywka.kratkasize) / 1000;
      } else if (event.type != Event_NONE) {
        /* puts("got an unknown event"); */
      }

    }

    /* persist any move that was still animating when leaving the board */
    if (resume_dirty) resume_save(&rozgrywka);
    }
  }

  /* clean up and quit */
  finish_ai_thread(&rozgrywka);
  rozgrywka.ai_movex = -1;
  rozgrywka.ai_movey = -1;
  kropki_freegame(rozgrywka.game);
  gra_close();

  return(0);
}
