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

use work.math_pkg.all;
use work.gfx_util_pkg.all;
use work.gfx_if_pkg.all;
use work.ram_pkg.all;

entity rasterizer is
	generic (
		-- how many bits downto 0 are stored for the color of a pixel
		COLOR_WIDTH : integer := 2;
		-- gfx bounding box
		WIDTH  : integer := 400;
		HEIGHT : integer := 240;
		--  FIFO element count
		FIFO_DATA_DEPTH  : integer := 8;
		FIFO_INSTR_DEPTH : integer := 8;
		-- FIFO elementh width
		FIFO_DATA_WIDTH  : integer := 16;
		FIFO_INSTR_WIDTH : integer := 8;
		-- DP RAM 
		-- double buffering requires a factor 2
		VRAM_ADDR_WIDTH : integer := log2c(2 * 400 * 240);
		VRAM_DATA_WIDTH : integer := 2
	);
	port (
		clk : in std_logic;
		res_n : in std_logic;

		gfx_instr      : in std_logic_vector(FIFO_INSTR_WIDTH - 1 downto 0);
		gfx_instr_wr   : in std_logic;
		gfx_instr_full : out std_logic;
		gfx_data       : in std_logic_vector(FIFO_DATA_WIDTH - 1 downto 0);
		gfx_data_wr    : in std_logic;
		gfx_data_full  : out std_logic;
		gfx_frame_sync : out std_logic;

		-- frame synchronization signal
		frame_start    : in std_logic;
		vram_base_addr : out std_logic_vector(VRAM_ADDR_WIDTH - 1 downto 0);

		-- exposed dp ram storage
		-- (only write buffers! read buffers managed by frame_reader)
		vram_wr   : out std_logic;
		vram_addr : out std_logic_vector(VRAM_ADDR_WIDTH - 1 downto 0);
		vram_data : out std_logic_vector(VRAM_DATA_WIDTH - 1 downto 0)
	);
end entity;

