--
-- Copyright (C) 2021  <fastrgv@gmail.com>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You may read the full text of the GNU General Public License
-- at <http://www.gnu.org/licenses/>.
--

-------------------------------
-- Coterminal Space Invaders
-------------------------------
-- files:
--------------------
-- cinv.adb
-- aliens.ad?
-- globals.ad?
-- player.ad?
-- ufo.ad?
--------------------


with tput00_h;

with ada.directories;
with System;
with Interfaces.C;
use  type interfaces.c.unsigned;
with interfaces.c.strings;

with Ada.Command_Line;
with Ada.Strings.Unbounded;
with Ada.Strings.Unbounded.Text_IO;

with ada.numerics.generic_elementary_functions;

with text_io;

with ada.calendar;

with ada.strings.fixed;

with sysutils;

with GNATCOLL.Terminal;  use GNATCOLL.Terminal;

with Interfaces.C; use Interfaces.C;

with snd4ada;

with realtime;

with gnat.os_lib;

-----------------------
with player;
with aliens;
with ufo;
with globals;
-----------------------



procedure cinv is


	use Ada.Strings.Unbounded;
	use Ada.Strings.Unbounded.Text_IO;
	use text_io;
	use interfaces.c;
	use interfaces.c.strings;

	use ada.calendar;

	mswin: constant boolean := (gnat.os_lib.directory_separator='\');


	package fmath is new
			Ada.Numerics.generic_elementary_functions( float );
	use fmath;

	package myint_io is new text_io.integer_io(integer);




	pending: boolean := false;

	rtime  : interfaces.c.int;

------------------------------------------------------------------------

game_loop: constant integer := 1;
game_nextlevel: constant integer := 2;
game_paused: constant integer := 3;
game_over: constant integer := 4;
game_exit: constant integer := 5;

aliens_move_counter,
aliens_shot_counter,
player_shot_counter,
ufo_move_counter,
status
	: integer;


procedure initlevel is
	--ich: character;
begin

--put_line("initlevel0...hit any key to proceed");
--get_immediate(ich); --Ok

	player.playerreset;

	aliens.aliensreset;

	--ufo.uforeset;

	aliens.bunkersreset;

	aliens.analyze;

end initlevel;

procedure evaluatecommandline is
	nargs: constant integer := ada.command_line.argument_count;
begin
	if nargs=2 then -- assume 1st is -l
		declare
			levstr: string := ada.command_line.argument(2);
			lst: natural;
		begin
			myint_io.get(levstr, globals.skill_level, lst);
		end; --declare
	elsif nargs /= 0 then
		put("Zero or 2 commandline arguments expected");
		raise program_error;
	end if;

end evaluatecommandline;

procedure finish is
	scor: constant integer := globals.score;
begin

	--show final score
	put("Final score: "); myint_io.put(scor,9); new_line;

	put(", Final level: "); myint_io.put(globals.level,4); new_line;

	put("Final rating... ");
	if globals.lives>0 then put_line("Quitter");
	elsif scor<5000 then put_line("Alien Fodder");
	elsif scor<7500 then put_line("Easy Target");
	elsif scor<10000 then put_line("Barely Mediocre");
	elsif scor<12500 then put_line("Shows Promise");
	elsif scor<15000 then put_line("Alien Blaster");
	elsif scor<20000 then put_line("Earth Defender");
	elsif scor<19999 then put_line("Supreme Protector");
	end if;

end finish;






function isvalid(ch: character) return boolean is
	ok: boolean := false;
begin
	case ch is
		when ' ' => ok:=true; --fire missile
		when 'M'|'K' => ok:=true; --mswin
		when 'C'|'D' => ok:=true; --linux/osx
		when 'd'|'a' => ok:=true;
		when 'l'|'j' => ok:=true;
		when 'q'|'p' => ok:=true;
		when others => 
			--allow <enter>:
			if character'pos(ch)=13 or character'pos(ch)=10 then
				ok:=true;
			else
				ok:=false;
			end if;

	end case;
	return ok;
end;


procedure handle_key_down( ch: character; digest: out boolean ) is
begin

	digest:=false;

-- note that arrow keys typically produce cap letter
-- chars, preceded by 1 or 2 non-printable chars.
--
-- on Linux:		<home>='H'	<end>='F'
--   A		
-- D B C
--
-- or on MSWin:	<home>='G'	<end>='O'
--   H
-- K P M


	case ch is

		when ' ' => -- shoot
			player.playerLaunchMissile;
			digest:=true;

		when 'K' | 'D' | 'a' | 'j' =>	--lf

			player.playermoveleft;
			digest:=true;

		when 'M' | 'C' | 'd' | 'l' =>	--rt

			player.playermoveright;
			digest:=true;

		when 'q' =>	status:=game_exit; digest:=true;
	
		when 'p' => 
			-- toggle
			if status=game_paused then status:=game_loop;
			elsif status/=game_paused then status:=game_paused;
			end if;
			digest:=true;

		when others => null;

	end case;


end handle_key_down;



	nextime : Time;
function readinput return character is
	avail, digested, pending: boolean := false;
	ich, qch : character;
	ret: integer;

	aliensReachedPlayer,
	noAliensRemain,
	playerWasHit : boolean;

begin

----------------------- grab keystroke -----------------------------

		avail:=false; get_immediate(ich, avail);
		if avail and then isvalid(ich) then --enqueue new user command
			qch:=ich;
			pending:=true; -- => 1 or more is in the queue
		end if;
		if pending then --deal with most recent user command
			handle_key_down(qch,digested);
			pending := not digested;  --feed again, if need be
		end if;

--------------- regulate timing
	nextime:=nextime+globals.tick;
	delay until nextime; --update timer

----------------------- grab keystroke -----------------------------

		avail:=false; get_immediate(ich, avail);
		if avail and then isvalid(ich) then --enqueue new user command
			qch:=ich;
			pending:=true;
		end if;
		if pending then --deal with most recent user command
			handle_key_down(qch,digested);
			pending := not digested;  --feed again, if need be
		end if;

---------------------------------------------------------------------
-- done with input & timer
---------------------------------------------------------------------


	if status=game_paused and then qch='p' then
		status:=game_loop;

	elsif status = game_over then
		null;

	else --default
		null; --handled elsewhere
	end if;



---------------------------------------------------------------------
	--"handleTimer":
---------------------------------------------------------------------

--qch:=' ';
--put_line("inside readinput...hit any key to proceed");
--get_immediate(ich); --Ok

	if status=game_nextlevel then

		globals.level:=globals.level+1;
		initlevel;

		aliens_move_counter:=0;
		aliens_shot_counter:=0;
		player_shot_counter:=0;
		ufo_move_counter:=0;

		--smaller(faster) @ higher levels
		globals.weite:=globals.weiteHi - globals.level*globals.deltaWeite; 

		status:=game_loop;

	elsif status=game_loop then -- do game handling

		if aliens_move_counter=0 then

			--move aliens
			ret := aliens.aliensmove; -- 1 => reached player
			aliensReachedPlayer := (ret=1);

			if aliensReachedPlayer then
				globals.lives:=0;
				status:=game_over;
			end if;

		end if;


		if player_shot_counter=0 then
			--move player missile
			ret:=player.playerMoveMissile; -- 1 => no aliens remain
			noAliensRemain := (ret=1);
			if noAliensRemain then
				status:=game_nextlevel;
			end if;
		end if;



		if aliens_shot_counter=0 then
			--move alien missiles
			ret:=aliens.aliensMissileMove; -- 1 => player was hit
			playerWasHit := (ret=1);
			if playerWasHit then
				globals.lives := globals.lives-1; --player loses one life
				--playerExplode; -- graphics
				snd4ada.playSnd(globals.death);
				if globals.lives=0 then 
					status:=game_over; 
				end if;
			end if;
		end if;


		--if ufo_move_counter=0 and ufoshowufo=1 then
		--	ufomoveleft;
		--end if;

		aliens_shot_counter := aliens_shot_counter+1;
		if aliens_shot_counter>=globals.missileMoveTics then 
			aliens_shot_counter:=0; 
		end if;

		player_shot_counter := player_shot_counter+1;
		if player_shot_counter>=globals.missileMoveTics then 
			player_shot_counter:=0; 
		end if;
		--if player_shot_counter>=1 then player_shot_counter:=0; end if;
		-- comment: I believe shot_counter controls the animation
		-- 			speed of the missiles, so all should be same.

		aliens_move_counter := aliens_move_counter+1;
		if aliens_move_counter>=globals.weite then 
			aliens_move_counter:=0; 
		end if;

		ufo_move_counter := ufo_move_counter+1;
		if ufo_move_counter>=3 then ufo_move_counter:=0; end if;
		

	end if;


	return qch;

end readinput;



procedure initsounds( path: string ) is
begin

	snd4ada.initSnds;


	globals.gameover:= --player lost a life
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"gameover.wav"));
	if globals.gameover<0 then
		put_line("snd4ada.initSnd ERROR gameover");
		raise program_error;
	end if;


	globals.death:= --player lost a life
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"eatghost.wav"));
	if globals.death<0 then
		put_line("snd4ada.initSnd ERROR death");
		raise program_error;
	end if;


	globals.myshoot:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"laser-gun.wav"));
	if globals.myshoot<0 then
		put_line("snd4ada.initSnd ERROR shoot");
		raise program_error;
	end if;


	globals.mykill:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"invaderKilled3.wav"));
	if globals.mykill<0 then
		put_line("snd4ada.initSnd ERROR kill");
		raise program_error;
	end if;

	globals.myUfoKill:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"ufoKilled.wav"));
	if globals.myUfoKill<0 then
		put_line("snd4ada.initSnd ERROR ufoKill");
		raise program_error;
	end if;

	globals.vad1:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"fastinvader1.wav"));
	if globals.vad1<0 then
		put_line("snd4ada.initSnd ERROR vad1");
		raise program_error;
	end if;

	globals.vad2:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"fastinvader2.wav"));
	if globals.vad2<0 then
		put_line("snd4ada.initSnd ERROR vad2");
		raise program_error;
	end if;

	globals.vad3:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"fastinvader3.wav"));
	if globals.vad3<0 then
		put_line("snd4ada.initSnd ERROR vad3");
		raise program_error;
	end if;

	globals.vad4:=
		snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"fastinvader4.wav"));
	if globals.vad4<0 then
		put_line("snd4ada.initSnd ERROR vad4");
		raise program_error;
	end if;


	--globals.intro:=snd4ada.initSnd(Interfaces.C.Strings.New_String(path&"intro.wav"));
	--if globals.intro<0 then
	--	put_line("snd4ada.initSnd ERROR intro");
	--	raise program_error;
	--end if;



