--
--   Copyright (C) 2003 by J. Kearney, Bolton, Massachusetts
--
--   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 2 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 should have received a copy of the GNU General Public License along
-- with this program; if not, write to the Free Software Foundation, Inc.,
-- 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--

library IEEE;			
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.numeric_std.ALL;

library unisim;
use unisim.vcomponents.all;

use work.IOB_Config.ALL;
use work.VT52_cfg.ALL;

entity VT52 is
	port
		(-- addresses
		 keyb_addr: in DevID;
		 print_addr: in DevID;

		 -- clocks
		 clk: in std_logic;
		 clkdiv24: in std_logic;
		 vidclk: in std_logic;

--		 DEBUGIO: inout std_logic_vector(0 to 11);

		 -- CPU bus interface
		 reset: in boolean;
		 IOTact: in boolean;
		 IOTdev: in DevID;
		 IOTcmd: in DevCmd;

	 	 cpu_write_n : in std_logic;
		 clk_write_n : in std_logic;
		 IOTread : in boolean;
		 dx : inout std_logic_vector(0 to 11);
		 cpu_c0_n : out std_logic;
		 cpu_c1_n : out std_logic;
		 cpu_skip_n : out std_logic;
		 IRQ: out std_logic;

		 -- Keyboard
		 kb_clk: inout std_logic;
		 kb_data: inout std_logic;

		 -- VGA
		 vga_RGB: out RGB;
		 vga_HS: out std_logic;
		 vga_VS: out std_logic;
		 
		 -- bell
		 bell: out boolean;
		 
		 -- break
		 break: out boolean);
end VT52;

architecture RTL of VT52 is

	component ps2kbd is
		port
			(reset: in boolean;
			 clk: in std_logic;
			 clkdiv24: in std_logic;
		  	 alt_keypad_mode: in boolean;
			 hold_screen_mode: in boolean;
			 send_identify: in boolean;
			 hold_line_release: out std_logic;
			 hold_scrn_release: out std_logic;
          break_key: out std_logic;
          ack : in std_logic;
          data : out std_logic_vector(6 downto 0);
          data_avail : out std_logic;
          kb_clk_raw : inout std_logic;
          kb_data_raw : inout std_logic);
	end component;

	component VideoGen is
		generic
			(CLKFREQ: integer := 29491200;
			 HCENTER: real := 0.50;
			 VCENTER: real := 0.50;
			 curs_blink: boolean := true;
			 curs_block: boolean := false;
	       fore_color: RGB := "111";
	       back_color: RGB := "000");
	    port
		 	(clk : in std_logic;
	       X : out COLUMN;
	       Y : out ROW;
			 text_rd: out std_logic;
	       text : in std_logic_vector(7 downto 0);
	       curs_X : in COLUMN;
	       curs_Y : in ROW;
			 invert_screen: in boolean;
	       vga_RGB : out RGB;
	       vga_HSYNC : out std_logic;
	       vga_VSYNC : out std_logic);
	end component;

	component addrmap
		port
			(x: in COLUMN;
			 y: in ROW;
			 ybase: in ROW;
			 addr: OUT std_logic_vector(10 downto 0));
	end component;

	component PosEdge
		port
		  (reset: in boolean;
			clk: in std_logic;
			inp: in std_logic;          
			outp: out std_logic);
	end component;

	component bbfifo_16xN is
		generic
			(WIDTH : integer);
		port
			(data_in : in std_logic_vector(WIDTH-1 downto 0);
			 data_out : out std_logic_vector(WIDTH-1 downto 0);
			 reset : in std_logic;               
			 write : in std_logic; 
			 read : in std_logic;
			 full : out std_logic;
			 half_full : out std_logic;
			 data_present : out std_logic;
			 clk : in std_logic);
	end component;

	-- flags and interrupts
	signal IE, printer_flag, keyb_flag: std_logic;

	signal write: boolean;

	-- keyboard logic

	subtype ASCII7 is std_logic_vector(6 downto 0);
	signal rsel, rselread, alt_keypad_mode: boolean;
	signal advance_n, advance_pe, kb_valid_data,
			 kb_avail, kb_ack, resetsl: std_logic;
	signal kb_ASCII, keyQhead: ASCII7;
	signal inppacing: integer range 0 to INTERCHDELAY/24;
	signal inpready, inpavail, inpavail_pe: std_logic;
	signal iclrflag: boolean;

	-- video logic
	signal vga_VS_int, VS_pe: std_logic;

	signal scan_X, curs_X, save_X: COLUMN;
	signal top_Y, scan_Y, curs_Y, save_Y: ROW;
	signal invtext, scr_rd_en, scr_wr_en: std_logic;
	signal scr_rd_addr, scr_wr_addr: std_logic_vector(10 downto 0);
	signal scr_rd, scr_wr: std_logic_vector(7 downto 0);

	-- output control signals
	signal oclrflag: boolean;
	signal osetflag: std_logic;
	signal tsel: boolean;

	-- terminal control logic

	signal outrdy, THRE, THRE_1, THRE_2: std_logic;
	signal outpacing: integer range 0 to INTERCHDELAY/24;
	signal outc: std_logic_vector(6 downto 0);
	signal bell_int, visible_bell, erase_op: boolean;
	signal bell_count: integer range 0 to BELLDURATION;
	signal identify: boolean;

	signal break_key, hold_line_release, hold_scrn_release: std_logic;
	signal break_int: boolean;
	signal break_count: integer range 0 to 7;

	signal first_curs_X, last_curs_X: boolean;

	type OutState is
		(
		BumpIdle,
		Idle,
		IncrX,
		WaitCmd,
		WaitX,
		WaitY,
		ClearLine,
		WriteEOL,
		WriteEOS
		);

	signal ostate: OutState;

