%%!/usr/bin/env escript
%%! +S 1 -hidden

%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 根据#attr{}和attr.hrl中的属性宏定义，构建#attr{}中的字段
%%% 与属性编号数值的映射接口，便于属性计算时的转换，而无须
%%% 使#attr{}中的字段序号与宏定义的编号保持一致
%%%
%%% 隐含的规则是，#attr{}的字段，对应属性的宏必须是字段名的大写
%%%
%%% 使用方法：
%%%     当#attr{}中字段顺序调整、增删字段、修改相应属性的宏定义
%%%     对应的编号（这里定义为AttrId）时，需要重新创建映射的
%%%     attr_conv模块。
%%%
%%%     方法一（推荐）
%%%         escript attr_tool.erl
%%%         在配套的bat脚本中，执行attr命令即可自动调用
%%%     方法二
%%%         1. 首先重新编译attr_tool模块
%%%         2. 在erl中调用attr_tool:c()生成新的attr_conv.erl
%%% @end
%%% -------------------------------------------------------
-module(attr_tool).
-author("huangyongxing@yeah.net").
-export([
        c/0
    ]).

%% for hide warning (escript main function)
-export([
        main/1
    ]).

-include("../../include/attr.hrl").

%% escript程序入口
main(["help"]) ->
    ScriptName = escript:script_name(),
    io:format(
        "  usage:~n"
        "    escript ~ts [help]~n",
        [ScriptName]
    );
main(_) ->
    %% 检测文件路径，切换ebin目录（需要用到util模块）
    ScriptName = escript:script_name(),
    Dir = filename:dirname(ScriptName),
    Result =
    case file:set_cwd(Dir) of
        ok  ->
            file:set_cwd("../../ebin");
        Err ->
            Err
    end,
    case Result of
        ok ->
            {ok, CurrDir} = file:get_cwd(),
            io:format("ebin path: ~s~n~n", [CurrDir]),
            case code:load_file(util) of
                {module, _} ->
                    c();
                {error, What} ->
                    io:format("load error: ~p~n", [What])
            end;
        _ ->
            io:format("error while change directory to ebin: ~p~n", [Result])
    end,
    ok.

c() ->
    AttrFields = get_upper_attr_fields(),
    ModuleName = "attr_conv",
    FilePath = "../src/common/" ++ ModuleName ++ ".erl",
    FileTemp = file_template(ModuleName, AttrFields),
    Result = file:write_file(FilePath, FileTemp),
    io:format("write file [~ts]~n"
        "  result:~p~n",
        [FilePath, Result]
    ).

%% 取得#attr{}字段名大写与序号组成的TupleList
%% 其中AttrId对应的字段名转换为大写对应宏名
get_upper_attr_fields() ->
    AttrFields = record_info(fields, attr),
    get_upper_attr_fields(AttrFields, 2).

get_upper_attr_fields([Field | List], Index) ->
    UpperFiledInfo = {string:to_upper(atom_to_list(Field)), Index},
    [UpperFiledInfo | get_upper_attr_fields(List, Index + 1)];
get_upper_attr_fields([], _Index) ->
    [].

%% 生成属性id到record索引的关系
gen_get_attr_index(AttrFields) ->
    Bin = list_to_binary([
            list_to_binary([
                    <<"get_attr_index(?">>, list_to_binary(AttrUpperName), <<") -> ">>, integer_to_binary(Index), <<";\n">>
                ]) || {AttrUpperName, Index} <- AttrFields
        ]),
    <<Bin/binary, "get_attr_index(_AttrId) -> 0.\n\n">>.

%% 生成record索引到属性id的关系
gen_get_attr_id(AttrFields) ->
    Bin = list_to_binary([
            list_to_binary([
                    <<"get_attr_id(">>, integer_to_binary(Index), <<") -> ?">>, list_to_binary(AttrUpperName), <<";\n">>
                ]) || {AttrUpperName, Index} <- AttrFields
        ]),
    <<Bin/binary, "get_attr_id(_AttrIndex) -> 0.\n\n">>.

