%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2017-2022 VMware, Inc. or its affiliates.  All rights reserved.
%%
-module(ra_log_reader).

-compile(inline_list_funcs).

-export([
         init/6,
         init/8,
         close/1,
         update_segments/2,
         handle_log_update/2,
         segment_refs/1,
         num_open_segments/1,
         update_first_index/2,
         fold/5,
         sparse_read/3,
         fetch_term/2,
         delete_closed_mem_table_object/2,
         closed_mem_tables/1,
         open_mem_table_lookup/1,
         range_overlap/4
         ]).

-include("ra.hrl").

-define(STATE, ?MODULE).

-type access_pattern() :: sequential | random.
%% holds static or rarely changing fields
-record(cfg, {uid :: ra_uid(),
              counter :: undefined | counters:counters_ref(),
              directory :: file:filename(),
              open_mem_tbls :: ets:tid(),
              closed_mem_tbls :: ets:tid(),
              access_pattern = random :: access_pattern()
             }).

-type segment_ref() :: {From :: ra_index(), To :: ra_index(),
                        File :: string()}.
-record(?STATE, {cfg :: #cfg{},
                 first_index = 0 :: ra_index(),
                 segment_refs = [] :: [segment_ref()],
                 open_segments :: ra_flru:state()
                }).

-opaque state() :: #?STATE{}.


-export_type([
              state/0
              ]).

%% PUBLIC

-spec init(ra_uid(), file:filename(), ra_index(), non_neg_integer(),
           [segment_ref()], ra_system:names()) -> state().
init(UId, Dir, FirstIdx, MaxOpen, SegRefs, Names) ->
    init(UId, Dir, FirstIdx, MaxOpen, random, SegRefs, Names, undefined).

-spec init(ra_uid(), file:filename(), ra_index(), non_neg_integer(),
           access_pattern(),
           [segment_ref()], ra_system:names(),
           undefined | counters:counters_ref()) -> state().
init(UId, Dir, FirstIdx, MaxOpen, AccessPattern, SegRefs,
     #{open_mem_tbls := OpnMemTbls,
       closed_mem_tbls := ClsdMemTbls}, Counter)
  when is_binary(UId) ->
    Cfg = #cfg{uid = UId,
               counter = Counter,
               directory = Dir,
               open_mem_tbls = ets:whereis(OpnMemTbls),
               closed_mem_tbls = ets:whereis(ClsdMemTbls),
               access_pattern = AccessPattern},
    FlruHandler = fun ({_, Seg}) ->
                          _ = ra_log_segment:close(Seg),
                          decr_counter(Cfg, ?C_RA_LOG_OPEN_SEGMENTS, 1)
                  end,
    #?STATE{cfg = Cfg,
            open_segments = ra_flru:new(MaxOpen, FlruHandler),
            first_index = FirstIdx,
            segment_refs = SegRefs}.

-spec close(state()) -> ok.
close(#?STATE{open_segments = Open}) ->
    _ = ra_flru:evict_all(Open),
    ok.

-spec update_segments([segment_ref()], state()) -> state().
update_segments(NewSegmentRefs,
                #?STATE{open_segments = Open0,
                        segment_refs = SegmentRefs0} = State) ->
    SegmentRefs = compact_seg_refs(NewSegmentRefs, SegmentRefs0),
    %% check if any of the updated segrefs refer to open segments
    %% we close these segments so that they can be re-opened with updated
    %% indexes if needed
    Open = lists:foldl(fun ({_, _, F}, Acc0) ->
                               case ra_flru:evict(F, Acc0) of
                                   {_, Acc} -> Acc;
                                   error -> Acc0
                               end
                       end, Open0, SegmentRefs),
    State#?MODULE{segment_refs = SegmentRefs,
                  open_segments = Open}.

-spec handle_log_update({ra_log_update, undefined | pid(), ra_index(),
                         [segment_ref()]}, state()) -> state().
