%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2012. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%

%%
%% ------------------------------------------------------------
%%
%% This is a driver for the 'gstk' application modified to 
%% handle events for gs. 'gstk' is a modified standalone wish.
%% 
%% FIXME
%% mkdir tcl ; cd tcl
%% ( cd /usr/local/pgm/tcl-8.3.3 ; tar -cf - * ) | tar -xf -
%% ( cd /usr/local/pgm/tk-8.3.3 ; tar -cf - * ) | tar -xf -
%% rm -fr include man bin/tclsh
%% cd ..
%% tar -cf tcl.tar *
%%
%% ------------------------------------------------------------

-module(gstk_port_handler).
-compile([{nowarn_deprecated_function,{gs,error,2}}]).

-include("gstk.hrl").

% The executable can have many names. There is not always
% a plain "wish" program. 
% FIXME There has to be a better solution....
% FIXME Add option in app file or environmen  variable.

-define(WISHNAMES, ["wish85","wish8.5",
		    "wish84","wish8.4",
		    "wish83","wish8.3",
		    "wish82","wish8.2",
		    "wish"]).

%% ------------------------------------------------------------
%%                  DEBUG FUNCTIONS 
%% ------------------------------------------------------------
-export([exec/1,call/2,
	 start_link/1,init/2,ping/1,stop/1]).
-export([wait_for_connection/2]).

-define(START_TIMEOUT , 1000 * 30).
-define(ACCEPT_TIMEOUT, 1000 * 20).

-define(DEBUGLEVEL, 4).

-ifdef(DEBUG).

-define(DBG(DbgLvl,Format, Data),dbg(DbgLvl, Format, Data)).
-define(DBG_STR(DbgLvl, What, Str),dbg_str(DbgLvl, What, Str)).

dbg(DbgLvl, Format, Data) when DbgLvl =< ?DEBUGLEVEL ->
    ok = io:format("DBG: " ++ Format, Data);
dbg(_DbgLvl, _Format, _Data) -> ok.

dbg_str(DbgLvl, What, Str) when DbgLvl =< ?DEBUGLEVEL ->
    ok = io:format("DBG: ~s~s\n", [What,dbg_s(Str)]);
dbg_str(_DbgLvl, _What, _Data) -> ok.

dbg_s([]) ->
    [];
dbg_s([C | Str]) when list(C) ->
    [dbg_s(C) | dbg_s(Str)];
dbg_s([C | Str]) when C >= 20, C < 255 ->
    [C | dbg_s(Str)];
dbg_s([$\n | Str]) ->
    ["\\n" | dbg_s(Str)];
dbg_s([$\r | Str]) ->
    ["\\r" | dbg_s(Str)];
dbg_s([$\t | Str]) ->
    ["\\t" | dbg_s(Str)];
dbg_s([C | Str]) when integer(C) ->
    [io_lib:format("\\~.3.0w",[C]) | dbg_s(Str)].

-else.

-define(DBG(DbgLvl,Format, Data), true).
-define(DBG_STR(DbgLvl, What, Str), true).

-endif.

%% ------------------------------------------------------------
%%                  INTERFACE FUNCTIONS 
%% ------------------------------------------------------------

% Note: gs is not a true application so this doesn't work :-(
% Communication protocol between Erlang backend and wish program
% that can be set in the application environment, e.i. tested
% with "erl -gs backend_comm socket"
%
%   backend_comm = socket | port
%
% We fake reading the application variables from the command line.
% Note that multiple -gs arguments can't be used.

get_env(App, KeyAtom) ->
    KeyStr = atom_to_list(KeyAtom),
    ?DBG(1,"Result from init:get_argument(~w): ~p\n",
	[KeyAtom,init:get_argument(App)]),
    case init:get_argument(App) of
	{ok,[[KeyStr,ValStr]]} ->
	    {ok,list_to_atom(ValStr)};
	_ ->
	    undefined
    end.

