-module(ktn_io_string).
-export([new/1]).
-export([start_link/1, init/1, loop/1, skip/2, skip/3]).
-type state() :: #{buffer := string(), original := string()}.
-export_type([state/0]).
-hank([{unnecessary_function_arguments, [{skip, 3}]}]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
-spec new(string() | binary()) -> pid().
new(Str) when is_binary(Str) ->
new(binary_to_list(Str));
new(Str) ->
start_link(Str).
%%------------------------------------------------------------------------------
%% IO server
%%
%% Implementation of a subset of the io protocol in order to only support
%% reading operations.
%%------------------------------------------------------------------------------
-spec start_link(string()) -> pid().
start_link(Str) ->
spawn_link(?MODULE, init, [Str]).
-spec init(string()) -> ok.
init(Str) ->
State = #{buffer => Str, original => Str},
?MODULE:loop(State).
-spec loop(state()) -> ok.
loop(#{buffer := Str} = State) ->
receive
{io_request, From, ReplyAs, Request} ->
{Reply, NewStr} = request(Request, Str),
_ = reply(From, ReplyAs, Reply),
?MODULE:loop(State#{buffer := NewStr});
{file_request, From, Ref, close} ->
file_reply(From, Ref, ok);
{file_request, From, Ref, {position, Pos}} ->
{Reply, NewState} = file_position(Pos, State),
_ = file_reply(From, Ref, Reply),
?MODULE:loop(NewState);
_Unknown ->
?MODULE:loop(State)
end.
-spec reply(pid(), pid(), any()) -> any().
reply(From, ReplyAs, Reply) ->
From ! {io_reply, ReplyAs, Reply}.
-spec file_reply(pid(), pid(), any()) -> any().
file_reply(From, ReplyAs, Reply) ->
From ! {file_reply, ReplyAs, Reply}.
-spec file_position(integer(), state()) -> {any(), state()}.
file_position(Pos, #{original := Original} = State) ->
Buffer = lists:nthtail(Pos, Original),
{{ok, Pos}, State#{buffer => Buffer}}.
-spec request(any(), string()) -> {string() | {error, request}, string()}.
request({get_chars, _Encoding, _Prompt, N}, Str) ->
get_chars(N, Str);
request({get_line, _Encoding, _Prompt}, Str) ->
get_line(Str);
request({get_until, _Encoding, _Prompt, Module, Function, XArgs}, Str) ->
get_until(Module, Function, XArgs, Str);
request(_Other, State) ->
{{error, request}, State}.
-spec get_chars(integer(), string()) -> {string() | eof, string()}.
get_chars(_N, []) ->
{eof, []};
get_chars(1, [Ch | Str]) ->
{[Ch], Str};
get_chars(N, Str) ->
do_get_chars(N, Str, []).
-spec do_get_chars(integer(), string(), string()) -> {string(), string()}.
do_get_chars(0, Str, Result) ->
{lists:flatten(Result), Str};
do_get_chars(_N, [], Result) ->
{Result, []};
do_get_chars(N, [Ch | NewStr], Result) ->
do_get_chars(N - 1, NewStr, [Result, Ch]).
-spec get_line(string()) -> {string() | eof, string()}.
get_line([]) ->
{eof, []};
get_line(Str) ->
do_get_line(Str, []).
-spec do_get_line(string(), string()) -> {string() | eof, string()}.
do_get_line([], Result) ->
{lists:flatten(Result), []};
do_get_line("\r\n" ++ RestStr, Result) ->
{lists:flatten(Result), RestStr};
do_get_line("\n" ++ RestStr, Result) ->
{lists:flatten(Result), RestStr};
do_get_line("\r" ++ RestStr, Result) ->
{lists:flatten(Result), RestStr};
do_get_line([Ch | RestStr], Result) ->
do_get_line(RestStr, [Result, Ch]).
-spec get_until(module(), atom(), list(), eof | string()) -> {term(), string()}.
get_until(Module, Function, XArgs, Str) ->
apply_get_until(Module, Function, [], Str, XArgs).
-spec apply_get_until(module(), atom(), any(), string() | eof, list()) ->
{term(), string()}.
apply_get_until(Module, Function, State, String, XArgs) ->
case apply(Module, Function, [State, String | XArgs]) of
{done, Result, NewStr} ->
{Result, NewStr};
{more, NewState} ->
apply_get_until(Module, Function, NewState, eof, XArgs)
end.
-spec skip(string() | {cont, integer(), string()}, term(), integer()) ->
{more, {cont, integer(), string()}} | {done, integer(), string()}.
skip(Str, _Data, Length) ->
skip(Str, Length).
-spec skip(string() | {cont, integer(), string()}, integer()) ->
{more, {cont, integer(), string()}} | {done, integer(), string()}.
skip(Str, Length) when is_list(Str) ->
{more, {cont, Length, Str}};
skip({cont, 0, Str}, Length) ->
{done, Length, Str};
skip({cont, Length, []}, Length) ->
{done, eof, []};
skip({cont, Length, [_ | RestStr]}, _Length) ->
{more, {cont, Length - 1, RestStr}}.