%%-----------------------------------------------------------------------------
%% module  :       mod_mobilize_mgr
%% @author  :      zmh
%% Created :       20200811
%% description:    全民动员 总进度管理
%%-----------------------------------------------------------------------------
-module(mod_mobilize_mgr).

-include("common.hrl").
-include("def_module.hrl").
-include("custom_act.hrl").
-include("custom_act_mobilize.hrl").

-compile(export_all).

-export ([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
-export ([
    start_link/0,
    cast/1,
    timer_check/0,          %% 15分钟检测一次全服(假数据)进度
    get_fake_map/0,
    gm/2
    % test:
    ,calc_click_time/3
]).

cast(Msg) ->
    gen_server:cast(?MODULE, Msg).

timer_check()->
    case lib_custom_act_api:get_open_custom_act_infos(?CUSTOM_ACT_TYPE_MOBILIZE) of
        [_|_] ->
            gen_server:cast(?MODULE,{timer_check});
        _ ->
            ignore
    end.

daily_log()->
    gen_server:cast(?MODULE,{daily_log}).

get_fake_map()->
    case gen_server:call(?MODULE, {get_fake_map}, 2000) of
        {ok, FakeMap}->
            skip;
        _ ->
            FakeMap = #{}
    end,
    FakeMap.

gm(Type,Dot)->
    Now = utime:unixdate(),
    case Type of
        0->
            STime = Now-86400*Dot,
            ETime = Now+86400*Dot;
        _->
            STime = Now,
            ETime = Now+86400*Dot
    end,
    cast({init_fake,1,STime,ETime}).

%% API
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
    State = load_mobilize_fake(#{}),
    {ok, State}.

handle_call(Msg, From, State) ->
    case catch do_handle_call(Msg, From, State) of
        {'EXIT', Error} ->
            ?ERR("handle_call error: ~p~nMsg=~p~n", [Error, Msg]),
            {reply, error, State};
        Return  ->
            Return
    end.

handle_cast(Msg, State) ->
    case catch do_handle_cast(Msg, State) of
        {'EXIT', Error} ->
            ?ERR("handle_cast error: ~p~nMsg=~p~n", [Error, Msg]),
            {noreply, State};
        Return  ->
            Return
    end.

handle_info(Info, State) ->
    case catch do_handle_info(Info, State) of
        {'EXIT', Error} ->
            ?ERR("handle_info error: ~p~nInfo=~p~n", [Error, Info]),
            {noreply, State};
        Return  ->
            Return
    end.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

terminate(_Reason, _State) ->
    ?ERR("~p is terminate:~p", [?MODULE, _Reason]),
    ok.

do_handle_call({get_fake_map},_From,State)->
    {reply,{ok,State},State};

do_handle_call(_Msg, _From, State) ->
    {reply, ok, State}.

%% 初始化
do_handle_cast({init_fake, SubType, STime, ETime}, _State) ->
    TaskTypes = data_custom_mobilize:get_task_type_id_list_by_subtype(SubType),
    NState = init_fake(SubType, TaskTypes, STime, ETime, #{}),
    save_mobilize_fake(NState),
    {noreply,NState};

%% 非首次初始化 补充
do_handle_cast({redo_init_fake, SubType, ActSTime, ActETime},State)->
    TaskTypes = data_custom_mobilize:get_task_type_id_list_by_subtype(SubType),
    NState = redo_init_fake(SubType, TaskTypes, ActSTime, ActETime,State),
    save_mobilize_fake(NState),
    {noreply,NState};

%% 每15分钟检测一次
%% 先按次数来加固定积分(通过配置计算出来)。
%% 如果次数用完则按配置规则重新计算固定积分
%% 注意: 配置中的point指百分比，有可能超过100，避免数据都集中在100%
do_handle_cast({timer_check}, State) ->
    Now = utime:unixtime(),
    F = fun(Key, Faker, {FakeMap, Adder}) ->
        {TaskType, FakeId, _StartDay} = Key,
        {FakeRule, CheckRule} = data_custom_mobilize:get_fake_rule(FakeId),
        #fake_cord{
            stime = STime,
            etime = ETime,
            ltime = LTime,

            remain_score = RemainScore,
            process = CurProgress
        } = Faker,

        MaxProgress = calc_max_process(TaskType),       %% 满进度值
        CurRatio = util:floor(CurProgress/MaxProgress*100),         %% 当前进度占比

        TotalTime = max(ETime - STime, 1),
        UsedTime  = max(Now - STime, 0),
        LastUsedTime = max(LTime - STime, 0),
        LastFake = analyze_fake_rule(util:floor(LastUsedTime/TotalTime*100), FakeRule),
        CurFake  = analyze_fake_rule(util:floor(UsedTime/TotalTime*100), FakeRule),
        CurNeedRatio = analyze_fake_rule(CurFake, FakeRule, CheckRule), %% 这次需求目标进度比
        % LastRatio = analyze_fake_rule(LastFake, FakeRule, CheckRule),   %% 上次目标进度比(如果处于相同时间段与这次的相同)
        % ?INFO_TTY("CurRatio is ~w~n",[CurRatio]),
        % ?INFO_TTY("CurNeedRatio, LastRatio is ~w~n",[{CurNeedRatio, LastRatio}]),
        %io:format("----:~p~n",[{CurF,NextF,NextF0,NeedTime,NextETime,Add}]),
        %% 是否在时间范围内
        if not (Now >= STime andalso Now =< ETime) ->
            {
                maps:put(Key, Faker, FakeMap), Adder
            };
        true ->
            %% 如果满足进度
            case CurRatio >= CurNeedRatio of
                true -> %% 清理补数据
                    NewFaker = Faker#fake_cord{
                        remain_score = 0, per_score  = 0,
                        remain_click = 0, process = CurProgress
                    },
                    db_update_click(NewFaker),
                    {
                        maps:put(Key, NewFaker, FakeMap), Adder + 1
                    };
                _ ->
                    case RemainScore > 0 of
                        %% 1> 积分还有没补完，继续补分
                        true ->
                            {FakeMap1, Adder1} = continue_add_score(FakeMap, Adder, Faker, Key),
                            {FakeMap1, Adder1};
                        %% 2> 积分补完 检测是否需要重新计算补分次数
                        _ ->
                            case LastFake =:= CurFake of
                                true -> %% 没跨时间节点 不处理
                                    % NFaker = Faker#fake_cord{ltime = Now},
                                    % update_process(NFaker),
                                    {
                                        maps:put(Key, Faker, FakeMap), Adder
                                    };
                                _ -> %% 跨时间节点 检测积分节点是否满足
                                    case CurRatio >= CurNeedRatio of
                                        true -> %% 符合预期 不干预
                                            % NFaker = Faker#fake_cord{ltime = Now},
                                            % update_process(NFaker),
                                            {
                                                maps:put(Key, Faker, FakeMap), Adder
                                            };
                                        _ -> %% 不符合预期
                                            AimPro = util:floor(MaxProgress*CurNeedRatio/100),
                                            calc_new_adder(
                                                FakeMap, Adder, Faker, Key, CurFake,
                                                FakeRule, AimPro, TotalTime, Now
                                            )
                                    end
                            end
                    end
            end
        end
    end,
    {NState, IsUpdate} = maps:fold(F, {State, 0}, State),
    case IsUpdate>0 of
        true-> %% 有更新变化  通知玩家再拿一遍数据
            erlang:send_after(1000,self(), brocast_role);
        _->
            ok
    end,
    {noreply,NState};

do_handle_cast({daily_log},State)->
    Now = utime:unixtime(),
    Fakers = maps:values(State),
    SqlArgs = save_mobilize_fake_log(Fakers, Now, []),
    SqlArgs =/= [] andalso db:execute(io_lib:format(?SQL_MOBILIZE_FAKE_LOG_BATCH, [SqlArgs])),
    {noreply,State};

%% @doc
%% 注意：
%% 有部分任务需要从活动开始时间就统计
%% 而假数据触发则统一需要从天数区间的第一个天数开始
%% @end
do_handle_cast({sync_server_precess, SubType, DayInterVal, ActSTime, ActETime, TaskTypeId, Add, OldValue},State)->
    {StarDay, EndDay} = DayInterVal,
    FakeId  = custom_act_mobilize:get_task_fake(SubType, TaskTypeId),
    Key = {TaskTypeId, FakeId, StarDay},
    TaskType = data_custom_mobilize:get_task_type(TaskTypeId),
    {STime, ETime} =
        custom_act_mobilize:task_type_from_start_to_end(SubType, TaskTypeId, StarDay, EndDay, ActSTime, ActETime),
    Default = #fake_cord{id = Key, act = SubType, stime = STime, etime = ETime, ltime = STime},
    Faker   = maps:get(Key, State, Default),
    #fake_cord{process = OPro, real_pro = ORPro}=Faker,
    case lists:member(TaskType, ?MOBILIZE_SPEC_TASK_TYPE_LIST) of
        true ->
            NPro   = max(0, OPro - OldValue + Add),
            NRPro  = max(0, ORPro - OldValue + Add);
        _ ->
            NPro   = OPro + Add,
            NRPro  = ORPro + Add
    end,
    NFaker = Faker#fake_cord{process = NPro, real_pro = NRPro},
    NState = maps:put(Key, NFaker, State),
    update_process(NFaker),
    NowTime = utime:unixtime(),
    %% 开始有假数据触发了才通知玩家更新
    NowTime >= STime andalso
        erlang:send_after(1000, self(), {event_process_change, SubType, TaskTypeId, FakeId, OPro, NPro}),
    {noreply,NState};

do_handle_cast({clean_fake},_State)->
    %% 清理所有全民动员相关数据
    db:execute("TRUNCATE TABLE mobilize_fake_cord"),
    db:execute("TRUNCATE TABLE mobilize_task"),
    db:execute("TRUNCATE TABLE mobilize_days_reward"),
    db:execute("TRUNCATE TABLE mobilize_power"),
    {noreply,#{}};

do_handle_cast(_Msg, State) ->
    {noreply, State}.

do_handle_info(brocast_role,State)->
    lib_player:apply_cast_online(custom_act_mobilize, send_mobilize_task_info, []),
    {noreply,State};

%% change: 只更新当前的任务类型的假数据即可
do_handle_info({event_process_change, _SubType, TaskType, _FakeId, OPro, NPro},State)->
    % TaskTypes = data_custom_mobilize:get_task_type_id_list_by_subtype_fake(SubType,FakeId),
    case check_process_event([TaskType], OPro, NPro, false) of
        true->
            lib_player:apply_cast_online(custom_act_mobilize, send_mobilize_task_info, []);
        _->
            ok
    end,
    {noreply,State};

do_handle_info(_Msg, State) ->
    {noreply, State}.

continue_add_score(FakeMap, Adder, Faker, Key) ->
    #fake_cord{
        remain_score = RemainScore,
        per_score    = PerAdd,
        remain_times = RTimes,      %% 总补助次数
        remain_click = OClick,      %% 多出的补助次数

        process = CurProgress
    } = Faker,
    {
        NewProgress, NewReMainSore, NewClick
    } = case OClick + 1 >= RTimes of
            true -> %% 次数够
                NewPro  = CurProgress + PerAdd,
                NRemainScore = max(RemainScore - PerAdd, 0),
                NClick  = 0,
                {NewPro, NRemainScore, NClick};
            _ ->
                NewPro  = CurProgress,
                NRemainScore = RemainScore,
                NClick  = OClick + 1,
                {NewPro, NRemainScore, NClick}
        end,
    NewFaker = case NRemainScore =< 0 of
                   true-> %% 补助完成
                       Faker#fake_cord{
                           remain_score = 0, per_score  = 0,
                           remain_click = 0, process = CurProgress
                       };
                   _ ->
                       Faker#fake_cord{
                           remain_score = NewReMainSore, process = NewProgress,
                           remain_click = NewClick
                       }
               end,
    db_update_click(NewFaker),
    {
        maps:put(Key, NewFaker, FakeMap), Adder + 1
    }.