architecture rasterizer_arch of rasterizer is

	-- log2c(WIDTH|HEIGHT) - 1 for range calculation, used in gfx_line,
	-- gfx_circle and gfx_rect
	subtype pixel_x_range_t  is Natural range log2c(WIDTH) - 1 downto 0;
	subtype pixel_y_range_t  is Natural range log2c(HEIGHT) - 1 downto 0;
	subtype gfx_data_range_t is Natural range FIFO_DATA_WIDTH - 1 downto 0;

	-- internal fifo signals
	-- instr
	signal gfx_instr_rd_data   : std_logic_vector(FIFO_INSTR_WIDTH - 1 downto 0);
	signal gfx_instr_rd        : std_logic;
	signal gfx_instr_empty     : std_logic;
	-- data
	signal gfx_data_rd_data    : std_logic_vector(FIFO_DATA_WIDTH - 1 downto 0);
	signal gfx_data_rd         : std_logic;
	signal gfx_data_empty      : std_logic;

	-- gfx_util signal definitions
	type control_signals_t is record
		start : std_logic;
		busy  : std_logic;
	end record;

	type drawing_parameters_t is record
		-- individual operands, can have different meanings:
		-- x0 for line, x_center for circle, x for rect
		op1 : std_logic_vector(gfx_data_range_t);
		-- y0 for line, y_center for circle, y for rect
		op2 : std_logic_vector(gfx_data_range_t);
		-- x1 for line, radius for circle, w for rect
		op3 : std_logic_vector(gfx_data_range_t);
		-- y1 for line, nothing for circle, h for rect
		op4 : std_logic_vector(gfx_data_range_t);
	end record;

	type pattern_t is record
		bw, bh, dx, dy : std_logic_vector(3 downto 0);
		ls             : std_logic_vector(4 downto 0);
	end record;

	type pattern_array_t is array(7 downto 0) of pattern_t;

	type extra_drawing_parameters_t is record
		-- parameters additionally needed for rects
		pattern : pattern_t;
		fill    : std_logic;
		draw    : std_logic;
	end record;

	type pixel_coordinates_output_t is record
		valid : std_logic;
		x     : std_logic_vector(pixel_x_range_t);
		y     : std_logic_vector(pixel_y_range_t);
	end record;

	constant INITIAL_PARAMETERS : drawing_parameters_t := (
		op1 => (others => '0'),
		op2 => (others => '0'),
		op3 => (others => '0'),
		op4 => (others => '0')
	);

	-- gfx_util signals
	signal control_line     : control_signals_t;
	signal control_circle   : control_signals_t;
	signal control_rect     : control_signals_t;
	signal extra_parameters : extra_drawing_parameters_t;
	signal pixel_line       : pixel_coordinates_output_t;
	signal pixel_circle     : pixel_coordinates_output_t;
	signal pixel_rect       : pixel_coordinates_output_t;
	signal pixel_rect_color : std_logic;

	-- predefined patterns
	-- all secondary color
	constant PATTERN_0     : pattern_t := (dx => "1111", dy => "1111", bw => "0000", bh => "0000", ls => "00000");
	-- some in between
	constant PATTERN_1     : pattern_t := (dx => "0100", dy => "0000", bw => "0100", bh => "0100", ls => "00010");
	constant PATTERN_2     : pattern_t := (dx => "0100", dy => "0000", bw => "0100", bh => "0100", ls => "00100");
	constant PATTERN_3     : pattern_t := (dx => "0010", dy => "0000", bw => "0010", bh => "0010", ls => "00000");
	constant PATTERN_4     : pattern_t := (dx => "0000", dy => "0010", bw => "0010", bh => "0010", ls => "00000");
	constant PATTERN_5     : pattern_t := (dx => "1001", dy => "0110", bw => "1100", bh => "0011", ls => "00000");
	constant PATTERN_6     : pattern_t := (dx => "0110", dy => "1001", bw => "0011", bh => "1100", ls => "00100");
	-- all primary color
	constant PATTERN_7     : pattern_t := (dx => "0000", dy => "0000", bw => "1111", bh => "1111", ls => "00000");

	-- ops_cnt constants
	constant OPS_NOP         : integer := 0;
	constant OPS_CLEAR       : integer := 0;
	constant OPS_SET_PIXEL   : integer := 2;
	constant OPS_DRAW_LINE   : integer := 4;
	constant OPS_DRAW_CIRCLE : integer := 3;
	constant OPS_DRAW_RECT   : integer := 4;
	constant OPS_SET_PATTERN : integer := 2;
	constant OPS_SET_COLOR   : integer := 1;
	constant OPS_SET_CFG     : integer := 0;
	constant OPS_FRAME_SYNC  : integer := 0;

	-- fsm signal definitions
	type fsm_state_t is (
		IDLE, READ_INSTR, PROCESS_OPS, READ_OP,
		INSTR_NOP, INSTR_CLEAR, INSTR_SET_PIXEL,
		INSTR_DRAW_LINE_LOAD, INSTR_DRAW_LINE,
		INSTR_DRAW_CIRCLE_LOAD, INSTR_DRAW_CIRCLE,
		INSTR_DRAW_RECT_LOAD, INSTR_DRAW_RECT,
		INSTR_SET_PATTERN, INSTR_SET_COLOR, INSTR_SET_CFG,
		INSTR_FRAME_SYNC
	);
	-- color range
	subtype col_range_t is Natural range COLOR_WIDTH - 1 downto 0;
	type fsm_data_t is record
		-- state
		saved_instr : fsm_state_t;
		-- TODO: generics?
		immediate   : std_logic_vector(4 downto 0);
		-- colors
		col_primary   : std_logic_vector(col_range_t);
		col_secondary : std_logic_vector(col_range_t);
		-- ops
		parameters : drawing_parameters_t;
		-- rect
		patterns : pattern_array_t;
		-- counters
		ops_cnt      : integer range 0 to 4;
		ops_required : integer range 0 to 4;
		wr_cnt       : integer range 0 to WIDTH * HEIGHT;
		-- toggles
		double_buffering : std_logic;
		first_buffer     : std_logic;
		-- double buffering store
		vram_base_addr : std_logic_vector(VRAM_ADDR_WIDTH - 1 downto 0);
	end record;

	-- fsm signals
	constant INITIAL_STATE    : fsm_state_t := IDLE;
	constant INITIAL_FSM_DATA : fsm_data_t := (
		saved_instr => INITIAL_STATE,
		immediate => (others => '0'),
		col_primary => (others => '1'),
		col_secondary => (others => '0'),
		parameters => INITIAL_PARAMETERS,
		patterns => (PATTERN_0, PATTERN_1, PATTERN_2, PATTERN_3, PATTERN_4, PATTERN_5, PATTERN_6, PATTERN_7),
		ops_cnt => 0,
		ops_required => 0,
		wr_cnt => 0,
		double_buffering => '0',
		first_buffer => '1',
		vram_base_addr => (others => '0')
	);

	signal fsm_data, fsm_data_next : fsm_data_t;
	signal state, state_next       : fsm_state_t;