%% 生成属性相加及相减的操作函数
gen_attr_plus_and_minus() ->
    Fields      = [atom_to_list(Field) || Field <- record_info(fields, attr)],
    Funcs       = [{attr_plus, "+"}, {attr_minus, "-"}],
    Comments    = [
        <<"%% 属性相加及相减的操作函数\n"/utf8>>,
        <<"%% 不调用util:record_plus/2及util:record_minus/2，直接匹配后操作会更省内存（约为1/4内存开销，但运算速度相当）\n\n"/utf8>>
    ],
    FuncsBinList = [
        gen_operate_func(attr, Fields, FuncName, Operation)
        || {FuncName, Operation} <- Funcs
    ],
    list_to_binary(Comments ++ FuncsBinList).

gen_operate_func(Tag, Fields, FuncName, Operation) ->
    FieldsMatchA    = fields_match(Fields, "", "A"),
    FieldsMatchB    = fields_match(Fields, "", "B"),
    FieldsOperate   = fields_operate(Fields, [], "A", "B", Operation),
    util:fmt_bin("~s(AttrA, AttrB) ->~n"
        "    #~s{~n"
        "        ~s~n"
        "    } = AttrA,~n"
        "    #~s{~n"
        "        ~s~n"
        "    } = AttrB,~n"
        "    #~s{~n"
        "        ~s~n"
        "    }.~n~n"
        , [FuncName, Tag, FieldsMatchA, Tag, FieldsMatchB, Tag, FieldsOperate]).

fields_match([Field | Others], AccList, SubFix) ->
    VarName     = to_var_name(Field, SubFix),
    FieldMatch  = lists:concat([Field, " = ", VarName]),
    fields_match(Others, [FieldMatch | AccList], SubFix);
fields_match([], AccList, _SubFix) ->
    StrList = lists:reverse(AccList),
    string:join(StrList, ", ").

fields_operate([Field | Others], AccList, SubFixA, SubFixB, Operation) ->
    VarA = to_var_name(Field, SubFixA),
    VarB = to_var_name(Field, SubFixB),
    Str  = util:fmt_list("~s = ~s ~s ~s", [Field, VarA, Operation, VarB]),
    fields_operate(Others, [Str | AccList], SubFixA, SubFixB, Operation);
fields_operate([], AccList, _, _, _) ->
    StrList = lists:reverse(AccList),
    string:join(StrList, ", ").

to_var_name(Field, SubFix) ->
    StrList = string:tokens(Field, "_"),
    lists:concat([upper_first_char(Str) || Str <- StrList]) ++ SubFix.

upper_first_char([Char | CharList]) ->
    string:to_upper([Char]) ++ CharList;
upper_first_char(CharList) ->
    CharList.