calc_new_adder(FakeMap, Adder, Faker, Key, CurFake, FakeRule, AimPro, TotalTime, Now) ->
    #fake_cord{
        stime   = STime,
        process = CurProgress
    } = Faker,
    Add = max(AimPro-CurProgress,1),
    NextFake0 = calc_next_time(CurFake, FakeRule),
    %% 矫正一下数据吧(其实这步比较多余)
    NextFake = case NextFake0 =< CurFake of
                   true -> 100;
                   _ -> NextFake0
               end,
    NextETime = STime + util:floor(TotalTime*NextFake/100),
    NeedTime = max(NextETime - Now, 1),
    {TimeNum, PerT} = calc_click_time(NeedTime,Add,1),
    NFaker = Faker#fake_cord{

        remain_score = Add,
        per_score = PerT,
        remain_times = TimeNum,
        remain_click = 0,

        ltime = Now
    },
    db_update_click(NFaker),
    {
        maps:put(Key, NFaker, FakeMap), Adder+1
    }.

%% old: 以活动整个周期为任务的开始结束时间
%% new: 2021-04-29  每个任务都有自己的天数跨度 % todo:筛选天数要满足开服的
init_fake(_SubType, [], _, _, FakeMap)->
    FakeMap;
init_fake(SubType, [TaskTypeId|T], ActSTime, ActETime, FakeMap) ->
    FakeId = custom_act_mobilize:get_task_fake(SubType, TaskTypeId),
    case data_custom_mobilize:get_day_interval(SubType, TaskTypeId) of
        [{_, _} |_] = List ->
            F = fun({StartDay, EndDay}, TFakeMap) ->
                STime = utime:that_day_after(ActSTime, max(0, StartDay-1)),
                ETime = utime:that_day_after(ActSTime, max(0, EndDay)),
                case FakeId > 0 of
                    true ->
                        Key = {TaskTypeId, FakeId, StartDay},
                        NFake = #fake_cord{
                            id = Key,
                            act = SubType,
                            stime = STime,
                            etime = ETime,
                            ltime = STime
                        },
                        maps:put(Key,NFake,TFakeMap);
                    _ ->
                        TFakeMap
                end end,
            NFakeMap = lists:foldl(F, FakeMap, List),
            init_fake(SubType, T, ActSTime, ActETime, NFakeMap);
        _ ->
            init_fake(SubType, T, ActSTime, ActETime,FakeMap)
    end.