end initsounds;



procedure drawintro is
	Ok: boolean;
	tab: constant string := "                         ";
	tab0: constant string := "                  ";
	info: terminal_info;
begin

	info.init_for_stdout(auto);
	info.set_bg(black);


	if mswin then
		SysUtils.bShell("cls", Ok); -- erase-terminal
	else
		SysUtils.bShell("clear", Ok); -- erase-terminal
	end if;


	info.set_fg(cyan);
	new_line;
	put(tab);
	put("        P L A Y"); new_line;
	new_line;
	put(tab);
	info.set_fg(yellow);
	put("     Space Invaders"); new_line;
	new_line;
	put(tab);
	info.set_fg(magenta);
	put(" *score advance table*"); new_line;
	put(tab);

	info.set_fg(red);
	put("   /@\ = 30 points"); new_line;
	put(tab);

	info.set_fg(green);
	put("   /O\ = 20 points"); new_line;
	put(tab);

	info.set_fg(blue);
	put("   /o\ = 10 points"); new_line;
	new_line;

	info.set_fg(yellow);
	put(tab0);
	put("      extra life @ 6_000 points"); new_line;
	new_line;

	info.set_fg(cyan);
	put(tab0);
	put(" hit <space> to begin; <q> to quit"); new_line;

   Info.Set_Color (Standard_Output, Style => Reset_All);

