-module(client).

-export([main/1, initial_state/2]).

-include_lib("./defs.hrl").

-spec main(_InitialState) -> _.
-spec listen(_State) -> _.
-spec initial_state(_Nick, _GuiName) -> _InitialClientState.
-spec loop(_State, _Request, _Ref) -> _.
-spec do_join(_State, _Ref, _ChatName) -> _.
-spec do_leave(_State, _Ref, _ChatName) -> _.
-spec do_new_nick(_State, _Ref, _NewNick) -> _.
-spec do_new_incoming_msg(_State, _Ref, _SenderNick, _ChatName, _Message) -> _.

%% Receive messages from GUI and handle them accordingly
%% All handling can be done in loop(...)
main(InitialState) ->
    %% The client tells the server it is connecting with its initial nickname.
    %% This nickname is guaranteed unique system-wide as long as you do not assign a client
    %% the nickname in the form "user[number]" manually such that a new client happens
    %% to generate the same random number as you assigned to your client.
    whereis(server)!{self(), connect, InitialState#cl_st.nick},
    %% if running test suite, tell test suite that client is up
    case whereis(testsuite) of
	undefined -> ok;
	TestSuitePID -> TestSuitePID!{client_up, self()}
    end,
    %% Begins listening
    listen(InitialState).

%% This method handles all incoming messages from either the GUI or the
%% chatrooms that are not directly tied to an ongoing request cycle.
listen(State) ->
    receive
        {request, From, Ref, Request} ->
	    %% the loop method will return a response as well as an updated
	    %% state to pass along to the next cycle
            {Response, NextState} = loop(State, Request, Ref),
	    case Response of
		{dummy_target, Resp} ->
		    io:format("Use this for whatever you would like~n"),
		    From!{result, self(), Ref, {dummy_target, Resp}},
		    listen(NextState);
		%% if shutdown is received, terminate
		shutdown ->
		    ok_shutdown;
		%% do nothing if GUI receives a quit instruction
		ack_quit ->
		    From!{self(), Ref, ack_quit},
		    listen(NextState);
		%% if ok_msg_received, then we don't need to reply to sender.
		ok_msg_received ->
		    listen(NextState);
		%% otherwise, reply to sender with response
		_ ->
		    From!{result, self(), Ref, Response},
		    listen(NextState)
	    end
    end.

%% This function just initializes the default state of a client.
%% This should only be used by the GUI. Do not change it, as the
%% GUI code we provide depends on it.
initial_state(Nick, GUIName) ->
    #cl_st { gui = GUIName, nick = Nick, con_ch = maps:new() }.

%% ------------------------------------------
%% loop handles each kind of request from GUI
%% ------------------------------------------
loop(State, Request, Ref) ->
    case Request of
	%% GUI requests to join a chatroom with name ChatName
	{join, ChatName} ->
	    do_join(State, Ref, ChatName);

	%% GUI requests to leave a chatroom with name ChatName
	{leave, ChatName} ->
	    do_leave(State, Ref, ChatName);

	%% GUI requests to send an outgoing message Message to chatroom ChatName
	{outgoing_msg, ChatName, Message} ->
	    do_msg_send(State, Ref, ChatName, Message);

	%% GUI requests the nickname of client
	whoami ->
        % - should this be done here? If not, where else? - 
        GuiPid = whereis(list_to_atom(State#cl_st.gui)),
        CurrentNick = State#cl_st.nick,
        
        GuiPid!{result, self(), Ref, CurrentNick},
        % - end sus location-
	    {{dummy_target, dummy_response}, State};

	%% GUI requests to update nickname to Nick
	{nick, Nick} ->
            do_new_nick(State, Ref, Nick);

	%% GUI requesting to quit completely
	quit ->
	    do_quit(State, Ref);

	%% Chatroom with name ChatName has sent an incoming message Message
	%% from sender with nickname SenderNick
	{incoming_msg, SenderNick, ChatName, Message} ->
	    do_new_incoming_msg(State, Ref, SenderNick, ChatName, Message);

	{get_state} ->
	    {{get_state, State}, State};

	%% Somehow reached a state where we have an unhandled request.
	%% Without bugs, this should never be reached.
	_ ->
	    io:format("Client: Unhandled Request: ~w~n", [Request]),
	    {unhandled_request, State}
    end.

%% executes `/join` protocol from client perspective
do_join(State, Ref, ChatName) ->
    GuiPid = whereis(list_to_atom(State#cl_st.gui)),
    ConnectedChats = State#cl_st.con_ch,

    case maps:get(ChatName, ConnectedChats, error) of
        error ->
            % User wants to attempt to join a chatroom
            Server = whereis(server),
            Server!{self(), Ref, join, ChatName},

            receive
                {ChatPid, IncomingRef, connect, History} ->
                    GuiPid!{result, self(), IncomingRef, History},

                    {{dummy_target, dummy_response}, % Update connected chats 
                     State#cl_st{con_ch = maps:put(ChatName, ChatPid, ConnectedChats)}} 
            end;
        _ ->
            % Error, already connected to the specified chatroom
            GuiPid!{result, self(), Ref, err},
            {{dummy_target, dummy_response}, State}
    end.


%% executes `/leave` protocol from client perspective
do_leave(State, Ref, ChatName) ->
    GuiPid = whereis(list_to_atom(State#cl_st.gui)),
    ConnectedChats = State#cl_st.con_ch,

    case maps:get(ChatName, ConnectedChats, error) of
        error -> % User is not in chatroom, error.
            GuiPid!{result, self(), Ref, err},
            {{dummy_target, dummy_response}, State};
            % Note, this will also proc if they typed it in the System tab, not sure if anything can be done about this

        _ ->
            Server = whereis(server),
            Server!{self(), Ref, leave, ChatName},

            receive
                {_ServerPid, IncomingRef, ack_leave} ->
                    GuiPid!{result, self(), IncomingRef, ok}, % Left successfully

                    % Update state to remove the chatroom from connected chats.
                    {{dummy_target, dummy_response}, 
                     State#cl_st{con_ch = maps:remove(ChatName, ConnectedChats)}}
            end
    end.


%% executes `/nick` protocol from client perspective
do_new_nick(State, Ref, NewNick) ->
    GuiPid = whereis(list_to_atom(State#cl_st.gui)),
    CurrentNick = State#cl_st.nick,
   
    case CurrentNick of
        NewNick -> % If the user's desired nickname is the same as the current one, notify user/give them an error message
            GuiPid!{result, self(), Ref, err_same},
            {{dummy_target, dummy_response}, State};
        _ ->
            
            ServerPid = whereis(server),
            ServerPid!{self(), Ref, nick, NewNick},
            
            receive
                {ServerPid, Ref, err_nick_used} -> % Nick already taken
                    GuiPid!{result, self(), Ref, err_nick_used},
                    {{dummy_target, dummy_response}, State};
                {ServerPid, Ref, ok_nick} -> % Nick successfully changed
                    GuiPid!{result, self(), Ref, ok_nick},
                    {{dummy_target, dummy_response}, 
                     State#cl_st{nick = NewNick}}
            end
    end.

%% executes send message protocol from client perspective.
do_msg_send(State, Ref, ChatName, Message) ->
    GuiPid = whereis(list_to_atom(State#cl_st.gui)),
    ConnectedChats = State#cl_st.con_ch,

    % Find the chatroom PID from the connected chats map
    case maps:get(ChatName, ConnectedChats, undefined) of
        undefined ->
            % This should literally not be possible (would require the user sending a message to a chat room that doesn't exist / isn't registered, 
            % but maybe if the server dies and the clients/GUIs try typing in the chatroom it could proc
            GuiPid!{result, self(), Ref, {error, "Connection to the chatroom " ++ ChatName ++ " has been severed. The message was not sent."}},
            {{dummy_target, dummy_response}, State};

        ChatPid ->
            % Send the message to the chatroom
            ChatPid!{self(), Ref, message, Message},

            receive
                {ChatPid, IncomingRef, ack_msg} ->
                    % Push the message to the GUI so the client actually sees the message
                    GuiPid!{result, self(), IncomingRef, {msg_sent, State#cl_st.nick}}
            end,
            {{dummy_target, dummy_response}, State}
    end.

%% executes new incoming message protocol from client perspective
do_new_incoming_msg(State, _Ref, CliNick, ChatName, Msg) ->
    %% pass message along to gui
    gen_server:call(list_to_atom(State#cl_st.gui), {msg_to_GUI, ChatName, CliNick, Msg}),
    {ok_msg_received, State}.

%% executes quit protocol from client perspective
do_quit(ClientState, RequestRef) ->
    Server = whereis(server),
    GuiPid = whereis(list_to_atom(ClientState#cl_st.gui)),

    % Send quit request to the server as user wants to quit
    Server!{self(), RequestRef, quit},

    % Wait until server acknowledges the request
    receive
        {Server, RequestRef, ack_quit} ->
            % Tell the user that they have quit successfully (pdf mentions that this may not work due to GUI issues*)
            GuiPid!{self(), RequestRef, ack_quit}
    end,

    {shutdown, ClientState}.