redo_init_fake(_SubType,[],_,_,FakeMap)->
    FakeMap;
redo_init_fake(SubType,[H|T], ActSTime, ActETime, FakeMap)->
    FakeId = custom_act_mobilize:get_task_fake(SubType,H),
    DayList = data_custom_mobilize:get_day_interval(SubType, H),
    F = fun({StartDay, EndDay}, TFakeMap) ->
        STime = utime:that_day_after(ActSTime, max(0, StartDay-1)),
        ETime = utime:that_day_after(ActSTime, max(0, EndDay)),
        case FakeId > 0 of
            true ->
                Key = {H, FakeId, StartDay},
                NFake = #fake_cord{
                    id = Key,
                    act = SubType,
                    stime = STime,
                    etime = ETime,
                    ltime = STime
                },
                maps:put(Key,NFake,TFakeMap);
            _ ->
                TFakeMap
        end end,
    NFakeMap = lists:foldl(F, FakeMap, DayList),
    redo_init_fake(SubType, T, ActSTime, ActETime, NFakeMap).

check_process_event([],_,_,Return)->
    Return;
check_process_event([H|T],OPro,NPro,Return)->
    TaskIds = data_custom_mobilize:get_task_id_list(H),
    OpenDay = util:get_open_day(),
    case check_task_event(H,OpenDay,TaskIds,OPro,NPro,false) of
        true->
            true;
        _->
            check_process_event(T,OPro,NPro,Return)
    end.