start_link(Gstk) ->
    ?DBG(1, "start_link(~w)~n", [Gstk]),
%    io:format("STARTS ~p\n",[erlang:localtime()]),
    Mode = 
	% FIXME: Want to use application:get_env() if we where an true app
	case {os:type(),get_env(gs,backend_comm)} of
	    {{win32,_Flavor},undefined} ->
		use_socket;
	    {_OS,undefined} ->
		use_port;
	    {_OS,{ok,socket}} ->
		use_socket;
	    {_OS,{ok,port}} ->
		use_port
	end,
    ?DBG(1,"We use mode: ~w (~w)\n",[Mode,get_env(gs,backend_comm)]),
    Pid = spawn_link(gstk_port_handler, init, [Gstk,Mode]),
    receive
	{Pid, ok} ->
	    {ok, Pid};
	{Pid, error, Reason} ->
	    {error, Reason}
    after ?START_TIMEOUT ->
	    {error, timeout}
    end.

call(PortHandler, Cmd) ->
    PortHandler ! {call, ["erlcall {",Cmd,$}]},
    receive
        {result, Result}         -> 
            ?DBG(1, "call reply: ~p~n", [Result]),
            {result,     Result};
        {bad_result, Bad_Result} ->
            ?DBG(1, "bad call reply: ~p~n", [Bad_Result]),
            {bad_result, Bad_Result}
    end.

ping(PortHandler) ->
    ?DBG(1, "ping~n", []),
    PortHandler ! {ping, self()},
    receive
	{pong,_From,PortOrSock} -> {ok,PortOrSock}
    end.

stop(PortHandler) ->
    ?DBG(1, "stop~n", []),
    PortHandler ! {stop,self()},
    receive
	{stopped,PortHandler} -> ok
    end.

%% Purpose: asyncron call to tk 
%% too expensive 
% FIXME   
exec(Cmd) ->
    get(port_handler) ! {exec, ["erlexec {",Cmd,$}]},
    ok.

% in gstk context, but I don't want "ifndef nt40" in other 
% modules than this one.
%exec(Cmd) ->
%    ?DBG_STR(1, "", ["erlexec {",Cmd,"}"]),
%    case get(port) of
%	{socket,Sock} ->
%	    gen_tcp:send(Sock, ["erlexec {",Cmd,$}]);
%	{port,Port} ->
%	    Port ! {get(port_handler),{command,["erlexec {",Cmd,$}]}}
%    end,
%    ok.

%% ===========================================================================
%% The server
%% ===========================================================================

%% ---------------------------------------------------------------------
%% We initiate by starting the wish port program and use the pipe
%% or a socket to communicate with it.
%%
%% gstk: is the pid of the gstk process that started me. 
%%      all my input (from the port) is forwarded to it.
%%----------------------------------------------------------------------
-record(state,{out,gstk}).