handle_log_update({ra_log_update, From, FstIdx, SegRefs},
                  #?STATE{open_segments = Open0} = State) ->
    Open = ra_flru:evict_all(Open0),
    case From of
        undefined -> ok;
        _ ->
            %% reply to the updater process
            From ! ra_log_update_processed
    end,
    State#?MODULE{segment_refs = SegRefs,
                  first_index = FstIdx,
                  open_segments = Open}.

-spec update_first_index(ra_index(), state()) ->
    {state(), [segment_ref()]}.
update_first_index(FstIdx, #?STATE{segment_refs = SegRefs0,
                                   open_segments = OpenSegs0} = State) ->
    case lists:partition(fun({_, To, _})
                               when To >= FstIdx -> true;
                            (_) -> false
                         end, SegRefs0) of
        {_, []} ->
            {State, []};
        {Active, Obsolete} ->
            ObsoleteKeys = [element(3, O) || O <- Obsolete],
            % close any open segments
            OpenSegs = lists:foldl(fun (K, OS0) ->
                                           case ra_flru:evict(K, OS0) of
                                               {_, OS} -> OS;
                                               error -> OS0
                                           end
                                   end, OpenSegs0, ObsoleteKeys),
            {State#?STATE{open_segments = OpenSegs,
                          first_index = FstIdx,
                          segment_refs = Active},
             Obsolete}
    end.

-spec segment_refs(state()) -> [segment_ref()].
segment_refs(#?STATE{segment_refs = SegmentRefs}) ->
    SegmentRefs.

-spec num_open_segments(state()) -> non_neg_integer().
num_open_segments(#?STATE{open_segments = Open}) ->
     ra_flru:size(Open).

mem_tbl_fold(_Tid, From, To, _Fun, Acc)
  when From > To ->
    Acc;
mem_tbl_fold(Tid, From, To, Fun, Acc0) ->
    [Entry] = ets:lookup(Tid, From),
    Acc = Fun(Entry, Acc0),
    mem_tbl_fold(Tid, From+1, To, Fun, Acc).


-spec fold(ra_index(), ra_index(), fun(), term(), state()) ->
    {state(), term()}.
fold(FromIdx, ToIdx, Fun, Acc,
    #?STATE{cfg = #cfg{} = Cfg} = State)
  when ToIdx >= FromIdx ->
    Plan = read_plan(Cfg, FromIdx, ToIdx),
    lists:foldl(
      fun ({ets, Tid, CIx, From, To}, {S, Ac}) ->
              ok = incr_counter(Cfg,  CIx, To - From + 1),
              {S, mem_tbl_fold(Tid, From, To, Fun, Ac)};
          ({segments, From, To}, {S, Ac}) ->
              ok = incr_counter(Cfg, ?C_RA_LOG_READ_SEGMENT, To - From + 1),
              segment_fold(S, From, To, Fun, Ac)
      end, {State, Acc}, Plan);
fold(_FromIdx, _ToIdx, _Fun, Acc,
     #?STATE{} = State) ->
    {State, Acc}.

-spec sparse_read(state(), [ra_index()], [log_entry()]) ->
    {[log_entry()], state()}.
sparse_read(#?STATE{cfg = #cfg{} = Cfg} = State, Indexes0, Entries0) ->
    try open_mem_tbl_sparse_read(Cfg, Indexes0, Entries0) of
        {Entries1, OpenC, []} ->
            ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPEN_MEM_TBL, OpenC),
            {Entries1, State};
        {Entries1, OpenC, Rem1} ->
            ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPEN_MEM_TBL, OpenC),
            try closed_mem_tbl_sparse_read(Cfg, Rem1, Entries1) of
                {Entries2, ClosedC, []} ->
                    ok = incr_counter(Cfg, ?C_RA_LOG_READ_CLOSED_MEM_TBL, ClosedC),
                    {Entries2, State};
                {Entries2, ClosedC, Rem2} ->
                    ok = incr_counter(Cfg, ?C_RA_LOG_READ_CLOSED_MEM_TBL, ClosedC),
                    {Open, _, SegC, Entries} = (catch segment_sparse_read(State, Rem2, Entries2)),
                    ok = incr_counter(Cfg, ?C_RA_LOG_READ_SEGMENT, SegC),
                    {Entries, State#?MODULE{open_segments = Open}}
            catch _:_ ->
                      sparse_read(State, Indexes0, Entries0)
            end
    catch _:_ ->
              %% table was most likely concurrently deleted
              %% try again
              %% TODO: avoid infinite loop
              sparse_read(State, Indexes0, Entries0)
    end.

-spec fetch_term(ra_index(), state()) -> {ra_index(), state()}.
fetch_term(Idx, #?STATE{cfg = #cfg{uid = UId,
                                   open_mem_tbls = OpenTbl,
                                   closed_mem_tbls = ClosedTbl} = Cfg} = State0) ->
    incr_counter(Cfg, {?C_RA_LOG_FETCH_TERM, 1}),
    case ets:lookup(OpenTbl, UId) of
        [{_, From, To, Tid}] when Idx >= From andalso Idx =< To ->
            Term = ets:lookup_element(Tid, Idx, 2),
            {Term, State0};
        _ ->
            case closed_mem_table_term_query(ClosedTbl, Idx, UId) of
                undefined ->
                    segment_term_query(Idx, State0);
                Term ->
                    {Term, State0}
            end
    end.

-spec delete_closed_mem_table_object(state(), term()) -> true.
delete_closed_mem_table_object(#?STATE{cfg =
                                       #cfg{closed_mem_tbls = Tbl}}, Id) ->
    true = ets:delete_object(Tbl, Id).

-spec closed_mem_tables(state()) -> list().
closed_mem_tables(#?STATE{cfg = #cfg{uid = UId,
                                     closed_mem_tbls = Tbl}}) ->
    closed_mem_tables(Tbl, UId).

-spec open_mem_table_lookup(state()) -> list().
open_mem_table_lookup(#?STATE{cfg = #cfg{uid = UId,
                                         open_mem_tbls = Tbl}}) ->
    ets:lookup(Tbl, UId).


