#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include "termio.h"

#define WID 10
#define HEI 22

#define USE_UNICODE


const int stones[7][4]={
	{0b0000010001110000, 0b0000001100100010, 0b0000000001110001, 0b0000001000100110},
	{0b0000000101110000, 0b0000001000100011, 0b0000000001110100, 0b0000011000100010},
	{0b0000001101100000, 0b0000010001100010, 0b0000001101100000, 0b0000010001100010},
	{0b0000001001110000, 0b0000001000110010, 0b0000000001110010, 0b0000001001100010},
	{0b0000011000110000, 0b0000001001100100, 0b0000011000110000, 0b0000001001100100},
	{0b0000111100000000, 0b0100010001000100, 0b0000111100000000, 0b0100010001000100},
	{0b0000011001100000, 0b0000011001100000, 0b0000011001100000, 0b0000011001100000},
};

static int leveldelay(int level){
	int d=700-(level-1)*35;
	if(d<50)d=50;
	return d;
}


struct board {
	int lines[HEI];
	int stone,str,stx,sty;

	int ncleared,level,delay;
};

static bool boardvalid(const struct board *bd){
	int st=stones[bd->stone][bd->str];
	for(int y=0;y<4;y++){
		for(int x=0;x<4;x++){
			int bit=1<<(4*y+x);
			if(st&bit){
				if(bd->sty+y>=HEI)return false;
				if(bd->stx+x<0||bd->stx+x>=WID)return false;
			}
			if(bd->lines[bd->sty+y]&((!!(st&bit))<<(bd->stx+x)))return false;
		}
	}
	return true;
}

static void persist(struct board *bd){
	int st=stones[bd->stone][bd->str];
	for(int y=0;y<4;y++){
		for(int x=0;x<4;x++){
			bd->lines[bd->sty+y]|=(!!(st&(1<<(4*y+x))))<<(bd->stx+x);
		}
	}
}

// [0,mx)
static int randomrange(unsigned int mx){
	assert(mx<=(unsigned int)RAND_MAX+1);
	unsigned int diff=((unsigned int)RAND_MAX+1)%mx;
	unsigned int r=random();
	// happens at most 50% of the time
	if(r>RAND_MAX-diff)return randomrange(mx);
	return r%mx;
}

// returns false iff game over
static bool spawn(struct board *bd){
	bd->stone=randomrange(7);
	bd->str=random()%4;
	bd->stx=WID/2-1;
	for(bd->sty=0;(stones[bd->stone][bd->str]&((1<<(4+4*-bd->sty))-1))==0;bd->sty--);
	return boardvalid(bd);
}

static void show(const struct board *bd){
	struct board bd2;
	memcpy(&bd2,bd,sizeof bd2);
	persist(&bd2);
	clearscreen();

#ifdef USE_UNICODE
	assert(WID%2==0);
	int boardwid=WID*3/2;
#else
	int boardwid=WID;
#endif

	for(int y=0;y<HEI;y++){
		moveto(0,y);
		tputc('|');
		for(int x=0;x<WID;x++){
#ifdef USE_UNICODE
			if(x%2==1){
				if(bd2.lines[y]&(1<<(x-1))){
					if(bd2.lines[y]&(1<<x))tprintf("█");
					else tprintf("▌");
				} else {
					if(bd2.lines[y]&(1<<x))tprintf("▐");
					else tputc(' ');
				}
			}
			if(bd2.lines[y]&(1<<x))tprintf("█");
			else tputc(' ');
#else
			if(bd2.lines[y]&(1<<x))tputc('#');
			else tputc(' ');
#endif
		}
		tputc('|');
	}

	moveto(0,HEI);
	tputc('+');
	for(int x=0;x<boardwid;x++)tputc('-');
	tputc('+');

	moveto(boardwid+3,1);
	tprintf("Lines: %d\n\nLevel: %d",bd->ncleared,bd->level);

	redraw();
}

static void checklines(struct board *bd){
	for(int y=HEI-1;y>=0;y--){
		if(bd->lines[y]==(1<<WID)-1){
			memmove(bd->lines+1,bd->lines,y*sizeof(int));
			bd->lines[0]=0;
			y++;
			bd->ncleared++;
			if(bd->ncleared%10==0){
				bd->level++;
				bd->delay=leveldelay(bd->level);
			}
		}
	}
}

// returns false iff game over
static bool advance(struct board *bd){
	if(bd->stone<0){
		return spawn(bd);
	}
	bd->sty++;
	if(!boardvalid(bd)){
		bd->sty--;
		persist(bd);
		checklines(bd);
		return spawn(bd);
	}
	return true;
}

static void move(struct board *bd,int dir){
	bd->stx+=dir;
	if(!boardvalid(bd))bd->stx-=dir;
}

static void rotR(struct board *bd){
	bd->str=(bd->str+1)%4;
	if(!boardvalid(bd))bd->str=(bd->str+3)%4;
}

static void delaytimeout(const struct board *bd,struct timeval *tv){
	tv->tv_sec=bd->delay/1000;
	tv->tv_usec=bd->delay%1000*1000;
}


int main(void){
	{
		struct timeval tv;
		gettimeofday(&tv,NULL);
		srandom(tv.tv_sec*1000000L+tv.tv_usec);
	}

	initkeyboard(false);
	atexit(endkeyboard);
	initscreen();
	atexit(endscreen);
	cursorvisible(false);

	struct board bd;
	memset(&bd,0,sizeof bd);
	bd.stone=-1;
	bd.ncleared=0;
	bd.level=1;
	bd.delay=leveldelay(bd.level);
	spawn(&bd);

	struct timeval timeleft;
	delaytimeout(&bd,&timeleft);

	while(true){
		show(&bd);

		struct timeval start;
		gettimeofday(&start,NULL);

		fd_set inset;
		FD_ZERO(&inset);
		FD_SET(0,&inset);
		struct timeval timeout;
		memcpy(&timeout,&timeleft,sizeof timeout);

		int ret=select(1,&inset,NULL,NULL,&timeout);
		if(ret<0){
			if(errno!=EINTR){
				endscreen();
				perror("select");
				exit(1);
			} else {
				FD_ZERO(&inset);
			}
		}

		struct timeval end;
		gettimeofday(&end,NULL);

		if(ret==0){
			if(!advance(&bd))break;
			delaytimeout(&bd,&timeleft);
			continue;
		}
		end.tv_sec-=start.tv_sec;
		end.tv_usec-=start.tv_usec;
		if(end.tv_usec<0){
			end.tv_sec--;
			end.tv_usec+=1000000;
		}

		timeleft.tv_sec-=end.tv_sec;
		timeleft.tv_usec-=end.tv_usec;
		if(timeleft.tv_usec<0){
			timeleft.tv_sec--;
			if(timeleft.tv_sec<0){
				timeleft.tv_sec=timeleft.tv_usec=0;
			} else {
				timeleft.tv_usec+=1000000;
			}
		}

		if(FD_ISSET(0,&inset)){
			int key=tgetkey();
			if(key=='Q'||key=='q')break;
			else if(key=='S'||key=='s'||key==KEY_DOWN){
				if(!advance(&bd))break;
				delaytimeout(&bd,&timeleft);
			} else if(key=='A'||key=='a'||key==KEY_LEFT)move(&bd,-1);
			else if(key=='D'||key=='d'||key==KEY_RIGHT)move(&bd,1);
			else if(key=='W'||key=='w'||key==KEY_UP)rotR(&bd);
			else bel();
		}
	}

	endscreen();
	printf("Game Over!\nLines cleared: %d\nLevel reached: %d\n",bd.ncleared,bd.level);
}