%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2015. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

-module(erl_anno).

-export([new/1, is_anno/1]).
-export([column/1, end_location/1, file/1, generated/1,
         line/1, location/1, record/1, text/1]).
-export([set_file/2, set_generated/2, set_line/2, set_location/2,
         set_record/2, set_text/2]).

%% To be used when necessary to avoid Dialyzer warnings.
-export([to_term/1, from_term/1]).

-export_type([anno/0, line/0, column/0, location/0, text/0]).

-export_type([anno_term/0]).

-define(LN(L), is_integer(L), L >= 0).
-define(COL(C), (is_integer(C) andalso C >= 1)).

%% Location.
-define(LCOLUMN(C), ?COL(C)).
-define(LLINE(L), ?LN(L)).

%% Debug: define DEBUG to make sure that annotations are handled as an
%% opaque type. Note that all abstract code need to be compiled with
%% DEBUG=true. See also ./erl_pp.erl.

%-define(DEBUG, true).

-type annotation() :: {'file', filename()}
                    | {'generated', generated()}
                    | {'location', location()}
                    | {'record', record()}
                    | {'text', string()}.

-opaque anno() :: location() | [annotation(), ...].
-type anno_term() :: term().

-type column() :: pos_integer().
-type generated() :: boolean().
-type filename() :: file:filename_all().
-type line() :: non_neg_integer().
-type location() :: line() | {line(), column()}.
-type record() :: boolean().
-type text() :: string().

-ifdef(DEBUG).
%% Anything 'false' accepted by the compiler.
-define(ALINE(A), is_reference(A)).
-define(ACOLUMN(A), is_reference(A)).
-else.
-define(ALINE(L), ?LN(L)).
-define(ACOLUMN(C), ?COL(C)).
-endif.

-spec to_term(Anno) -> anno_term() when
      Anno :: anno().

-ifdef(DEBUG).
to_term(Anno) ->
    simplify(Anno).
-else.
to_term(Anno) ->
    Anno.
-endif.

-spec from_term(Term) -> Anno when
      Term :: anno_term(),
      Anno :: anno().

-ifdef(DEBUG).
from_term(Term) when is_list(Term) ->
    Term;
from_term(Line) when is_integer(Line), Line < 0 -> % Before OTP 19
    set_generated(true, new(-Line));
from_term(Term) ->
    [{location, Term}].
-else.
from_term(Line) when is_integer(Line), Line < 0 -> % Before OTP 19
    set_generated(true, new(-Line));
from_term(Term) ->
    Term.
-endif.

-spec new(Location) -> anno() when
      Location :: location().

new(Line) when ?LLINE(Line) ->
    new_location(Line);
new({Line, Column}=Loc) when ?LLINE(Line), ?LCOLUMN(Column) ->
    new_location(Loc);
new(Term) ->
    erlang:error(badarg, [Term]).

-ifdef(DEBUG).
new_location(Location) ->
    [{location, Location}].
-else.
new_location(Location) ->
    Location.
-endif.

-spec is_anno(Term) -> boolean() when
      Term :: any().

is_anno(Line) when ?ALINE(Line) ->
    true;