end drawintro;



function max( i,j: integer ) return integer is
begin
	if i>j then return i;
	else return j; end if;
end;

-- "draw" screen from top to bottom, & left to right.
procedure draw is
use globals;

	info: terminal_info;

	Ok: boolean;

	off, nl, r0,r1,rlast: integer := 0; -- nl:#lines

	-- screen array simplifies draw logic:
	-- note that layout matches aliens.bunker, etc:
	subtype yrng is integer range 0..screenheight-1; --0..23
	subtype xrng is integer range 0..screenwidth-1; --0..79
	screen: array(yrng,xrng) of character 
		:= (others=>(others=>' '));

	subtype str80 is string(1..80);
	linestr: str80;

	ppx: constant integer := xrng(player.player.posX);
	ppy: constant integer := yrng(playerposy); -- 22

	apx: constant integer := aliens.aliens.posX;
	apy: constant integer := aliens.aliens.posY; --top 0..19

	-- colors by row:
	--         0
	-- screen( apy, x ) : red (high aliens)
	-- blank line
	-- screen( apy+2, x ) : green (medium aliens)
	-- blank line
	-- screen( apy+4, x ) : green (medium aliens)
	-- blank line

	-- screen( apy+6, x ) : blue (low aliens)
	-- blank line
	-- screen( apy+8, x ) : blue (low aliens)
	-- blank line
	--
	-- screen( apy+10 .. 15, x ) : grey (scattered missiles)
	--
	-- screen(16..19,x) : magenta (bunkers+missiles)

	-- screen(20..21,x) : grey (stattered missiles)
	-- screen(22,x) : green (player)
	-- screen(23,x) : blank line
	--

	rr,cc: integer;