%% LOCAL

segment_term_query(Idx, #?MODULE{segment_refs = SegRefs,
                                 cfg = Cfg,
                                 open_segments = OpenSegs} = State) ->
    {Result, Open} = segment_term_query0(Idx, SegRefs, OpenSegs, Cfg),
    {Result, State#?MODULE{open_segments = Open}}.

segment_term_query0(Idx, [{From, To, Filename} | _], Open0,
                    #cfg{directory = Dir,
                         access_pattern = AccessPattern} = Cfg)
  when Idx >= From andalso Idx =< To ->
    case ra_flru:fetch(Filename, Open0) of
        {ok, Seg, Open} ->
            Term = ra_log_segment:term_query(Seg, Idx),
            {Term, Open};
        error ->
            AbsFn = filename:join(Dir, Filename),
            {ok, Seg} = ra_log_segment:open(AbsFn,
                                            #{mode => read,
                                              access_pattern => AccessPattern}),

            incr_counter(Cfg, ?C_RA_LOG_OPEN_SEGMENTS, 1),
            Term = ra_log_segment:term_query(Seg, Idx),
            {Term, ra_flru:insert(Filename, Seg, Open0)}
    end;
segment_term_query0(Idx, [_ | Tail], Open, Cfg) ->
    segment_term_query0(Idx, Tail, Open, Cfg);
segment_term_query0(_Idx, [], Open, _) ->
    {undefined, Open}.

range_overlap(F, L, S, E)
  when E >= F andalso
       L >= S andalso
       F =< L ->
    X = max(F, S),
    {X, min(L, E), F, X - 1};
range_overlap(F, L, _, _) ->
    {undefined, F, L}.

