#!/usr/bin/env escript
%% -*- erlang -*-
-include_lib("kernel/include/file.hrl").
-mode(compile).

main(["--show-vsn"]) ->
    case find_vsn() of
        {ok, {Vsn, _Source}} ->
            io:put_chars([Vsn, "\n"]),
            do_exit(0);
        {error, _} ->
            print_no_vsn_error(),
            do_exit(1)
    end;
main(["--show-vsn-source"]) ->
    case find_vsn() of
        {ok, {_Vsn, Source}} ->
            io:put_chars([atom_to_list(Source), "\n"]),
            do_exit(0);
        {error, _} ->
            print_no_vsn_error(),
            do_exit(1)
    end;
main(["--have-vsn"]) ->
    case find_vsn() of
        {ok, {_Vsn, _Source}} ->
            io:put_chars("true\n");
        {error, _} ->
            io:put_chars("false\n")
    end,
    do_exit(0);
main(["--check-have-vsn"]) ->
    case find_vsn() of
        {ok, {_Vsn, _Source}} ->
            do_exit(0);
        {error, _} ->
            do_exit(1)
    end;
main(_) ->
    io:format("usage: ~s --show-vsn | --show-vsn-source~n",
              [escript:script_name()]),
    do_exit(1).

do_exit(ExitCode) ->
    halt(ExitCode).

find_vsn() ->
    case find_vsn_from_file() of
        {ok, Vsn} ->
            {ok, {Vsn, file}};
        {error, Reason1} ->
            case find_vsn_from_git() of
                {ok, Vsn} ->
                    {ok, {Vsn, git}};
                {error, Reason2} ->
                    {error, {Reason1, Reason2}}
            end
    end.

find_vsn_from_file() ->
    %% assume in top dir
    %% else move up until either .git or gpb.vsn??
    %% though not across file systems?
    %% or until dir looks like top of gpb (eg src/gpb.erl exists)
    case file:get_cwd() of
        {ok, Cwd} ->
            try_walk_root_wise(Cwd, "gpb.vsn");
        {error, Reason} ->
            {error, {no_cwd, Reason}}
    end.

try_walk_root_wise(Dir, Base) ->
    case is_at_gpb_root(Dir) of
        true ->
            case file:read_file(filename:join(Dir, Base)) of
                {ok, Vsn} ->
                    {ok, first_line_no_nl(binary_to_list(Vsn))};
                {error, _} ->
                    {error, no_file}
            end;
        false ->
            case up_dir(Dir) of
                {ok, Up} -> try_walk_root_wise(Up, Base);
                {error, Reason} -> {error, Reason}
            end
    end.

is_at_gpb_root(Dir) ->
    lists:any(
      fun(F) -> F() end,
      [fun() -> filelib:is_dir(filename:join(Dir, ".git")) end,
       fun() -> filelib:is_regular(fjoin(Dir, ".git")) end, % a git worktree?
       fun() -> filelib:is_regular(fjoin(Dir, "gpb.vsn")) end,
       fun() -> filelib:is_regular(fjoin([Dir, "src", "gpb.erl"])) end]).

fjoin(Dir, Name) -> filename:join(Dir, Name).
fjoin(Comps) -> filename:join(Comps).

up_dir(Dir) ->
    case filename:dirname(Dir) of
        Dir ->
            {error, at_file_system_root};
        "." -> % windows
            {error, at_file_system_root};
        Up ->
            %% Check if we move cross 
            case {file:read_file_info(Dir), file:read_file_info(Up)} of
                {{ok, #file_info{major_device=SameDev}},
                 {ok, #file_info{major_device=SameDev}}} ->
                    {ok, Up};
                {{ok, _},
                 {ok, _}} ->
                    {error, cross_device};
                {_, {error, Reason}} ->
                    {error, Reason};
                {{error, Reason}, _} ->
                    {error, Reason};
                _ ->
                    {error, undefined}
            end
    end.

find_vsn_from_git() ->
    Cmd = case os:type() of
              {win32,_} ->
                  "git describe --always --tags --match [0-9]*.[0-9]*";
              _ ->
                  "git describe --always --tags --match '[0-9]*.[0-9]*'"
          end,
    Output = os:cmd(Cmd),
    Line1 = first_line_no_nl(Output),
    case looks_like_vsn_output(Line1) of
        true  ->
            {ok, Line1};
        false ->
            {error, {Cmd, Output}}
    end.

-define(is_digit(C), ($0 =< C andalso C =< $9)).

looks_like_vsn_output([C1 | _]=Str) when ?is_digit(C1) ->
    %% Examples of valid outputs:
    %%   "4.17.3"
    %%   "4.17.3-1-g0bfce0e"
    is_vsn_str_1(Str);
looks_like_vsn_output(_) ->
    false.


is_vsn_str_1([C | Rest]) when ?is_digit(C) -> is_vsn_str_1(Rest);
is_vsn_str_1("." ++ Rest) -> is_vsn_str_2(Rest);
is_vsn_str_1(_) -> false.

is_vsn_str_2([C | Rest]) when ?is_digit(C) -> is_vsn_str_2(Rest);
is_vsn_str_2("." ++ Rest) -> is_vsn_str_3(Rest);
is_vsn_str_2(_) -> false.


is_vsn_str_3([C | Rest]) when ?is_digit(C) -> is_vsn_str_3(Rest);
is_vsn_str_3("-" ++ _) -> true;
is_vsn_str_3("") -> true;
is_vsn_str_3(_) -> false.

first_line_no_nl("\r" ++ _) -> "";
first_line_no_nl("\n" ++ _) -> "";
first_line_no_nl([C | Tl]) -> [C | first_line_no_nl(Tl)];
first_line_no_nl("") -> "".

print_no_vsn_error() ->
    io:put_chars(["No version information found: no gpb.vsn file "
                  "and not in a git repo.\n"]).