begin

	-- misc

	write <= (cpu_write_n = '0');
	IRQ <= IE and (printer_flag or keyb_flag);

-- debugging
--	process (printer_flag, tsel, outavail, IOTact, outavail, IOTdev, print_addr)
--	begin
--		if DEBUG then
--			DEBUGIO(0) <= printer_flag;
--			if IOTact then DEBUGIO(1) <= '1'; else DEBUGIO(1) <= '0'; end if;
--			DEBUGIO(2) <= outavail;
--			if IOTdev = print_addr then DEBUGIO(3) <= '1'; else DEBUGIO(3) <= '0'; end if;
--			DEBUGIO(6 to 11) <= std_logic_vector(IOTdev(0 to 5));
--		end if;
--	end process;

	-- Input (PS/2 keyboard)

	keyb: ps2kbd port map
		(reset => reset, clk => clk, clkdiv24 => clkdiv24,
		 alt_keypad_mode => alt_keypad_mode,
		 hold_screen_mode => false,
		 send_identify => identify,
		 hold_line_release => hold_line_release,
		 hold_scrn_release => hold_scrn_release,
		 break_key => break_key,
		 ack => kb_ack, data => kb_ASCII, data_avail => kb_avail,
		 kb_clk_raw => kb_clk, kb_data_raw => kb_data);

	resetsl <= '1' when reset else '0';

	kb_valid_data <= kb_ack and kb_avail;

	fifo: bbfifo_16xN
		generic map (WIDTH => 7)
		port map
			(data_in => kb_ASCII, data_out => keyQhead,
			 reset => resetsl,
			 write => kb_valid_data,
			 read => advance_pe,
			 full => OPEN,
			 half_full => OPEN,
			 data_present => inpready,
			 clk => clk);
	
	-- FIFO filling process
	process (reset, clk, kb_avail, kb_ack)
	begin
		if reset then
			kb_ack <= '0';
		elsif rising_edge(clk) then
			if kb_ack = '1' then
				kb_ack <= '0';
			elsif ((break_key or hold_line_release or hold_scrn_release or kb_avail) = '1') and
					-- n.b. we don't want to put another key into the FIFO during
					-- an IOT in which the flag might be cleared.  Not only would we
					-- lose the second key, but the flag will never go to '1' again
					-- because there will never be a rising edge on keyQ/data_present.
			      (not rsel) then
				kb_ack <= '1';
			end if;
		end if;
	end process;

	-- manage input pacing timer.  We limit the rate of input characters to
	-- a speed reasonable for a VT52
	process (reset, clkdiv24, iclrflag, inppacing)
	begin
		if reset then
			inppacing <= 0;
		elsif iclrflag then
			inppacing <= INTERCHDELAY/24;
		elsif rising_edge(clkdiv24) then
			if inppacing /= 0 then
				inppacing <= inppacing - 1;
			end if;
		end if;
	end process;

	-- manage keyboard flag
	inpavail <= '1' when (inpready = '1') and (inppacing = 0) else '0';
	inpavail_det: PosEdge port map (reset, clk, inpavail, inpavail_pe);

	-- reading process
	rsel <= IOTact and (IOTdev = keyb_addr);
	rselread <= rsel and IOTread;

	dx(5 to 11) <= keyQhead when rselread else (others => 'Z');
	dx(0 to 4) <= (others => '0') when rselread else (others => 'Z');

	-- keyboard flag management
	iclrflag <= rsel and ((IOTcmd = KCC) or (IOTcmd = KRB));
				
	process (reset, clk, inpavail_pe, iclrflag)
	begin
		if reset or iclrflag then
			keyb_flag <= '0';
		elsif rising_edge(clk) then
			if inpavail_pe = '1' then
				keyb_flag <= '1';
			end if;
		end if;
	end process;

	-- need to advance head after IOT that reads char
	advance_n <= '0' when rselread and (inpready = '1') else '1';
	adv_det: PosEdge port map (reset, clk, advance_n, advance_pe);

	-- latch IE bit
	process (reset, clk_write_n)
	begin
		if reset then
			IE <= '0';
		elsif rising_edge(clk_write_n) then
			if rsel and (IOTcmd = KIE) then
				IE <= dx(11);
			end if;
		end if;
	end process;

	-- c0/1/skip feedback
	process (rsel, write, iotcmd, keyb_flag)
	begin
		if rsel and write then

			-- c0/c1
			case IOTcmd is

				when KCC =>
					cpu_c0_n <= '0';
					cpu_c1_n <= 'Z';

				when KRS =>
					cpu_c0_n <= 'Z';
					cpu_c1_n <= '0';

				when KRB =>
					cpu_c0_n <= '0';
					cpu_c1_n <= '0';

				when others =>
					cpu_c0_n <= 'Z';
					cpu_c1_n <= 'Z';

			end case;

			-- skip
			case IOTcmd is

				when KSF =>
					if keyb_flag = '1' then
						cpu_skip_n <= '0';
					else
						cpu_skip_n <= 'Z';
					end if;

				when others =>
					cpu_skip_n <= 'Z';

			end case;
		else
			cpu_c0_n <= 'Z';
			cpu_c1_n <= 'Z';
			cpu_skip_n <= 'Z';
		end if;
	end process;

	-- Output (VGA display)
	tsel <= IOTact and (IOTdev = print_addr);

	-- manage output pacing timer

	process (reset, clkdiv24, ostate, outpacing)
	begin
		if reset or (ostate /= Idle) then
			outrdy <= '0';
			outpacing <= INTERCHDELAY/24;
		elsif rising_edge(clkdiv24) then
			if outrdy = '0' then
			   if outpacing = 0 then
					outrdy <= '1';
				else
					outpacing <= outpacing - 1;
				end if;
			end if;
		end if;
	end process;

	-- manage print flag
	osetflag <= '1' when tsel and write and (IOTcmd = TFL) else '0';
	oclrflag <= tsel and write and ((IOTcmd = TCF) or (IOTcmd = TLS));
				
	process (reset, clk, outrdy, osetflag, oclrflag)
	begin
		if reset or oclrflag or (osetflag = '1') then
			printer_flag <= osetflag;
		elsif rising_edge(outrdy) then
			printer_flag <= '1';
		end if;
	end process;

	-- manage transmit
	THRE <= '1' when THRE_1 = THRE_2 else '0';

	process (reset, clk_write_n, tsel, THRE_2)
	begin
		if reset then
			THRE_1 <= '0';
		elsif rising_edge(clk_write_n) then
			if tsel and ((IOTcmd = TPC) or (IOTcmd = TLS)) then
				-- latch the output character, reversing the bits while we're at it
				outc <= dx(5) & dx(6) & dx(7) & dx(8) & dx(9) & dx(10) & dx(11);
				THRE_1 <= not THRE_2;
			end if;
		end if;
	end process;

	-- c0/1/skip feedback
	process (tsel, write, iotcmd, printer_flag, keyb_flag)
	begin
		if tsel and write then

			-- skip
			case IOTcmd is

				when TSF =>
					if printer_flag = '1' then
						cpu_skip_n <= '0';
					else
						cpu_skip_n <= 'Z';
					end if;

				when TSK =>
					if (printer_flag or keyb_flag) = '1' then
						cpu_skip_n <= '0';
					else
						cpu_skip_n <= 'Z';
					end if;

				when others =>
					cpu_skip_n <= 'Z';

			end case;
		else
			cpu_skip_n <= 'Z';
		end if;
	end process;


	-- map the user cursor and screen refresh cursor to RAM addresses
	curs_map: addrmap
		port map
			(x => curs_X, Y => curs_Y, Ybase => top_Y, addr => scr_wr_addr);

	scan_map: addrmap
		port map
			(x => scan_X, Y => scan_Y, Ybase => top_Y, addr => scr_rd_addr);

	-- the text buffer
	textram: for n in 0 to 3 generate
	begin
		textb: RAMB4_S2_S2
			port map
				(
				-- reading port
				RSTA => '0', WEA => '0', ENA => scr_rd_en, CLKA => clk,
				ADDRA => scr_rd_addr, DIA => "00",
				DOA => scr_rd(n*2+1 downto n*2),

				-- writing port
				RSTB => '0', WEB => '1', ENB => scr_wr_en, CLKB => clk,
				ADDRB => scr_wr_addr, DOB => open,
				DIB => scr_wr(n*2+1 downto n*2)
				);
	end generate;

	vga_VS <= vga_VS_int;

	-- this module generates the timing and video signals, and converts the ASCII into
	-- its corresponding scan patterns
	video: VideoGen
		generic map
			(curs_blink => CURSBLINK,
			 curs_block => CURSBLOCK,
	       fore_color => FOREG,
	       back_color => BACKG)
	    port map
		 	(clk => vidclk,
			 X => scan_x, Y => scan_y,
	       text_rd => scr_rd_en, text => scr_rd,
	       curs_X => curs_X, curs_Y => curs_Y,
			 invert_screen => visible_bell,
	       vga_RGB => vga_RGB, vga_HSYNC => vga_HS, vga_VSYNC => vga_VS_int);

	visible_bell <= (bell_int or break_int) when BELLVISIBLE else false;
	bell <= bell_int when BELLSOUND else false;

	break_int <= (break_count /= 0);
	break <= break_int;

	-- character writing logic.
	-- this is the entire state machine that interprets character writes as
	-- displayable characters or executes control or escape functions

	vs_det: PosEdge port map (reset, clk, vga_VS_int, VS_pe);

	first_curs_X <= (curs_X = 0);
	last_curs_X  <= (curs_X = COLUMNS-1);

	scr_wr(7) <= invtext;
	scr_wr(6 downto 0) <= "0100000" when erase_op else outc;

	-- the state machine
	process (reset, clk, ostate)
	begin
		if reset then

			THRE_2 <= '0';

			-- initial state=> clear screen
			curs_X <= 0;
			curs_Y <= 0;
			save_X <= 0;
			save_Y <= 0;
			erase_op <= true;
			scr_wr_en <= '1';
			ostate <= WriteEOS;

			bell_count <= 0;
			bell_int <= false;
			invtext <= '0';
			alt_keypad_mode <= false;
			identify <= false;
			break_count <= 0;

		elsif rising_edge(clk) then

			identify <= false;

			if break_count = 0 then
				if break_key = '1' then
					break_count <= 1;
				end if;
			else
				if break_count = 7 then
					break_count <= 0;
				elsif VS_pe = '1' then
					break_count <= break_count + 1;
				end if;
			end if;

			case ostate is

				when BumpIdle =>
					-- we use this state as a way to momentarily leave Idle, so
					-- that the flag machinery is triggered
					ostate <= Idle;

				when Idle =>

					-- reset various state flags
					erase_op <= false;

					-- bell counter
					if VS_PE = '1' then
						if bell_count /= 0 then
							bell_count <= bell_count - 1;
						else
							bell_int <= false;
						end if;
					end if;

					if THRE = '0' then

						THRE_2 <= THRE_1;	-- handshake character

						if outc(6 downto 5) /= "00" then

							-- write the character to the screen
							scr_wr_en <= '1';
							ostate <= IncrX;

						else

							-- interpret control character
							case outc(4 downto 0) is
								when "00111" =>	-- BEL
									bell_count <= BELLDURATION;
									bell_int <= true;
									ostate <= BumpIdle;

								when "01000" =>	-- BS
									if not first_curs_X then
										curs_X <= curs_X - 1;
									end if;
									ostate <= BumpIdle;

								when "01001" =>	-- Tab
									if curs_X < ((COLUMNS-1) / 8)*8 then
										curs_X <= to_integer(to_unsigned(curs_X, 7) or "111") + 1;
									end if;
									ostate <= BumpIdle;

								when "01010" =>	-- LF
									if curs_Y = ROWS-1 then
										if top_Y = ROWS-1 then
											top_Y <= 0;
										else
											top_Y <= top_Y + 1;
										end if;
										ostate <= ClearLine;
									else
										curs_Y <= curs_Y + 1;
										ostate <= BumpIdle;
									end if;

								when "01101" =>	-- CR
									curs_X <= 0;
									ostate <= BumpIdle;

								when "01110" =>	-- SO	(inverse)
									invtext <= '1';
									ostate <= BumpIdle;

								when "01111" =>	-- SI	(normal)
									invtext <= '0';
									ostate <= BumpIdle;

								when "11011" =>	-- ESC
									ostate <= WaitCmd;

								when others =>		-- unknown ESC code, don't display it
									ostate <= BumpIdle;

							end case;

						end if;
					end if;

				when WaitCmd =>

					if THRE = '0' then

						THRE_2 <= THRE_1;	-- handshake character

						case outc is

							when "1000001" =>	-- 'A'	cursor up
								if curs_Y /= 0 then
									curs_Y <= curs_Y - 1;
								end if;
								ostate <= Idle;
							
							when "1000010" =>	-- 'B'	cursor down
								if curs_Y /= ROWS-1 then
									curs_Y <= curs_Y + 1;
								end if;
								ostate <= Idle;
							
							when "1000011" =>	-- 'C'	cursor right
								if not last_curs_X then
									curs_X <= curs_X + 1;
								end if;
								ostate <= Idle;
							
							when "1000100" =>	-- 'D'	cursor left
								if not first_curs_X then
									curs_X <= curs_X - 1;
								end if;
								ostate <= Idle;

							when "1001000" =>	-- 'H'	cursor home
								curs_X <= 0;
								curs_Y <= 0;
								ostate <= Idle;


							when "0111101" =>	-- '='	alt keypad mode
								alt_keypad_mode <= true;
								ostate <= Idle;

							when "0111110" =>	-- '>'	norm keypad mode
								alt_keypad_mode <= false;
								ostate <= Idle;

							when "1001001" =>	-- 'I'	reverse linefeed
								if curs_Y = 0 then
									if top_Y = 0 then
										top_Y <= ROWS-1;
									else
										top_Y <= top_Y - 1;
									end if;
									ostate <= ClearLine;
								else
									curs_Y <= curs_Y - 1;
									ostate <= Idle;
								end if;

							when "1001010" =>	-- 'J'	erase to end of screen
								erase_op <= true;
								save_X <= curs_X;
								save_Y <= curs_Y;
								scr_wr_en <= '1';
								ostate <= WriteEOS;

							when "1001011" =>	-- 'K'	erase to end of line
								erase_op <= true;
								save_X <= curs_X;
								scr_wr_en <= '1';
								ostate <= WriteEOL;

							when "1011010" =>	-- 'Z'	identify
								identify <= true;
								ostate <= Idle;
							
							when "1011001" =>	-- 'Y'	cursor positioning
								ostate <= WaitY;

							when others =>
								ostate <= Idle;

						end case;

					end if;

				when WaitY =>

					if THRE = '0' then
						THRE_2 <= THRE_1;	-- handshake character
						curs_Y <= to_integer(unsigned(outc)) - 32;
						ostate <= WaitX;
					else
						ostate <= Idle;
					end if;

				when WaitX =>

					if THRE = '0' then
						THRE_2 <= THRE_1;	-- handshake character
						curs_X <= to_integer(unsigned(outc)) - 32;
					end if;
					ostate <= Idle;

				when IncrX =>
					scr_wr_en <= '0';
					if not last_curs_X then
						curs_X <= curs_X + 1;
					end if;
					ostate <= Idle;

				when ClearLine =>				-- common code for fwd and rev line feed
					erase_op <= true;
					save_X <= curs_X;
					curs_X <= 0;
					scr_wr_en <= '1';
					ostate <= WriteEOL;

				when WriteEOL =>				-- keep writing until end of line
					if last_curs_X then
						scr_wr_en <= '0';
						ostate <= Idle;
						curs_X <= save_X;
					else
						curs_X <= curs_X + 1;
						ostate <= WriteEOL;
					end if;

				when WriteEOS =>				-- keep writing until end of screen
					if last_curs_X then
						if curs_Y = ROWS-1 then
							scr_wr_en <= '0';
							ostate <= Idle;
							curs_X <= save_X;
							curs_Y <= save_Y;
						else
							curs_Y <= curs_Y + 1;
							curs_X <= 0;
							ostate <= WriteEOS;
						end if;
					else
						curs_X <= curs_X + 1;
						ostate <= WriteEOS;
					end if;

				end case;

		end if;
	end process;

end RTL;