read_plan(#cfg{uid = UId,
               open_mem_tbls = OpenTbl,
               closed_mem_tbls = ClosedTbl},
          FromIdx, ToIdx) ->
    Acc0 = case ets:lookup(OpenTbl, UId) of
               [{_, TStart, TEnd, Tid}] ->
                   case range_overlap(FromIdx, ToIdx, TStart, TEnd) of
                       {undefined, _, _} ->
                           {FromIdx, ToIdx, []};
                       {S, E, F, T} ->
                           {F, T,
                            [{ets, Tid, ?C_RA_LOG_READ_OPEN_MEM_TBL, S, E}]}
                   end;
               _ ->
                   {FromIdx, ToIdx, []}
           end,

    {RemF, RemL, Plan} =
        case closed_mem_tables(ClosedTbl, UId) of
            [] ->
                Acc0;
            Tables ->
                lists:foldl(
                  fun({_, _, S, E, Tid}, {F, T, Plan} = Acc) ->
                          case range_overlap(F, T, S, E) of
                              {undefined, _, _} ->
                                  Acc;
                              {S1, E1, F1, T1} ->
                                  {F1, T1,
                                   [{ets, Tid, ?C_RA_LOG_READ_CLOSED_MEM_TBL, S1, E1}
                                    | Plan]}
                          end
                  end, Acc0, Tables)
        end,
    case RemF =< RemL of
        true ->
            [{segments, RemF, RemL} | Plan];
        false ->
            Plan
    end.

open_mem_tbl_sparse_read(#cfg{uid = UId,
                              open_mem_tbls = OpenTbl},
                         Indexes, Acc0) ->
    case ets:lookup(OpenTbl, UId) of
        [{_, TStart, TEnd, Tid}] ->
            mem_tbl_sparse_read(Indexes, TStart, TEnd, Tid, 0, Acc0);
        [] ->
            {Acc0, 0, Indexes}
    end.

closed_mem_tbl_sparse_read(#cfg{uid = UId,
                                closed_mem_tbls = ClosedTbl}, Indexes, Acc0) ->
    case closed_mem_tables(ClosedTbl, UId) of
        [] ->
            {Acc0, 0, Indexes};
        Tables ->
            lists:foldl(fun({_, _, TblSt, TblEnd, Tid}, {Ac, Num, Idxs}) ->
                                mem_tbl_sparse_read(Idxs, TblSt, TblEnd, Tid, Num, Ac)
                        end, {Acc0, 0, Indexes}, Tables)
    end.

mem_tbl_sparse_read([I | Rem], TblStart, TblEnd, Tid, C, Entries0)
  when I >= TblStart andalso I =< TblEnd ->
    [Entry] = ets:lookup(Tid, I),
    mem_tbl_sparse_read(Rem, TblStart, TblEnd, Tid, C + 1, [Entry | Entries0]);
mem_tbl_sparse_read(Rem, _TblStart, _TblEnd, _Tid, C, Entries0) ->
    {Entries0, C, Rem}.

segrefs_to_read(From0, To0, _SegRefs, Acc)
  when To0 < From0 ->
    Acc;
segrefs_to_read(From0, To0, [{SStart, SEnd, FileName} | SegRefs], Acc)
  when SStart =< To0 andalso
       SEnd >= From0 ->
    From = max(From0, SStart),
    To = min(To0, SEnd),
    Spec = {From, To, FileName},
    segrefs_to_read(From0, SStart - 1, SegRefs, [Spec | Acc]);
segrefs_to_read(From0, To0, [_ | SegRefs], Acc) ->
    segrefs_to_read(From0, To0, SegRefs, Acc).