%% 文件模板
file_template(ModuleName, AttrFields) ->
    FunBinStrGetIndex = gen_get_attr_index(AttrFields),
    FunBinStrGetId    = gen_get_attr_id(AttrFields),
    FuncsPlusAndMinus = gen_attr_plus_and_minus(),
    MaxAttrIndex      = size(#attr{}),
    list_to_binary([<<"%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc 提供#attr{}、[{AttrId, AttrVal}, ...]、#{}的转换接口
%%% 由工具模块 "/utf8>>, atom_to_binary(?MODULE, utf8), <<" 根据#attr{}结构生成
%%% 隐含的规则是，#attr{}的字段，对应属性的宏必须是字段名的大写
%%% -------------------------------------------------------
-module("/utf8>>, list_to_binary(ModuleName), <<").
-export([
        to_attr_record/1
        ,to_attr_tuple_list/1
        ,to_attr_map/1
        ,set_attr_val/2
        ,attr_list_filter/1
        ,get_attr_index/1
        ,get_attr_id/1
        ,attr_plus/2
        ,attr_minus/2
    ]).
-include(\"common.hrl\").
-include(\"attr.hrl\").

%% 属性最大索引编号（ 即size(#attr{}) ）
-define(MAX_ATTR_IDX, "/utf8>>, integer_to_binary(MaxAttrIndex), <<").

%% 转换为#attr{}
-spec to_attr_record(AttrData) -> Attr when
    AttrData :: [{AttrId, AttrVal}] | #attr{} | AttrMap,
    AttrMap :: map(),
    AttrId :: non_neg_integer(), AttrVal :: number(),
    Attr :: #attr{}.
to_attr_record(AttrData) ->
    if
        is_list(AttrData) ->
            lists:foldl(fun set_attr_val/2, #attr{}, AttrData);
        is_map(AttrData) ->
            maps:fold(fun set_attr_val/2, #attr{}, AttrData);
        true ->
            AttrData
    end.

%% 转换为列表[{AttrId, AttrVal},...]
-spec to_attr_tuple_list(AttrData) -> AttrTupleList when
    AttrData :: [{AttrId, AttrVal}] | #attr{} | AttrMap,
    AttrMap :: map(),
    AttrId :: non_neg_integer(), AttrVal :: number(),
    AttrTupleList :: [{AttrId, AttrVal}].
to_attr_tuple_list(Attr) when is_map(Attr) ->
    maps:to_list(Attr);
to_attr_tuple_list(Attr = #attr{}) ->
    [_ | AttrValList] = tuple_to_list(Attr),
    F = fun(AttrVal, {Index, Acc}) ->
            case AttrVal > 0 of
                true  ->
                    AttrId = get_attr_id(Index),
                    case AttrId > 0 of
                        true  -> {Index + 1, [{AttrId, AttrVal} | Acc]};
                        false ->
                            %% 这里意味着attr_conv需要重新生成，可能新增了字段，无法映射到AttrId
                            ?ERR(\"error attr index: \~w, please execute attr_tool:c().\", [Index]),
                            {Index + 1, Acc}
                    end;
                false ->
                    {Index + 1, Acc}
            end
    end,
    {_, AttrList} = lists:foldl(F, {2, []}, AttrValList),
    AttrList;
to_attr_tuple_list(AttrList) when is_list(AttrList) ->
    AttrList.

%% 转换为map格式的attr
-spec to_attr_map(AttrData) -> AttrMap when
    AttrData :: [{AttrId, AttrVal}] | #attr{} | AttrMap,
    AttrMap :: map(),
    AttrId :: non_neg_integer(), AttrVal :: number().
to_attr_map(AttrList) when is_list(AttrList) ->
    maps:from_list(AttrList);
to_attr_map(Attr = #attr{}) ->
    AttrList = to_attr_tuple_list(Attr),
    maps:from_list(AttrList);
to_attr_map(AttrMap) when is_map(AttrMap) ->
    AttrMap.

%% 设定#attr{}某属性id对应的属性值
-spec set_attr_val({AttrId, AttrVal}, Attr) -> NewAttr when
    AttrId :: non_neg_integer(), AttrVal :: number(),
    Attr :: #attr{},
    NewAttr :: #attr{}.
set_attr_val({AttrId, AttrVal}, Attr) ->
    Index = get_attr_index(AttrId),
    if
        Index > ?MAX_ATTR_IDX ->
            %% 可能删除了某些字段，意味着需要重新生成attr_conv
            ?ERR(\"error attr index:\~w, please execute attr_tool:c().\", [Index]),
            throw({<<\"error_attr_index\">>, Index});
        Index > 0 ->
            setelement(Index, Attr, AttrVal);
        true ->
            %% 由于原属性设计中的AttrId存在一些超出#attr{}范围的属性，这里
            %% 无法判定一定是出错，有可能是原设计逻辑中需要忽略的属性，所以不打印日志
            %% ?INFO(\"error attr id or virtual attr id:\~w\", [AttrId]),
            Attr
    end.

%% 只保留#attr{}中的属性
attr_list_filter(AttrList) ->
    [{AttrId, AttrVal} || {AttrId, AttrVal} <- AttrList, AttrId =< ?MAX_ATTR_IDX].

"/utf8>>, FunBinStrGetIndex, FunBinStrGetId, FuncsPlusAndMinus]).