check_task_event(_TaskType,_OpenDay,[],_OPro,_NPro,Return)->
    Return;
check_task_event(TaskType,OpenDay,[H|T],OPro,NPro,Return)->
    case data_custom_mobilize:get_mobilize_task(TaskType,H) of
        #custom_mobilize_task{server_pro=Need,day=StartDay,end_day=EndDay}->
            case StartDay =< OpenDay andalso OpenDay =< EndDay of
                true ->
                    case OPro=<Need andalso Need=<NPro of
                        true->
                            true;
                        _->
                            check_task_event(TaskType,OpenDay,T,OPro,NPro,Return)
                    end;
                _ ->
                    check_task_event(TaskType,OpenDay,T,OPro,NPro,Return)
            end;
        _->
            check_task_event(TaskType,OpenDay,T,OPro,NPro,Return)
    end.

%% 根据时间获取时间节点
analyze_fake_rule(_TimeRatio, [])->
    0;
analyze_fake_rule(TimeRatio, [{fake, Times}|_])->
    analyze_data(TimeRatio, Times, 0).

%% 根据时间节点获取进度百分比
analyze_fake_rule(Dot, [{fake, Dots}|_], [{point, Points}|_])->
    Index = ulists:index_of(Dot, Dots) + 1,
    Stage = find_in_list(Points, 1, Index),
    Stage.