segment_fold(#?STATE{segment_refs = SegRefs,
                     open_segments = OpenSegs,
                     cfg = Cfg} = State,
             RStart, REnd, Fun, Acc) ->
    SegRefsToReadFrom = segrefs_to_read(RStart, REnd, SegRefs, []),
    {Op, A} =
        lists:foldl(
          fun ({From, To, Fn}, {Open0, Ac0}) ->
                  {Seg, Open} = get_segment(Cfg, Open0, Fn),
                  {Open, ra_log_segment:fold(Seg, From, To,
                                             fun binary_to_term/1,
                                             Fun,
                                             Ac0)}
          end, {OpenSegs, Acc}, SegRefsToReadFrom),
    {State#?MODULE{open_segments = Op}, A}.


segment_sparse_read(#?STATE{open_segments = Open}, [], Entries0) ->
    {Open, [], 0, Entries0};
segment_sparse_read(#?STATE{segment_refs = SegRefs,
                            open_segments = OpenSegs,
                            cfg = Cfg}, Indexes, Entries0) ->
    lists:foldl(
      fun(_, {_, [], _, _} = Acc) ->
              %% we're done reading
              throw(Acc);
         ({From, To, Fn}, {Open0, [NextIdx | _] = Idxs, C, En0})
           when NextIdx >= From andalso NextIdx =< To ->
              {Seg, Open} = get_segment(Cfg, Open0, Fn),
              {ReadIdxs, RemIdxs} =
                  sparse_read_split(fun (I) ->
                                            I >= From andalso I =< To
                                    end, Idxs, []),
              {ReadSparseCount, Entries} =
                  ra_log_segment:read_sparse(Seg, ReadIdxs,
                                             fun binary_to_term/1, []),
              {Open, RemIdxs, C +  ReadSparseCount,
               lists:reverse(Entries, En0)};
         (_Segref, Acc) ->
              Acc
      end, {OpenSegs, Indexes, 0, Entries0}, SegRefs).

%% like lists:splitwith but without reversing the accumulator
sparse_read_split(Fun, [E | Rem] = All, Acc) ->
    case Fun(E) of
        true ->
            sparse_read_split(Fun, Rem, [E | Acc]);
        false ->
            {Acc, All}
    end;
sparse_read_split(_Fun, [], Acc) ->
    {Acc, []}.


get_segment(#cfg{directory = Dir,
                 access_pattern = AccessPattern} = Cfg, Open0, Fn) ->
    case ra_flru:fetch(Fn, Open0) of
        {ok, S, Open1} ->
            {S, Open1};
        error ->
            AbsFn = filename:join(Dir, Fn),
            case ra_log_segment:open(AbsFn,
                                     #{mode => read,
                                       access_pattern => AccessPattern})
            of
                {ok, S} ->
                    incr_counter(Cfg, ?C_RA_LOG_OPEN_SEGMENTS, 1),
                    {S, ra_flru:insert(Fn, S, Open0)};
                {error, Err} ->
                    exit({ra_log_failed_to_open_segment, Err,
                          AbsFn})
            end
    end.

closed_mem_tables(Tbl, Id) ->
    case ets:lookup(Tbl, Id) of
        [] ->
            [];
        Tables ->
            lists:sort(fun (A, B) ->
                               element(2, A) > element(2, B)
                       end, Tables)
    end.

closed_mem_table_term_query(Tbl, Idx, Id) ->
    case closed_mem_tables(Tbl, Id) of
        [] ->
            undefined;
        Tables ->
            closed_mem_table_term_query0(Idx, Tables)
    end.

closed_mem_table_term_query0(_Idx, []) ->
    undefined;
closed_mem_table_term_query0(Idx, [{_, _, From, To, Tid} | _Tail])
  when Idx >= From andalso Idx =< To ->
    ets:lookup_element(Tid, Idx, 2);
closed_mem_table_term_query0(Idx, [_ | Tail]) ->
    closed_mem_table_term_query0(Idx, Tail).

compact_seg_refs([], PreviousSegRefs) ->
    PreviousSegRefs;
compact_seg_refs(NewSegRefs, []) ->
    NewSegRefs;
compact_seg_refs(NewSegRefs,
                 [{_, _, SegFile} | RemSegRefs] = PreviousSegRefs) ->
    case lists:last(NewSegRefs) of
        {_, _, SegFile} ->
            % update information about the last previously seen segment
            NewSegRefs ++ RemSegRefs;
        _ ->
            NewSegRefs ++ PreviousSegRefs
    end.