init(Gstk, Mode) ->
    process_flag(trap_exit,true),

    % ------------------------------------------------------------
    % Set up paths
    % ------------------------------------------------------------

    PrivDir = code:priv_dir(gs),
    TclDir = filename:join(PrivDir,"tcl"),
    TclBinDir = filename:join(TclDir,"bin"),
    TclLibDir = filename:join(TclDir,"lib"),

    InitScript = filename:nativename(filename:join(PrivDir,"gstk.tcl")),

    ?DBG(1, "TclBinDir  : ~s\n", [TclBinDir]),
    ?DBG(1, "TclLibDir  : ~s\n", [TclLibDir]),
    ?DBG(1, "InitScript : ~s\n", [InitScript]),

    % ------------------------------------------------------------
    % Search for wish in priv and in system search path
    % ------------------------------------------------------------

    {Wish,Options} = 
	case filelib:wildcard(filename:join(TclBinDir,"wish*")) of
	    % If more than one wish in priv we assume they are the same
	    [PrivWish | _] ->
		% ------------------------------------------------
		% We have to set TCL_LIBRARY and TK_LIBRARY because else
		% 'wish' will search in the original installation directory
		% for 'tclIndex' and this may be an incompatible version on
		% the host we run on.
		% ------------------------------------------------

		[TclLibrary] =
		    filelib:wildcard(filename:join(PrivDir,
						   "tcl/lib/tcl[1-9]*")),
		[TkLibrary]  =
		    filelib:wildcard(filename:join(PrivDir,
						   "tcl/lib/tk[1-9]*")),

		Opts = [{env,[{"TCL_LIBRARY", TclLibrary},
			      {"TK_LIBRARY", TkLibrary},
			      {"LD_LIBRARY_PATH",TclLibDir}]},
		       {packet,4}],
		{PrivWish,Opts};
	    _ ->
		% We use the system wish program
		{search_wish(?WISHNAMES, Gstk),[{packet,4}]}
	end,


    ?DBG(1, "Wish       : ~s\n", [Wish]),

    Cmd =
	case Mode of
	    use_socket ->
		% ------------------------------------------------------------
		% Set up a listening socket and call accept in another process
		% ------------------------------------------------------------
		SocketOpts =
		    [
		     {nodelay, true},
		     {packet,4},
		     {reuseaddr,true}
		    ],
		% Let OS pick a number
		{ok,ListenSocket} = gen_tcp:listen(0, SocketOpts),
		{ok,ListenPort} = inet:port(ListenSocket),

		% Wait in another process
		spawn_link(?MODULE,wait_for_connection,[self(),ListenSocket]),
		lists:concat([Wish," ",InitScript," -- ",PrivDir," ",
			      ListenPort]);
	    use_port ->
		lists:concat([Wish," ",InitScript," -- ",PrivDir])
	end,

    ?DBG(1, "Port opts  :\n~p\n", [Options]),

    % FIXME remove timing if not debugging
    Port =
	case timer:tc(erlang,open_port,[{spawn, Cmd}, Options]) of
	    {_T,Port1} when is_port(Port1) ->
		?DBG(1,"open_port takes ~p milliseconds\n",[_T/1000]),
		link(Port1),
		Port1;
	    {_T,{error,_Reason1}} ->		% FIXME: Why throw away reason?!
		?DBG(1,"ERROR: ~p\n",[_Reason1]),
		Gstk ! {self(), error, backend_died},
		exit(normal)
	end,

    State =
	case Mode of
	    use_socket ->
		% ------------------------------------------------------------
		% Wait for a connection
		% ------------------------------------------------------------
		Sock =
		    receive
			{connected,Socket} ->
			    Socket;
			% FIXME: Why throw away reason?!
			{'EXIT', _Pid, _Reason2} ->
			    Gstk ! {self(), error, backend_died},
			    exit(normal)
		    end,

		?DBG(1,"Got socket ~p~n",[Sock]),
		#state{out={socket,Sock}, gstk=Gstk};
	    use_port ->
		#state{out={port,Port}, gstk=Gstk}
	end,

    Gstk ! {self(), ok},			% Tell caller we are prepared
    idle(State).

search_wish([], Gstk) ->
    Gstk ! {self(), error, backend_died},
    exit(normal);
search_wish([WishName | WishNames], Gstk) ->
    case os:find_executable(WishName) of
	false ->
	    search_wish(WishNames, Gstk);
	Wish ->
	    Wish
    end.
	
%%----------------------------------------------------------------------
%% If we use sockets we wait for connection from port prog
%%----------------------------------------------------------------------

wait_for_connection(CallerPid, ListenSocket) ->
    {ok,Sock} = gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT),
    ?DBG(1,"Got accept ~p~p~n",[self(),Sock]),
    ok = gen_tcp:controlling_process(Sock,CallerPid),
    CallerPid ! {connected,Sock}.

%% ===========================================================================
%% The main loop
%% ===========================================================================

idle(State) ->
    ?DBG(1, "idle~n", []),