begin
	-- structural
	fifo_1c1r1w_data_inst : fifo_1c1r1w
	generic map (
		DEPTH => FIFO_DATA_DEPTH,
		DATA_WIDTH => FIFO_DATA_WIDTH
	)
	port map (
		clk       => clk,
		res_n     => res_n,

		rd_data   => gfx_data_rd_data,
		rd        => gfx_data_rd,

		wr_data   => gfx_data,
		wr        => gfx_data_wr,

		empty     => gfx_data_empty,
		full      => gfx_data_full,
		half_full => open
	);

	fifo_1c1r1w_instr_inst : fifo_1c1r1w
	generic map (
		DEPTH => FIFO_INSTR_DEPTH,
		DATA_WIDTH => FIFO_INSTR_WIDTH
	)
	port map (
		clk       => clk,
		res_n     => res_n,

		rd_data   => gfx_instr_rd_data,
		rd        => gfx_instr_rd,

		wr_data   => gfx_instr,
		wr        => gfx_instr_wr,

		empty     => gfx_instr_empty,
		full      => gfx_instr_full,
		half_full => open
	);

	gfx_line_inst : gfx_line
	generic map (
		WIDTH => WIDTH,
		HEIGHT => HEIGHT
	)
	port map (
		clk   => clk,
		res_n => res_n,
	
		-- control signals
		start => control_line.start,
		stall => '0',
		busy  => control_line.busy,

		-- drawing parameters
		x0 => fsm_data.parameters.op1(pixel_x_range_t),
		y0 => fsm_data.parameters.op2(pixel_y_range_t),
		x1 => fsm_data.parameters.op3(pixel_x_range_t),
		y1 => fsm_data.parameters.op4(pixel_y_range_t),

		-- pixel coordinates
		pixel_valid => pixel_line.valid,
		pixel_x     => pixel_line.x,
		pixel_y     => pixel_line.y
	);

	gfx_circle_inst : gfx_circle
	generic map (
		WIDTH => WIDTH,
		HEIGHT => HEIGHT
	)
	port map (
		clk   => clk,
		res_n => res_n,

		-- control signals
		start => control_circle.start,
		stall => '0',
		busy  => control_circle.busy,

		-- drawing parameters
		x_center => fsm_data.parameters.op1(pixel_x_range_t),
		y_center => fsm_data.parameters.op2(pixel_y_range_t),
		radius   => fsm_data.parameters.op3(pixel_x_range_t),

		-- pixel coordinates
		pixel_valid => pixel_circle.valid,
		pixel_x     => pixel_circle.x,
		pixel_y     => pixel_circle.y
	);

	gfx_rect_inst : gfx_rect
	generic map (
		WIDTH => WIDTH,
		HEIGHT => HEIGHT
	)
	port map (
		clk   => clk,
		res_n => res_n,

		-- control signals
		start => control_rect.start,
		stall => '0',
		busy  => control_rect.busy,

		-- drawing parameters
		x => fsm_data.parameters.op1(pixel_x_range_t),
		y => fsm_data.parameters.op2(pixel_y_range_t),
		w => fsm_data.parameters.op3(pixel_x_range_t),
		h => fsm_data.parameters.op4(pixel_y_range_t),

		bw => extra_parameters.pattern.bw,
		bh => extra_parameters.pattern.bh,
		dx => extra_parameters.pattern.dx,
		dy => extra_parameters.pattern.dy,
		ls => extra_parameters.pattern.ls,

		fill => extra_parameters.fill,
		draw => extra_parameters.draw,

		-- pixel coordinates
		pixel_valid => pixel_rect.valid,
		pixel_x     => pixel_rect.x,
		pixel_y     => pixel_rect.y,
		pixel_color => pixel_rect_color
	);

	-- concurrent
	-- sequential
	fsm_sync : process(clk, res_n)
	begin
		if res_n = '0' then
			state <= INITIAL_STATE;
			fsm_data <= INITIAL_FSM_DATA;
		elsif rising_edge(clk) then
			state <= state_next;
			fsm_data <= fsm_data_next;
		end if;
	end process;

	fsm_async : process(all)
		variable index : integer;
	begin
		-- default assignment
		state_next <= state;
		fsm_data_next <= fsm_data;

		-- fifo signals
		gfx_instr_rd <= '0';
		gfx_data_rd <= '0';

		-- control signals
		control_line.start <= '0';
		control_circle.start <= '0';
		control_rect.start <= '0';

		-- vram write signals
		vram_wr <= '0';
		vram_addr <= (others => '0');
		vram_data <= (others => '0');

		-- aux rect signals
		if state = INSTR_DRAW_RECT_LOAD or state = INSTR_DRAW_RECT then
			-- load user specified pattern
			extra_parameters.pattern <= fsm_data.patterns(to_integer(unsigned(fsm_data.immediate(2 downto 0))));
		else
			extra_parameters.pattern <= PATTERN_0;
		end if;
		extra_parameters.fill <= '1';
		extra_parameters.draw <= '0';

		-- gfx signals
		gfx_frame_sync <= '0';
		-- double buffering
		vram_base_addr <= fsm_data.vram_base_addr;

		case state is
			when IDLE =>
				-- wait until fifo is not empty
				if not(gfx_instr_empty = '1') then
					-- out
					state_next <= READ_INSTR;
					-- next
					gfx_instr_rd <= '1';
					fsm_data_next.ops_cnt <= 0;
				end if;

			when READ_INSTR =>
				--report "handling instruction " & to_string(gfx_instr_rd_data);
				-- transition into processing of gfx_data
				state_next <= PROCESS_OPS;
				-- not strictly necessary, but for clarity
				gfx_instr_rd <= '0';
				-- determine which instruction was passed and
				-- save respective next state
				if    is_instr_format_B(gfx_instr_rd_data) then
					-- B format
					fsm_data_next.immediate <= gfx_instr_rd_data(4 downto 0);
					case gfx_instr_rd_data(5 downto 4) is
						when "10" =>
							fsm_data_next.saved_instr <= INSTR_SET_PATTERN;
							fsm_data_next.ops_required <= OPS_SET_PATTERN;
						when "01" =>
							fsm_data_next.saved_instr <= INSTR_SET_COLOR;
							fsm_data_next.ops_required <= OPS_SET_COLOR;
						when others =>
							fsm_data_next.saved_instr <= INSTR_SET_CFG;
							fsm_data_next.ops_required <= OPS_SET_CFG;
					end case;
				elsif is_instr_format_C(gfx_instr_rd_data) then
					fsm_data_next.immediate <= gfx_instr_rd_data(4 downto 0);
					-- C format
					case gfx_instr_rd_data(6 downto 5) is
						when others =>
							fsm_data_next.saved_instr <= INSTR_DRAW_RECT_LOAD;
							fsm_data_next.ops_required <= OPS_DRAW_RECT;
					end case;
				else
					-- A format
					case gfx_instr_rd_data(3 downto 0) is
						when "0001" =>
							fsm_data_next.saved_instr <= INSTR_CLEAR;
							fsm_data_next.ops_required <= OPS_CLEAR;
						when "1000" =>
							fsm_data_next.saved_instr <= INSTR_SET_PIXEL;
							fsm_data_next.ops_required <= OPS_SET_PIXEL;
						when "1001" =>
							fsm_data_next.saved_instr <= INSTR_DRAW_LINE_LOAD;
							fsm_data_next.ops_required <= OPS_DRAW_LINE;
						when "1010" =>
							fsm_data_next.saved_instr <= INSTR_DRAW_CIRCLE_LOAD;
							fsm_data_next.ops_required <= OPS_DRAW_CIRCLE;
						when "0010" =>
							fsm_data_next.saved_instr <= INSTR_FRAME_SYNC;
							fsm_data_next.ops_required <= OPS_FRAME_SYNC;
						when others =>
							fsm_data_next.saved_instr <= INSTR_NOP;
							fsm_data_next.ops_required <= OPS_NOP;
					end case;
				end if;

			when PROCESS_OPS =>
				if not(fsm_data.ops_cnt = fsm_data.ops_required) then
					if not(gfx_data_empty = '1') then
						-- next
						state_next <= READ_OP;
						-- out
						gfx_data_rd <= '1';
					end if;
				else
					-- continue with instr that was saved
					state_next <= fsm_data.saved_instr;
				end if;

			when READ_OP =>
				-- next
				state_next <= PROCESS_OPS;
				-- out
				gfx_data_rd <= '0';
				case fsm_data.ops_cnt is
					when 0 => fsm_data_next.parameters.op1 <= gfx_data_rd_data;
					when 1 => fsm_data_next.parameters.op2 <= gfx_data_rd_data;
					when 2 => fsm_data_next.parameters.op3 <= gfx_data_rd_data;
					when 3 => fsm_data_next.parameters.op4 <= gfx_data_rd_data;
					when others => null;
				end case;
				fsm_data_next.ops_cnt <= fsm_data.ops_cnt + 1;

			when INSTR_NOP =>
				state_next <= IDLE;

			when INSTR_CLEAR =>
				if fsm_data.wr_cnt = (WIDTH * HEIGHT) - 1 then
					state_next <= IDLE;
					fsm_data_next.wr_cnt <= 0;
				else
					fsm_data_next.wr_cnt <= fsm_data.wr_cnt + 1;
				end if;
				vram_wr <= '1';
				-- TODO: kind of ugly, make it prettier somehow?
				vram_addr(vram_base_addr'range) <= std_logic_vector(
					unsigned(vram_base_addr) +
					to_unsigned(fsm_data.wr_cnt, VRAM_ADDR_WIDTH));
				vram_data(fsm_data.col_secondary'range) <= fsm_data.col_secondary;

			when INSTR_SET_PIXEL =>
				-- next
				state_next <= IDLE;
				-- out
				vram_wr <= '1';
				vram_addr(vram_base_addr'range) <= std_logic_vector(
					unsigned(vram_base_addr) +
					unsigned(fsm_data.parameters.op1) +
					unsigned(fsm_data.parameters.op2)
				);

			when INSTR_DRAW_LINE_LOAD =>
				-- next
				state_next <= INSTR_DRAW_LINE;
				-- out
				control_line.start <= '1';

			when INSTR_DRAW_LINE =>
				-- stays busy until drawing is done
				if control_line.busy = '1' then
					-- goes high when data is ready
					if pixel_line.valid = '1' then
						vram_wr <= '1';
						vram_addr(vram_base_addr'range) <= std_logic_vector(
							unsigned(vram_base_addr) +
							unsigned(pixel_line.x) +
							unsigned(pixel_line.y) * to_unsigned(WIDTH, log2c(WIDTH)));
						vram_data(fsm_data.col_primary'range) <= fsm_data.col_primary;
					end if;
				else
					state_next <= IDLE;
				end if;

			when INSTR_DRAW_CIRCLE_LOAD =>
				-- next
				state_next <= INSTR_DRAW_CIRCLE;
				-- out
				control_circle.start <= '1';

			when INSTR_DRAW_CIRCLE =>
				-- stays busy until drawing is done
				if control_circle.busy = '1' then
					-- goes high when data is ready
					if pixel_circle.valid = '1' then
						vram_wr <= '1';
						vram_addr(vram_base_addr'range) <= std_logic_vector(
							unsigned(vram_base_addr) +
							unsigned(pixel_circle.x) +
							unsigned(pixel_circle.y) * to_unsigned(WIDTH, log2c(WIDTH)));
						vram_data(fsm_data.col_primary'range) <= fsm_data.col_primary;
					end if;
				else
					state_next <= IDLE;
				end if;

			when INSTR_DRAW_RECT_LOAD =>
				-- next
				state_next <= INSTR_DRAW_RECT;
				-- out
				extra_parameters.draw <= fsm_data.immediate(3);
				extra_parameters.pattern <= fsm_data.patterns(to_integer(unsigned(fsm_data.immediate(2 downto 0))));
				control_rect.start <= '1';

			when INSTR_DRAW_RECT =>
				-- keep driving draw until not busy anymore
				extra_parameters.draw <= fsm_data.immediate(3);
				-- stays busy until drawing is done
				if control_rect.busy = '1' then
					-- goes high when data is ready
					if pixel_rect.valid = '1' then
						vram_wr <= '1';
						vram_addr(vram_base_addr'range) <= std_logic_vector(
							unsigned(vram_base_addr) +
							unsigned(pixel_rect.x) +
							unsigned(pixel_rect.y) * to_unsigned(WIDTH, log2c(WIDTH)));
						-- determine color to draw
						if pixel_rect_color = '1' then
							vram_data(fsm_data.col_primary'range) <= fsm_data.col_primary;
						-- if immediate parameter A for alpha mode is not 1
						elsif not(fsm_data.immediate(4) = '1') then
							vram_data(fsm_data.col_secondary'range) <= fsm_data.col_secondary;
						end if;
					end if;
				else
					state_next <= IDLE;
				end if;

			when INSTR_SET_PATTERN =>
				-- next
				state_next <= IDLE;
				-- out
				-- be careful not to change preset patterns!
				if not(fsm_data.immediate(2 downto 0) = "000") and not(fsm_data.immediate(2 downto 0) = "111") then
					-- get index from immediate(2 downto 0)
					index := to_integer(unsigned(fsm_data.immediate(2 downto 0)));
					fsm_data_next.patterns(index).dx <= fsm_data.parameters.op1(15 downto 12);
					fsm_data_next.patterns(index).dy <= fsm_data.parameters.op1(11 downto 8);
					fsm_data_next.patterns(index).bw <= fsm_data.parameters.op1(7 downto 4);
					fsm_data_next.patterns(index).bh <= fsm_data.parameters.op1(3 downto 0);
					fsm_data_next.patterns(index).ls <= fsm_data.parameters.op2(4 downto 0);
				end if;

			when INSTR_SET_COLOR =>
				-- next
				state_next <= IDLE;
				-- out
				-- select color
				if fsm_data.immediate(0) = '1' then
					fsm_data_next.col_primary <= fsm_data.parameters.op1(fsm_data.col_primary'range);
				else
					fsm_data_next.col_secondary <= fsm_data.parameters.op1(fsm_data.col_secondary'range);
				end if;

			when INSTR_SET_CFG =>
				-- next
				state_next <= IDLE;
				-- out
				fsm_data_next.double_buffering <= fsm_data.immediate(0);

			when INSTR_FRAME_SYNC =>
				-- wait until start of frame
				if frame_start = '1' then
					-- next
					state_next <= IDLE;
					-- out
					gfx_frame_sync <= '1';
					if fsm_data.double_buffering = '1' then
						-- switch between buffers
						if fsm_data.vram_base_addr = std_logic_vector(to_unsigned(WIDTH * HEIGHT, VRAM_ADDR_WIDTH)) then
							fsm_data_next.vram_base_addr <= (others => '0');
						else
							fsm_data_next.vram_base_addr <= std_logic_vector(to_unsigned(WIDTH * HEIGHT, VRAM_ADDR_WIDTH));
						end if;
					end if;
				end if;

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

end architecture;