incr_counter(#cfg{counter = Cnt}, Ix, N) when Cnt =/= undefined ->
    counters:add(Cnt, Ix, N);
incr_counter(#cfg{counter = undefined}, _, _) ->
    ok.

incr_counter(#cfg{counter = Cnt}, {Ix, N}) when Cnt =/= undefined ->
    counters:add(Cnt, Ix, N);
incr_counter(#cfg{counter = undefined}, _) ->
    ok.

decr_counter(#cfg{counter = Cnt}, Ix, N) when Cnt =/= undefined ->
    counters:sub(Cnt, Ix, N);
decr_counter(#cfg{counter = undefined}, _, _) ->
    ok.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

compact_seg_refs_test() ->
    % {From, To, File}
    NewRefs = [{10, 100, "2"}],
    PrevRefs = [{10, 75, "2"}, {1, 9, "1"}],
    ?assertEqual([{10, 100, "2"}, {1, 9, "1"}], compact_seg_refs(NewRefs, PrevRefs)).

range_overlap_test() ->
    {undefined, 1, 10} = range_overlap(1, 10, 20, 30),
    {undefined, 21, 30} = range_overlap(21, 30, 10, 20),
    {20, 20, 20, 19} = range_overlap(20, 30, 10, 20),
    ?assertEqual({79, 99, 79, 78}, range_overlap(79, 99, 75, 111)),
    ?assertEqual({undefined, 79, 78}, range_overlap(79, 78, 50, 176)),
    ?assertEqual({undefined, 79, 78}, range_overlap(79, 78, 25, 49)),
    % {10, 10} = range_overlap(1, 10, 10, 30),
    % {5, 10} = range_overlap(1, 10, 5, 30),
    % {7, 10} = range_overlap(7, 10, 5, 30),
    ok.

read_plan_test() ->
    UId = <<"this_uid">>,
    OTbl = ra_log_open_mem_tables,
    OpnTbl = ets:new(OTbl, []),
    CTbl = ra_log_closed_mem_tables,
    ClsdTbl = ets:new(CTbl, [bag]),
    M1 = erlang:unique_integer([monotonic, positive]),
    M2 = erlang:unique_integer([monotonic, positive]),

    true = ets:insert(OpnTbl, {UId, 75, 111, OTbl}),
    true = ets:insert(ClsdTbl, {UId, M2, 50, 176, CTbl}),
    true = ets:insert(ClsdTbl, {UId, M1, 25, 49,  CTbl}),
    %% segments 0 - 24
    Cfg = #cfg{uid = UId,
               open_mem_tbls = OpnTbl,
               closed_mem_tbls = ClsdTbl},
    ?debugFmt("Read Plan: ~p~n", [read_plan(Cfg, 0, 100)]),
    ?assertMatch([{segments, 0, 24},
                  {ets, _, _, 25, 49},
                  {ets, _, _, 50, 74},
                  {ets, _, _, 75, 100}],
                 read_plan(Cfg, 0, 100)),

    ?debugFmt("Read Plan: ~p~n", [read_plan(Cfg, 10, 55)]),
    ?assertMatch([{segments, 10, 24},
                  {ets, _, _, 25, 49},
                  {ets, _, _, 50, 55}],
                 read_plan(Cfg, 10, 55)),
    ?assertMatch([
                  {ets, _, _, 79, 99}
                 ],
                 read_plan(Cfg, 79, 99)),
    ok.

segrefs_to_read_test() ->
    SegRefs = [{412,499,"00000005.segment"},
               {284,411,"00000004.segment"},
               {284,310,"00000004b.segment"},
               {200,285,"00000003.segment"},
               {128,255,"00000002.segment"},
               {0,127,"00000001.segment"}],

    ?assertEqual([{199,199,"00000002.segment"},
                  {200,283,"00000003.segment"},
                  {284,411,"00000004.segment"},
                  {412,499,"00000005.segment"}],
                 segrefs_to_read(199, 499, SegRefs, [])),
    ok.

-endif.