find_in_list([],_,_)->
    100;
find_in_list([H|T],Num,Index)->
    case Num =:= Index of
        true->
            H;
        _->
            find_in_list(T,Num+1,Index)
    end.

%% 获取向下取整的时间百分比
analyze_data(_,[],Stage)->
    Stage;
analyze_data(TimeRatio, [H|T], Stage)->
    case TimeRatio >= H of
        true ->
            analyze_data(TimeRatio, T, H);
        _ ->
            Stage
    end.

calc_next_time(Dot,[])->
    Dot;
calc_next_time(Dot, [{fake,Times}|_])->
    analyze_next_data(Dot, Times).

analyze_next_data(_,[H])->
    H;
analyze_next_data(Dot,[H|T])->
    case Dot >= H of
        true ->
            analyze_next_data(Dot,T);
        _ ->
            H
    end.

%%计算任务最大进度值
calc_max_process(TaskTypeId) ->
    TaskIds = data_custom_mobilize:get_task_id_list(TaskTypeId),
    OpenDay = util:get_open_day(),
    F = fun(TaskId, TList) ->
        case data_custom_mobilize:get_mobilize_task(TaskTypeId, TaskId) of
            #custom_mobilize_task{day = StartDay, end_day = EndDay}
            when StartDay =< OpenDay andalso OpenDay =< EndDay ->
                [TaskId|TList];
            _ ->
                TList
        end end,
    TaskIdList = lists:foldl(F, [], TaskIds),
    Max = case TaskIdList of
              [_|_] -> lists:max(TaskIdList);
              _ -> 0
          end,
    case data_custom_mobilize:get_mobilize_task(TaskTypeId,Max) of
        #custom_mobilize_task{server_pro=MaxPro}->
            skip;
        _->
            MaxPro = 1
    end,
    MaxPro.

%%计算时间间隔(15分钟-900)  %% todo 时间间隔比大于 Add 时候Times(Num)的计算
calc_click_time(NeedTime,Add,Num)->
    Data  = Num*900,
    TimeNum = max(util:floor(NeedTime/Data),1),
    Per     = Add/TimeNum,
    case Data>=10800 of
        true-> %% 最大三小时,即Num最大运算到12
            {Num,util:ceil(Per)};
        _->
            case Per<1 of
                true->
                    calc_click_time(NeedTime,Add,Num+1);
                _->
                    {Num,util:ceil(Per)}
            end
    end.

%%加载
load_mobilize_fake(FakeMap)->
    case db:get_all(?SQL_MOBILIZE_FAKE_GET) of
        DataList when is_list(DataList) andalso DataList =/= [] ->
            load_mobilize_fake(DataList,FakeMap);
        _ ->
            FakeMap
    end.