is_anno({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    true;
is_anno(Anno) ->
    (Anno =/= [] andalso
     is_anno1(Anno) andalso
     lists:keymember(location, 1, Anno)).

is_anno1([{Item, Value}|Anno]) ->
    is_anno2(Item, Value) andalso is_anno1(Anno);
is_anno1(A) ->
    A =:= [].

is_anno2(location, Line) when ?LN(Line) ->
    true;
is_anno2(location, {Line, Column}) when ?LN(Line), ?COL(Column) ->
    true;
is_anno2(generated, true) ->
    true;
is_anno2(file, Filename) ->
    is_filename(Filename);
is_anno2(record, true) ->
    true;
is_anno2(text, Text) ->
    is_string(Text);
is_anno2(_, _) ->
    false.

is_filename(T) ->
    is_list(T) orelse is_binary(T).

is_string(T) ->
    is_list(T).

-spec column(Anno) -> column() | 'undefined' when
      Anno :: anno().

column({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    Column;
column(Line) when ?ALINE(Line) ->
    undefined;
column(Anno) ->
    case location(Anno) of
        {_Line, Column} ->
            Column;
        _Line ->
            undefined
    end.

-spec end_location(Anno) -> location() | 'undefined' when
      Anno :: anno().

end_location(Anno) ->
    case text(Anno) of
        undefined ->
            undefined;
        Text ->
            case location(Anno) of
                {Line, Column} ->
                    end_location(Text, Line, Column);
                Line ->
                    end_location(Text, Line)
            end
    end.

-spec file(Anno) -> filename() | 'undefined' when
      Anno :: anno().

file(Line) when ?ALINE(Line) ->
    undefined;
file({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    undefined;
file(Anno) ->
    anno_info(Anno, file).

-spec generated(Anno) -> generated() when
      Anno :: anno().

generated(Line) when ?ALINE(Line) ->
    false;
generated({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    false;
generated(Anno) ->
    anno_info(Anno, generated, false).

-spec line(Anno) -> line() when
      Anno :: anno().

line(Anno) ->
    case location(Anno) of
        {Line, _Column} ->
            Line;
        Line ->
            Line
    end.

-spec location(Anno) -> location() when
      Anno :: anno().

location(Line) when ?ALINE(Line) ->
    Line;
location({Line, Column}=Location) when ?ALINE(Line), ?ACOLUMN(Column) ->
    Location;
location(Anno) ->
    anno_info(Anno, location).

-spec record(Anno) -> record() when
      Anno :: anno().

record(Line) when ?ALINE(Line) ->
    false;
record({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    false;
record(Anno) ->
    anno_info(Anno, record, false).

-spec text(Anno) -> text() | 'undefined' when
      Anno :: anno().

text(Line) when ?ALINE(Line) ->
    undefined;
text({Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column) ->
    undefined;
text(Anno) ->
    anno_info(Anno, text).

-spec set_file(File, Anno) -> Anno when
      File :: filename(),
      Anno :: anno().

set_file(File, Anno) ->
    set(file, File, Anno).

-spec set_generated(Generated, Anno) -> Anno when
      Generated :: generated(),
      Anno :: anno().

set_generated(Generated, Anno) ->
    set(generated, Generated, Anno).

-spec set_line(Line, Anno) -> Anno when
      Line :: line(),
      Anno :: anno().

set_line(Line, Anno) ->
    case location(Anno) of
        {_Line, Column} ->
            set_location({Line, Column}, Anno);
        _Line ->
            set_location(Line, Anno)
    end.

-spec set_location(Location, Anno) -> Anno when
      Location :: location(),
      Anno :: anno().

set_location(Line, L) when ?ALINE(L), ?LLINE(Line) ->
    new_location(Line);
set_location(Line, {L, Column}) when ?ALINE(L), ?ACOLUMN(Column),
                                     ?LLINE(Line) ->
    new_location(Line);
set_location({L, C}=Loc, Line) when ?ALINE(Line), ?LLINE(L), ?LCOLUMN(C) ->
    new_location(Loc);
set_location({L, C}=Loc, {Line, Column}) when ?ALINE(Line), ?ACOLUMN(Column),
                                              ?LLINE(L), ?LCOLUMN(C) ->
    new_location(Loc);
set_location(Location, Anno) ->
    set(location, Location, Anno).

-spec set_record(Record, Anno) -> Anno when
      Record :: record(),
      Anno :: anno().

set_record(Record, Anno) ->
    set(record, Record, Anno).

-spec set_text(Text, Anno) -> Anno when
      Text :: text(),
      Anno :: anno().

set_text(Text, Anno) ->
    set(text, Text, Anno).

set(Item, Value, Anno) ->
    case {is_settable(Item, Value), Anno} of
        {true, Line} when ?ALINE(Line) ->
            set_anno(Item, Value, [{location, Line}]);
        {true, {L, C}=Location} when ?ALINE(L), ?ACOLUMN(C) ->
            set_anno(Item, Value, [{location, Location}]);
        {true, A} when is_list(A), A =/= [] ->
            set_anno(Item, Value, Anno);
        _ ->
            erlang:error(badarg, [Item, Value, Anno])
    end.

set_anno(Item, Value, Anno) ->
    case default(Item, Value) of
        true ->
            reset(Anno, Item);
        false ->
            R = case anno_info(Anno, Item) of
                    undefined ->
                        [{Item, Value}|Anno];
                    _ ->
                        lists:keyreplace(Item, 1, Anno, {Item, Value})
                end,
            reset_simplify(R)
    end.

reset(Anno, Item) ->
    A = lists:keydelete(Item, 1, Anno),
    reset_simplify(A).

-ifdef(DEBUG).
reset_simplify(A) ->
    A.
-else.
reset_simplify(A) ->
    simplify(A).
-endif.

simplify([{location, Location}]) ->
    Location;
simplify(Anno) ->
    Anno.

anno_info(Anno, Item, Default) ->
    try lists:keyfind(Item, 1, Anno) of
        false ->
            Default;
        {Item, Value} ->
            Value
    catch
        _:_ ->
            erlang:error(badarg, [Anno])
    end.

anno_info(Anno, Item) ->
    try lists:keyfind(Item, 1, Anno) of
        {Item, Value} ->
            Value;
        false ->
            undefined
    catch
        _:_ ->
            erlang:error(badarg, [Anno])
    end.

end_location("", Line, Column) ->
    {Line, Column};
end_location([$\n|String], Line, _Column) ->
    end_location(String, Line+1, 1);
end_location([_|String], Line, Column) ->
    end_location(String, Line, Column+1).

end_location("", Line) ->
    Line;
end_location([$\n|String], Line) ->
    end_location(String, Line+1);
end_location([_|String], Line) ->
    end_location(String, Line).

is_settable(file, File) ->
    is_filename(File);
is_settable(generated, Boolean) when Boolean; not Boolean ->
    true;
is_settable(location, Line) when ?LLINE(Line) ->
    true;
is_settable(location, {Line, Column}) when ?LLINE(Line), ?LCOLUMN(Column) ->
    true;
is_settable(record, Boolean) when Boolean; not Boolean ->
    true;
is_settable(text, Text) ->
    is_string(Text);
is_settable(_, _) ->
    false.

default(generated, false) -> true;
default(record, false) -> true;
default(_, _) -> false.