begin --draw

--put_line("draw 0");
--get_immediate(ich);

	-- first, define screen array

	-- fixed bunkers (magenta) [bunkery=16..19]
	for r in 0..3 loop
	for c in xrng loop
		if aliens.bunker(r,c)=1 then -- 1 => fragment still intact
			screen(bunkery+r,c) := '#';
		end if;
	end loop;
	end loop;


	-- alienBlock (0..aliens_max_number_y-1, 0..aliens_max_number_x-1)
	-- = type 1(hi), 2(med), 3(lo), or 0(destroyed)

	-- hi aliens(red)
	for r in apy..apy loop
		for c in 0..aliens_max_number_x-1 loop
		off:=apx+c*3;
		if aliens.alienBlock(0,c) /= 0 then -- 0 => alien obliterated
			screen(r,off+0):='/';
			screen(r,off+1):='@';
			screen(r,off+2):='\';
		end if;
		end loop;
	end loop;
	-- line apy+1 is blank


	-- medium aliens(green)
	for r in apy+2..apy+2 loop 
		for c in 0..aliens_max_number_x-1 loop
		off:=apx+c*3;
		if aliens.alienBlock(1,c) /= 0 then --(0..4,0..9)
			screen(r,off+0):='/';
			screen(r,off+1):='O';
			screen(r,off+2):='\';
		end if;
		end loop;
	end loop;
	-- line apy+3 is blank

	for r in apy+4..apy+4 loop 
		for c in 0..aliens_max_number_x-1 loop
		off:=apx+c*3;
		if aliens.alienBlock(2,c) /= 0 then --(0..4,0..9)
			screen(r,off+0):='/';
			screen(r,off+1):='O';
			screen(r,off+2):='\';
		end if;
		end loop;
	end loop;
	-- line apy+5 is blank





	-- low aliens(blue)

	for r in apy+6..apy+6 loop 
		for c in 0..aliens_max_number_x-1 loop
		off:=apx+c*3;
		if aliens.alienBlock(3,c) /= 0 then
			screen(r,off+0):='/';
			screen(r,off+1):='o';
			screen(r,off+2):='\';
		end if;
		end loop;
	end loop;
	-- line apy+7 is blank

	for r in apy+8..apy+8 loop 
		for c in 0..aliens_max_number_x-1 loop
		off:=apx+c*3;
		if aliens.alienBlock(4,c) /= 0 then
			screen(r,off+0):='/';
			screen(r,off+1):='o';
			screen(r,off+2):='\';
		end if;
		end loop;
	end loop;
	-- line apy+9 is blank






	-- player(green) [ ppy=22 ]
	screen(ppy,ppx+0):='/';
	screen(ppy,ppx+1):='-';
	screen(ppy,ppx+2):='^';
	screen(ppy,ppx+3):='-';
	screen(ppy,ppx+4):='\';




	-- alien missiles...
	-- alienshotX,Y : (0..aliens_max_missiles-1) of integer (0..9); shotX=0 => NotActive
	for i in 0..aliens.aliens_max_missiles-1 loop
		cc := aliens.alienshotX(i);
		if cc /= 0 then --active
			rr := aliens.alienshotY(i);
			screen(rr,cc):=':';
		end if;
	end loop;


	-- player missile
	if player.player.missileFired=1 then
		screen(player.player.missileY,player.player.missileX):='!';


	end if;



