library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.ball_game_pkg.all;
use work.nes_controller_pkg.all;

-- seven segment display (ssd)
entity ssd_controller is
	generic (
		-- amount of time in clock cycles the hex display should be on/off 
		-- set this generic to the equivalent of 250 ms
		BLINK_INTERVAL : integer := 125 * 1e5;
		-- number of times the hex display should blink,
		-- a value of one indicates that after the conversion the hex
		-- display shall stay on for BLINK_INTERVAL and then off for
		-- BLINK_INTERVAL, after that it should stay on
		BLINK_COUNT : integer := 3;
		-- specifies how many clock cycles an individual animation frame lasts 
		ANIM_CYCLES : integer := 5e7
	);
	port (
		-- global clock signal
		clk : in std_logic;
		-- global reset signal (low active, not internally synchronized)
		res_n : in std_logic; 

		-- current game state 
		game_state    : in ball_game_state_t;
		-- NES controller input
		controller    : in nes_buttons_t;
		-- current player points
		player_points : in std_logic_vector(15 downto 0);

		-- 1st (right-most) seven segment display (unused)
		hex0 : out std_logic_vector(6 downto 0);
		-- 2nd seven segment display (unused)
		hex1 : out std_logic_vector(6 downto 0);
		-- 3rd seven segment display (unused)
		hex2 : out std_logic_vector(6 downto 0);
		-- 4th seven segment display (unused)
		hex3 : out std_logic_vector(6 downto 0);
		-- 5th seven segment display (current direction)
		hex4 : out std_logic_vector(6 downto 0);
		-- 6th seven segment display (current direction)
		hex5 : out std_logic_vector(6 downto 0);
		-- 7th seven segment display (game state)
		hex6 : out std_logic_vector(6 downto 0);
		-- 8th (left-most) seven segment display (game state)
		hex7 : out std_logic_vector(6 downto 0)
	);
end entity;