%    io:format("IDLE ~p\n",[erlang:localtime()]),
    receive

	{call, Cmd} ->
	    output(State, Cmd),
	    idle(State);

	{exec, Cmd} ->
	    collect_exec_calls(Cmd, [], 0, State),
	    idle(State);

	{_Port, {data, Input}} ->
	    ?DBG_STR(2, "INPUT[port]: ", [Input]),
	    handle_input(State, Input),
	    idle(State);

	{tcp, _Sock, Input} ->
	    ?DBG_STR(2, "INPUT[sock]: ", [Input]),
	    handle_input(State, Input),
	    idle(State);

	{ping,From} ->
	    From ! {pong,self(),State#state.out},
	    idle(State);

	{stop,From} ->
	    From ! {stopped,self()};

	% FIXME: We are we not to terminate if watforsocket
	% terminated but what about the port???????
	{'EXIT',_Pid,normal} ->
	    ?DBG(1, "EXIT[~w]: normal~n", [_Pid]),
	    idle(State);

	{'EXIT',Pid,Reason} ->
	    %%io:format("Port died when in idle loop!~n"),
	    ?DBG(1,"EXIT[~w]~n~p~n",[Pid,Reason]),
	    exit({port_handler,Pid,Reason});

	Other ->
	    ?DBG(1,"OTHER: ~p~n",[Other]),
	    gs:error("gstk_port_handler: got other: ~w~n",[Other]),
	    idle(State)
    end.

%% ----------------------------------------------------------------------

-define(MAXQUEUE, 4).				% FIXME find value...

collect_exec_calls(Cmd, Queue, QueueLen, State) when QueueLen < ?MAXQUEUE ->
    receive
	{exec, NewCmd} ->
%	    io:format("collect~p~n", [NewCmd]),
	    collect_exec_calls(NewCmd, [Cmd | Queue], QueueLen+1, State)
    after 0 ->
	    if
		QueueLen == 0 ->
		    output(State, Cmd);
		true ->
		    output(State, join_cmd_reverse(Cmd, Queue, []))
	    end
    end;
collect_exec_calls(Cmd, Queue, _QueueLen, State) -> % Queue is full, output
    String = join_cmd_reverse(Cmd, Queue, []),
%    io:format("queue full: ~p~n", [String]),
    output(State, String).


join_cmd_reverse(Cmd, [], DeepStr) ->
    [DeepStr | Cmd];
join_cmd_reverse(Cmd, [Cmd1 | Cmds], DeepStr) ->
    join_cmd_reverse(Cmd, Cmds, [Cmd1,$; | DeepStr]).

%% ----------------------------------------------------------------------
%%
%% Handle incoming data
%% 1 - Event
%% 2 - Reply from call
%% 3 - Bad reply from call
%% 4 - Error
%% 5 - End of message
%% 

handle_input(State,[Type | Data]) ->
    GstkPid = State#state.gstk,
    case Type of
	1 ->
	    handle_event(GstkPid,Data);

	2 ->
	    GstkPid ! {result, Data};

	3 ->
	    GstkPid ! {bad_result, Data};

	4 ->
	    gs:error("gstk_port_handler: error in input : ~s~n",[Data])
    end.

%% ----------------------------------------------------------------------
%% output a command to the port
%% buffer several incoming execs
%%
output(#state{out = {socket,Sock}}, Cmd) ->
    ?DBG_STR(1, "OUTPUT[sock]: ", [Cmd]),
    ok = gen_tcp:send(Sock, Cmd);

output(#state{out = {port,Port}}, Cmd) ->
    ?DBG_STR(1, "OUTPUT[port]: ", [Cmd]),
    Port ! {self(), {command, Cmd}}.

% FIXME why test list?
handle_event(GstkPid, Bytes) when is_list(Bytes) ->
    Event = tcl2erl:parse_event(Bytes),
    ?DBG(1,"Event = ~p\n",[Event]),
    gstk:event(GstkPid, Event). %% Event is {ID, Etag, Args}