------------- done populating screen array ------------------------------


	info.init_for_stdout(auto);
	info.set_bg(black);

	if mswin then
		tput00_h.cursorHome; -- put cursor @ UL

	else -- linux or osx

		SysUtils.bShell("clear", Ok);

	end if;


	-- According to gnatcoll-terminal.ads
	-- these are all the available colors:
	-- magenta, yellow, red, grey, 
	-- blue, black, green, cyan


	-- we must draw all lines since there may be
	-- missiles to display anywhere above player
	-- so begin at line zero:

	--small aliens
	Info.Set_fg(red);
	for r in 0..apy+1 loop
	--myassert( r in yrng, 101 );
		for c in xrng loop
			linestr(c+1):=screen(r,c);
		end loop;
		put(linestr);
		new_line;
	end loop;
	rlast:=apy+1;


	--medium aliens
	Info.Set_fg(green);
	for r in apy+2..apy+5 loop
	--myassert( r in yrng, 102 );
		for c in xrng loop
			linestr(c+1):=screen(r,c);
		end loop;
		put(linestr);
		new_line;
	end loop;
	rlast:=apy+5;


	--large aliens
	Info.Set_fg(blue);
	for r in apy+6..apy+8 loop
	--myassert( r in yrng, 103 );
		for c in xrng loop
			linestr(c+1):=screen(r,c);
		end loop;
		put(linestr);
		new_line;
	end loop;
	rlast:=apy+8;
-- rlast==apy+8 is the last, lowest line of aliens to be drawn, so far.