load_mobilize_fake([],FakeMap)->
    FakeMap;
load_mobilize_fake([[TaskType,FakeId,Act,StartDay,STime,ETime,LTime,RScore,PScore,RTimes,RClick,Process,RealPro]|T],FakeMap)->
    Key = {TaskType,FakeId,StartDay},
    Faker = #fake_cord{
        id = Key,
        act = Act,
        stime = STime,
        etime = ETime,
        ltime = LTime,

        remain_score = RScore,
        per_score    = PScore,
        remain_times = RTimes,
        remain_click = RClick,
        process = Process,
        real_pro = RealPro
    },
    NFakeMap = maps:put(Key,Faker,FakeMap),
    load_mobilize_fake(T,NFakeMap).

save_mobilize_fake(FakeMap)->
    Fakers = maps:values(FakeMap),
    SqlArgs = save_mobilize_fake(Fakers, []),
    SqlArgs =/= [] andalso db:execute(io_lib:format(?SQL_MOBILIZE_FAKE_SAVE, [SqlArgs])),
    ok.

save_mobilize_fake([], SqlArgs) ->
    string:join(SqlArgs, ",");
save_mobilize_fake([H | T], SqlArgs) ->
    #fake_cord{
        id = {TaskType,FakeId,StartDay},
        act = Act,
        stime = STime,
        etime = ETime,
        ltime = LTime,

        remain_score = RScore,
        per_score    = PScore,
        remain_times = RTimes,
        remain_click = RClick,
        process = Process,
        real_pro = RealPro
    } = H,

    Arg = [TaskType,FakeId,Act,StartDay,STime,ETime,LTime,RScore,PScore,RTimes,RClick,Process,RealPro],
    DataFormat = io_lib:format("(~p, ~p, ~p, ~p, ~p, ~p, ~p, ~p,~p, ~p, ~p, ~p,~p)", Arg),
    NewSqlArgs = [DataFormat|SqlArgs],
    save_mobilize_fake(T, NewSqlArgs).

db_update_click(Faker)->
     #fake_cord{
        id = {TaskType,FakeId,Day},
        act = Act,
        ltime = LTime,
        remain_score = RScore,
        per_score    = PScore,
        remain_times = RTimes,
        remain_click = RClick,
        process = Process,
        real_pro = RealPro
    } = Faker,
    Sql = io_lib:format(?SQL_FAKE_CLICK_UPDATE,[LTime,RScore,PScore,RTimes,RClick,Process,RealPro,TaskType,FakeId,Act,Day]),
    db:execute(Sql).

update_process(Faker)->
    #fake_cord{
        id = {TaskType,FakeId,Day},
        act = Act,
        ltime = LTime,
        process = Process,
        real_pro = RealPro
    } = Faker,
    Sql = io_lib:format(?SQL_FAKE_PROCESS_UPDATE,[LTime,Process,RealPro,TaskType,FakeId,Act,Day]),
    db:execute(Sql).


save_mobilize_fake_log([], _Time,SqlArgs) ->
    string:join(SqlArgs, ",");
save_mobilize_fake_log([H | T], Time,SqlArgs) ->
    #fake_cord{
        id = {TaskType,FakeId, _Day},
        act = Act,
        stime = StartTime,
        process = Process,
        real_pro = RealPro
    } = H,
    case data_custom_mobilize:get_mobilize_task(TaskType,1) of
         #custom_mobilize_task{desc = Desc}->
            skip;
        _->
            Desc = <<>>
    end,
    NowDay = utime:day_span(StartTime, Time),
    Arg = [Act,TaskType,Desc,FakeId,Process,RealPro,NowDay,Time],
    DataFormat = io_lib:format("(~p, ~p, '~ts',~p, ~p, ~p,~p,~p)", Arg),
    NewSqlArgs = [DataFormat|SqlArgs],
    save_mobilize_fake_log(T, Time,NewSqlArgs).