architecture arch_ssd of ssd_controller is

	-- the seven segment display is structured as follows, with 6 downto 0 denoting the order (MSB to LSB):
	-- each segment is active-low

	--   +0+     +LSB+
	--   5 1     |   |
	--   +6+ <=> +MSB+
	--   4 2     |   |
	--   +3+     +---+

	--                                                              MSB   LSB
	--                                                              \ /   \ /
	-- give ssd characters names                                     6543210
	constant SSD_DASH             : std_logic_vector(hex0'RANGE) := "0111111";
	constant SSD_CIRCLE_UP        : std_logic_vector(hex0'RANGE) := "0011100";
	constant SSD_CIRCLE_DOWN      : std_logic_vector(hex0'RANGE) := "0100011";
	constant SSD_SQUARE_BRACKET_R : std_logic_vector(hex0'RANGE) := "1110000";
	constant SSD_SQUARE_BRACKET_L : std_logic_vector(hex0'RANGE) := "1000110";
	constant SSD_G                : std_logic_vector(hex0'RANGE) := "1000010";
	constant SSD_O                : std_logic_vector(hex0'RANGE) := "1000000";
	constant SSD_L                : std_logic_vector(hex0'RANGE) := "1000111";
	constant SSD_E                : std_logic_vector(hex0'RANGE) := "0000110";
	constant SSD_R                : std_logic_vector(hex0'RANGE) := "0101111";
	constant SSD_I                : std_logic_vector(hex0'RANGE) := "1101111";
	constant SSD_OFF              : std_logic_vector(hex0'RANGE) := "1111111";
	constant SSD_0                : std_logic_vector(hex0'RANGE) := "1000000";
	constant SSD_1                : std_logic_vector(hex0'RANGE) := "1111001";
	constant SSD_2                : std_logic_vector(hex0'RANGE) := "0100100";
	constant SSD_3                : std_logic_vector(hex0'RANGE) := "0110000";
	constant SSD_4                : std_logic_vector(hex0'RANGE) := "0011001";
	constant SSD_5                : std_logic_vector(hex0'RANGE) := "0010010";
	constant SSD_6                : std_logic_vector(hex0'RANGE) := "0000010";
	constant SSD_7                : std_logic_vector(hex0'RANGE) := "1111000";
	constant SSD_8                : std_logic_vector(hex0'RANGE) := "0000000";
	constant SSD_9                : std_logic_vector(hex0'RANGE) := "0010000";

	type points_state_t is (IDLE, DISPLAY_OVERFLOW, DISPLAY_4, DISPLAY_3, DISPLAY_2, DISPLAY_1, WAIT_NOBLINK, WAIT_INTERVAL, WAIT_COUNT);
	type anim_state_t is (F1, F2, F3, F4);

	-- initial state constants
	constant POINTS_STATE_T_INITIAL : points_state_t := IDLE;
	constant ANIM_STATE_T_INITIAL   : anim_state_t := F1;

	signal points_state      : points_state_t := POINTS_STATE_T_INITIAL;
	signal points_state_next : points_state_t;
	signal anim_state        : anim_state_t := ANIM_STATE_T_INITIAL;
	signal anim_state_next   : anim_state_t;

	-- use something that can be used for counting
	signal upoints           : unsigned (player_points'RANGE);
	signal upoints_next      : unsigned (player_points'RANGE);
	signal upoints_lock      : unsigned (player_points'RANGE);
	signal upoints_lock_next : unsigned (player_points'RANGE);

	signal clk_cnt        : integer range 0 to BLINK_INTERVAL := 0;
	signal clk_cnt_next   : integer range 0 to BLINK_INTERVAL := 0;
	signal blink_cnt      : integer range 0 to BLINK_COUNT := 0;
	signal blink_cnt_next : integer range 0 to BLINK_COUNT := 0;
	signal anim_cnt       : integer range 0 to ANIM_CYCLES := 0;
	signal anim_cnt_next  : integer range 0 to ANIM_CYCLES := 0;

	signal hex3_next : std_logic_vector(hex0'RANGE) := SSD_OFF;
	signal hex2_next : std_logic_vector(hex0'RANGE) := SSD_OFF;
	signal hex1_next : std_logic_vector(hex0'RANGE) := SSD_OFF;
	signal hex0_next : std_logic_vector(hex0'RANGE) := SSD_OFF;

	-- represent counter by storing each digit individually
	-- most significant digit, thousands
	signal digit3           : integer range 0 to 9 := 0;
	signal digit3_next      : integer range 0 to 9 := 0;
	signal digit2           : integer range 0 to 9 := 0;
	signal digit2_next      : integer range 0 to 9 := 0;
	signal digit1           : integer range 0 to 9 := 0;
	signal digit1_next      : integer range 0 to 9 := 0;
	-- least significant digit, ones
	signal digit0           : integer range 0 to 9 := 0;
	signal digit0_next      : integer range 0 to 9 := 0;
	-- flag indicating whether we should blink
	signal blink       : std_logic := '0';
	signal blink_next  : std_logic := '0';

	function get_ssd(digit : integer) return std_logic_vector is
	begin
		case digit is
			when 0 =>
				return SSD_0;
			when 1 =>
				return SSD_1;
			when 2 =>
				return SSD_2;
			when 3 =>
				return SSD_3;
			when 4 =>
				return SSD_4;
			when 5 =>
				return SSD_5;
			when 6 =>
				return SSD_6;
			when 7 =>
				return SSD_7;
			when 8 =>
				return SSD_8;
			when 9 =>
				return SSD_9;
			when others =>
				return SSD_OFF;
		end case;
	end function;

begin
	-- processes game_state enum data hex{6-7}
	game_fsm : process(game_state, anim_state, anim_cnt)
	begin
		anim_state_next <= anim_state;
		anim_cnt_next <= anim_cnt;

		fsm : case game_state is
			when IDLE =>
				hex7 <= SSD_DASH;
				hex6 <= SSD_DASH;
			when RUNNING =>
				hex7 <= SSD_CIRCLE_UP;
				hex6 <= SSD_CIRCLE_DOWN;

				animation_fsm : case anim_state is
					when F1 =>
						hex7 <= SSD_CIRCLE_UP;
						hex6 <= SSD_CIRCLE_DOWN;

						if anim_cnt = ANIM_CYCLES then
							anim_state_next <= F2;
							anim_cnt_next <= 0;
						else
							anim_cnt_next <= anim_cnt + 1;
						end if;

					when F2 =>
						hex7 <= SSD_CIRCLE_UP;
						hex6 <= SSD_CIRCLE_UP;

						if anim_cnt = ANIM_CYCLES then
							anim_state_next <= F3;
							anim_cnt_next <= 0;
						else
							anim_cnt_next <= anim_cnt + 1;
						end if;

					when F3 =>
						hex7 <= SSD_CIRCLE_DOWN;
						hex6 <= SSD_CIRCLE_UP;

						if anim_cnt = ANIM_CYCLES then
							anim_state_next <= F4;
							anim_cnt_next <= 0;
						else
							anim_cnt_next <= anim_cnt + 1;
						end if;

					when F4 =>
						hex7 <= SSD_CIRCLE_DOWN;
						hex6 <= SSD_CIRCLE_DOWN;

						if anim_cnt = ANIM_CYCLES then
							anim_state_next <= F1;
							anim_cnt_next <= 0;
						else
							anim_cnt_next <= anim_cnt + 1;
						end if;

					-- should be unreachable
					when others =>
						null;
				end case;
			when PAUSED =>
				hex7 <= SSD_SQUARE_BRACKET_R;
				hex6 <= SSD_SQUARE_BRACKET_L;
			when GAME_OVER =>
				hex7 <= SSD_G;
				hex6 <= SSD_O;
			-- should not be possible, except if game_state definition changes
			when others =>
				-- report statement needs to be commented for modelsim
				--report "ssd_controller expected game_state_t { IDLE, RUNNING, PAUSED, GAME_OVER } but definition seems to have changed" severity warning;
				hex7 <= SSD_OFF;
				hex6 <= SSD_OFF;
		end case;
	end process;

	-- processes nes button data hex{4-5}
	input : process(controller)
	begin
		-- default values
		hex5 <= SSD_OFF;
		hex4 <= SSD_OFF;

		if (controller.btn_right = '1' and controller.btn_left = '1') then
			hex5 <= SSD_E;
			hex4 <= SSD_R;
		elsif (controller.btn_right = '1') then
			hex5 <= SSD_R;
			hex4 <= SSD_I;
		elsif (controller.btn_left = '1') then
			hex5 <= SSD_L;
			hex4 <= SSD_E;
		end if;
	end process;

	-- synchronous process for the 3 process method
	-- only for sync signals hex{0-3}
	points_sync : process(clk, res_n)
	begin
		if res_n = '0' then
			points_state <= IDLE;
			anim_state <= F1;
			upoints <= to_unsigned(0, 16);
			upoints_lock <= to_unsigned(0, 16);
			blink <= '0';

			clk_cnt <= 0;
			blink_cnt <= 0;
			anim_cnt <= 0;

			hex3 <= SSD_OFF;
			hex2 <= SSD_OFF;
			hex1 <= SSD_OFF;
			hex0 <= SSD_OFF;

			digit3 <= 0;
			digit2 <= 0;
			digit1 <= 0;
			digit0 <= 0;
		elsif rising_edge(clk) then
			points_state <= points_state_next;
			anim_state <= anim_state_next;
			upoints <= upoints_next;
			upoints_lock <= upoints_lock_next;
			blink <= blink_next;

			clk_cnt <= clk_cnt_next;
			blink_cnt <= blink_cnt_next;
			anim_cnt <= anim_cnt_next;

			hex3 <= hex3_next;
			hex2 <= hex2_next;
			hex1 <= hex1_next;
			hex0 <= hex0_next;

			digit3 <= digit3_next;
			digit2 <= digit2_next;
			digit1 <= digit1_next;
			digit0 <= digit0_next;
		end if;
	end process;

	points_next_logic : process(player_points, points_state, clk_cnt, blink_cnt, upoints, upoints_lock, blink)
	begin
		-- defaults
		points_state_next <= points_state;

		fsm : case points_state is
			when IDLE =>
				-- detect change in points
				if not(unsigned(player_points) = upoints_lock) and unsigned(player_points) >= to_unsigned(1e4, 16) then
					points_state_next <= DISPLAY_OVERFLOW;
				elsif not(unsigned(player_points) = upoints_lock) then
					points_state_next <= DISPLAY_4;
				end if;

			when DISPLAY_OVERFLOW =>
				-- on overflow there is no blinking
				-- go straight to IDLE
				points_state_next <= IDLE;

			when DISPLAY_4 =>
				if upoints < to_unsigned(1e3, 16) then
					points_state_next <= DISPLAY_3;
				end if;

			when DISPLAY_3 =>
				if upoints < to_unsigned(1e2, 16) then
					points_state_next <= DISPLAY_2;
				end if;

			when DISPLAY_2 =>
				if upoints < to_unsigned(1e1, 16) then
					points_state_next <= DISPLAY_1;
				end if;

			when DISPLAY_1 =>
				if upoints < to_unsigned(1, 16) and blink = '1' then
					points_state_next <= WAIT_INTERVAL;
				elsif upoints < to_unsigned(1, 16) then
					points_state_next <= WAIT_NOBLINK;
				end if;

			when WAIT_NOBLINK =>
				points_state_next <= IDLE;

			when WAIT_INTERVAL =>
				if not(blink_cnt = BLINK_COUNT) and clk_cnt = BLINK_INTERVAL then
					points_state_next <= WAIT_COUNT;
				elsif blink_cnt = BLINK_COUNT and clk_cnt = BLINK_INTERVAL then
					points_state_next <= IDLE;
				end if;

			when WAIT_COUNT =>
				if clk_cnt = BLINK_INTERVAL then
					points_state_next <= WAIT_INTERVAL;
				end if;

			-- should be unreachable
			when others =>
				null;
		end case;
	end process;

	points_out_logic : process(player_points, points_state, blink, digit3, digit2, digit1, digit0, upoints, upoints_lock, hex3, hex2, hex1, hex0, clk_cnt, blink_cnt)
	begin
		-- defaults
		blink_next <= blink;
		digit3_next <= digit3;
		digit2_next <= digit2;
		digit1_next <= digit1;
		digit0_next <= digit0;
		hex3_next <= hex3;
		hex2_next <= hex2;
		hex1_next <= hex1;
		hex0_next <= hex0;
		upoints_next <= unsigned(player_points);
		upoints_lock_next <= unsigned(player_points);
		clk_cnt_next <= clk_cnt;
		blink_cnt_next <= blink_cnt;

		fsm : case points_state is
			when IDLE =>
				-- reset counters
				clk_cnt_next <= 0;
				blink_cnt_next <= 0;
				digit3_next <= 0;
				digit2_next <= 0;
				digit1_next <= 0;
				digit0_next <= 0;

				if unsigned(player_points) >= upoints_lock + to_unsigned(25, 16) then
					blink_next <= '1';
				else
					blink_next <= '0';
				end if;

			when DISPLAY_OVERFLOW =>
				hex3_next <= SSD_DASH;
				hex2_next <= SSD_DASH;
				hex1_next <= SSD_DASH;
				hex0_next <= SSD_DASH;

			when DISPLAY_4 =>
				if upoints >= to_unsigned(1e3, 16) then
					upoints_next <= upoints - to_unsigned(1e3, 16);
					digit3_next <= digit3 + 1;
				else
					upoints_next <= upoints;
				end if;

			when DISPLAY_3 =>
				if upoints >= to_unsigned(1e2, 16) then
					upoints_next <= upoints - to_unsigned(1e2, 16);
					digit2_next <= digit2 + 1;
				else
					upoints_next <= upoints;
				end if;

			when DISPLAY_2 =>
				if upoints >= to_unsigned(1e1, 16) then
					upoints_next <= upoints - to_unsigned(1e1, 16);
					digit1_next <= digit1 + 1;
				else
					upoints_next <= upoints;
				end if;

			when DISPLAY_1 =>
				if upoints >= to_unsigned(1, 16) then
					upoints_next <= upoints - to_unsigned(1, 16);
					digit0_next <= digit0 + 1;
				else
					upoints_next <= upoints;
				end if;

			when WAIT_NOBLINK =>
				hex3_next <= get_ssd(digit3);
				hex2_next <= get_ssd(digit2);
				hex1_next <= get_ssd(digit1);
				hex0_next <= get_ssd(digit0);

			when WAIT_INTERVAL =>
				-- disallow upoints to change during blinking
				upoints_lock_next <= upoints_lock;

				hex3_next <= get_ssd(digit3);
				hex2_next <= get_ssd(digit2);
				hex1_next <= get_ssd(digit1);
				hex0_next <= get_ssd(digit0);
				--report "in wait interval next repr for '" & to_string(digit3) & to_string(digit2) & to_string(digit1) & to_string(digit0) & "' is '" & to_string(get_ssd(digit3)) & to_string(get_ssd(digit2)) & to_string(get_ssd(digit1)) & to_string(get_ssd(digit0)) & "'";

				if clk_cnt = BLINK_INTERVAL then
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			when WAIT_COUNT =>
				-- disallow upoints to change during blinking
				upoints_lock_next <= upoints_lock;

				hex3_next <= SSD_OFF;
				hex2_next <= SSD_OFF;
				hex1_next <= SSD_OFF;
				hex0_next <= SSD_OFF;

				if clk_cnt = BLINK_INTERVAL and not(blink_cnt = BLINK_COUNT) then
					blink_cnt_next <= blink_cnt + 1;
					-- reset for next run
					clk_cnt_next <= 0;
				elsif clk_cnt = BLINK_INTERVAL then
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			-- should be unreachable
			when others =>
				null;
		end case;
	end process;

end architecture;