--------- done with aliens --------------------------------------

	-- we must draw all lines since there may be
	-- missiles to display anywhere above player


	-- shrinking no mans land:
	r1 := bunkerY-1;
	if rlast+1 <= r1 then

		Info.Set_fg(grey);
		for r in rlast+1..r1 loop
			for c in xrng loop
				linestr(c+1):=screen(r,c);
			end loop;
			put(linestr);
			new_line;
		end loop;
		rlast:=r1;

	end if; -- drawn all rows up to row=bunkerY-1

	-- 4 [shrinking] bunkers:
	r1 := bunkerY+bunkerHeight-1;
	if rlast+1 <= r1 then

		Info.Set_fg(magenta); -- bunkerY==16
		for r in rlast+1..r1 loop -- normally 16..19
			for c in xrng loop
				linestr(c+1):=screen(r,c);
			end loop;
			put(linestr);
			new_line;
		end loop;
		rlast:=r1;

	end if; -- drawn all rows up to max(apy+9, bunkerY+bunkerHeight-1)

	-- draw all lines ...

	Info.Set_fg(green); -- player
	for r in rlast+1..yrng'last loop
		for c in xrng loop
			linestr(c+1):=screen(r,c);
		end loop;
		put(linestr);
		new_line;
	end loop;


---------- done drawing animated game board proper ---------------------


	Info.Set_Fg(red);

	-- status lines:
	put(" Level: "&integer'image(globals.level));
	put("       Lives: "&integer'image(globals.lives));
	put_line("      Score: "&integer'image(globals.score)); -- line 23
	put_line("         Terminal Space Invaders           q=quit"); -- line 24

	--debug output here:
	put_line(" apy: "&integer'image(apy));

   Info.Set_Color (Standard_Output, Style => Reset_All);

end draw;







	path0 : constant string(1..7)  := "sounds/";
	path1 : constant string(1..13) := "../../sounds/";

	ich: character;
	Ok: boolean;

	refresh: integer := globals.skip;

begin --cinv

	if mswin then
		rtime:=realtime.hiPriority;
		-- note:  this seems necessary because some, 
		-- but not all, windows commandline terminals 
		-- seem to randomly freeze at normal priority.
	else
		rtime:=1;
	end if;


	if ada.directories.Exists(path0) then
		initsounds(path0);
	elsif ada.directories.Exists(path1) then
		initsounds(path1);
	else
		raise program_error;
	end if;



	--smaller(faster) @ higher levels
	globals.weite:=globals.weiteHi - globals.level*globals.deltaWeite; 

globals.myassert( globals.weite mod globals.skip = 0, 999 );
globals.myassert( globals.missileMoveTics mod globals.skip = 0, 998 );

	globals.score:=0; 
	globals.lives:=3; 
	globals.level:=0; 
	globals.skill_level:=5; --0=nightmare..9=beginner

	evaluatecommandline;
	status:=game_nextlevel;



	--snd4ada.playSnd(intro);
	drawintro;

	get_immediate(ich);
	if ich='q' then
		status:=game_exit;
	end if;


	nextime := clock;

	if mswin then
		SysUtils.bShell("cls", Ok); -- erase-terminal
	else
		SysUtils.bShell("clear", Ok); -- erase-terminal
	end if;



	pending:=false;
	while status /= game_exit and status /= game_over loop

		ich:=readinput; --called every time-tick
		-- gets user input,
		-- increments timer,
		-- updates status, 
		-- make all moves, 
		-- checks 4 collisions.


		refresh:=refresh-1; -- 2,1,0
		if refresh=0 then
			draw;
			refresh:=globals.skip; --3
		end if;

	end loop;

	if mswin then
		SysUtils.bShell("cls", Ok); -- erase-terminal
	else
		SysUtils.bShell("clear", Ok); -- erase-terminal
	end if;

	finish;
	snd4ada.playSnd(globals.gameover);
	delay 6.0; --for gameover music
	--delay 2.0; -- to read final score

	if mswin then
		SysUtils.bShell("cls", Ok); -- erase-terminal
	else
		SysUtils.bShell("clear", Ok); -- erase-terminal
	end if;


	if mswin then
		if rtime=0 then
			put_line("RealTime achieved");
		else
			put_line("RealTime NOT achieved");
		end if;
	end if;

	snd4ada.termSnds;


	-- debug output here:
--	new_line;
--	put_line("======= debug output ==========");
--	put("shipnum:"); --50
--	put(integer'image(globals.shipnum)); new_line;


end cinv;

