%% Copyright (c) Meta Platforms, Inc. and affiliates.
%%
%% 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.
-module(erlfmt_format).

-include("erlfmt_scan.hrl").

-export([to_algebra/1, comments/1, comments_with_pre_dot/1]).

-import(erlfmt_algebra, [
    force_breaks/0,
    group/1,
    format/2,
    string/1,
    empty/0,
    break/0,
    break/1,
    break/2,
    break/3,
    flex_break/1,
    flex_break/0,
    flex_break/2,
    flex_break/3,
    space/2,
    nest/2,
    nest/3,
    line/0,
    line/1,
    line/2,
    concat/1,
    concat/2,
    concat/3,
    next_break_fits/1,
    next_break_fits/2,
    fold_doc/2
]).

-define(INDENT, 4).

-spec to_algebra(erlfmt_parse:any_node()) -> erlfmt_algebra:doc().
to_algebra({shebang, Meta, String}) ->
    Doc = string(String),
    combine_comments(Meta, Doc);
to_algebra({function, Meta, Clauses}) ->
    Doc = clauses_to_algebra(Clauses),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, Name, no_parens}) ->
    Doc = concat(<<"-">>, attribute_to_algebra(Name)),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, {atom, _, define}, [Define, empty]}) ->
    Doc = concat(<<"-define(">>, expr_to_algebra(Define), <<",)">>),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, {atom, _, define}, [Define | Body]}) ->
    HeadD = concat(<<"-define(">>, expr_to_algebra(Define), <<",">>),
    NextBreakFits = length(Body) =:= 1 andalso is_next_break_fits(hd(Body)),
    with_next_break_fits(NextBreakFits, block_to_algebra(Meta, Body), fun(BodyD) ->
        Doc = surround(HeadD, <<" ">>, BodyD, <<"">>, <<")">>),
        combine_comments_with_dot(Meta, Doc)
    end);
to_algebra({attribute, Meta, {atom, _, RawName} = Name, [{list, Anno, Exports}]}) when
    RawName =:= export; RawName =:= export_type
->
    GroupedExports = {list, Anno, fa_groups(Exports)},
    Doc = call(Meta, [GroupedExports], concat(<<"-">>, expr_to_algebra(Name), <<"(">>), <<")">>),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, {atom, _, import}, [Name, {list, Anno, Imports}]}) ->
    GroupedImports = {list, Anno, fa_groups(Imports)},
    Doc = call(Meta, [Name, GroupedImports], <<"-import(">>, <<")">>),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, {atom, _, RawName} = Name, [Value]}) when
    RawName =:= type;
    RawName =:= opaque;
    RawName =:= spec;
    RawName =:= callback;
    RawName =:= nominal
->
    ValueD = next_break_fits(expr_to_algebra(Value), enabled),
    Doc = concat([<<"-">>, expr_to_algebra(Name), <<" ">>, ValueD]),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, {atom, _, record}, [Name, {tuple, TMeta, Values} = Tuple]}) ->
    HeadD = concat(<<"-record(">>, expr_to_algebra(Name), <<",">>),
    case has_no_comments_or_parens(TMeta) of
        true ->
            Doc = space(HeadD, container(TMeta, Values, <<"{">>, <<"})">>)),
            combine_comments_with_dot(Meta, Doc);
        false ->
            Doc = surround(HeadD, <<"">>, expr_to_algebra(Tuple), <<"">>, <<")">>),
            combine_comments_with_dot(Meta, Doc)
    end;
to_algebra({attribute, Meta, {atom, _, RawName} = Name, [Value]}) when
    RawName =:= doc; RawName =:= moduledoc
->
    ValueD = expr_to_algebra(Value),
    Doc = concat([<<"-">>, expr_to_algebra(Name), <<" ">>, ValueD]),
    combine_comments_with_dot(Meta, Doc);
to_algebra({attribute, Meta, Name, Values}) ->
    Doc = concat(<<"-">>, attribute_to_algebra(Name), call(Meta, Values, <<"(">>, <<")">>)),
    combine_comments_with_dot(Meta, Doc);
to_algebra(Expr) ->
    Meta = element(2, Expr),
    Doc = do_expr_to_algebra(Expr),
    case maps:get(dot, Meta, false) of
        true ->
            combine_comments_with_dot(Meta, Doc);
        false ->
            combine_comments(Meta, Doc)
    end.

attribute_to_algebra({atom, Meta, 'else'}) -> string(erlfmt_scan:get_anno(text, Meta));
attribute_to_algebra(Other) -> expr_to_algebra(Other).

expr_to_algebra(Expr) when is_tuple(Expr) ->
    Meta = element(2, Expr),
    Doc = do_expr_to_algebra(Expr),
    combine_comments(Meta, maybe_wrap_in_parens(Meta, Doc));
expr_to_algebra(Other) ->
    do_expr_to_algebra(Other).

do_expr_to_algebra({string, Meta, _Value}) ->
    string_to_algebra(erlfmt_scan:get_anno(text, Meta));
do_expr_to_algebra({char, #{text := "$ "}, $\s}) ->
    <<"$\\s">>;
do_expr_to_algebra({atom, _, 'maybe'}) ->
    string(<<"'maybe'">>);
do_expr_to_algebra({atom, _, 'else'}) ->
    string(<<"'else'">>);
do_expr_to_algebra({Atomic, Meta, _Value}) when ?IS_ATOMIC(Atomic) ->
    string(erlfmt_scan:get_anno(text, Meta));
do_expr_to_algebra({pid, _Meta, Dotted}) ->
    concat(<<"<">>, dotted_to_algebra(Dotted), <<">">>);
do_expr_to_algebra({dotted_special, _Meta, Name, Dotted}) ->
    concat([<<"#">>, expr_to_algebra(Name), <<"<">>, dotted_to_algebra(Dotted), <<">">>]);
do_expr_to_algebra({concat, _Meta, Values}) ->
    concat_to_algebra(Values);
do_expr_to_algebra({op, _Meta, Op, Expr}) ->
    unary_op_to_algebra(Op, Expr);
do_expr_to_algebra({op, Meta, Op, Left, Right}) ->
    binary_op_to_algebra(Op, Meta, Left, Right);
do_expr_to_algebra({tuple, Meta, Values}) ->
    flex_container(Meta, Values, <<"{">>, <<"}">>);
do_expr_to_algebra({list, Meta, Values}) ->
    container(Meta, Values, <<"[">>, <<"]">>);
do_expr_to_algebra({cons, _, Head, Tail}) ->
    cons_to_algebra(Head, Tail);
do_expr_to_algebra({bin, Meta, Values}) ->
    flex_container(Meta, Values, <<"<<">>, <<">>">>);
do_expr_to_algebra({bin_element, _Meta, Expr, Size, Types}) ->
    bin_element_to_algebra(Expr, Size, Types);
do_expr_to_algebra({map, Meta, Values}) ->
    container(Meta, Values, <<"#{">>, <<"}">>);
do_expr_to_algebra({map, Meta, Expr, Values}) ->
    concat(
        wrap_if_integer(expr_to_algebra(Expr), Expr), container(Meta, Values, <<"#{">>, <<"}">>)
    );
do_expr_to_algebra({map_field_assoc, _Meta, Key, Value}) ->
    field_to_algebra(<<"=>">>, Key, Value);
do_expr_to_algebra({map_field_exact, _Meta, Key, Value}) ->
    field_to_algebra(<<":=">>, Key, Value);
do_expr_to_algebra({record, Meta, Name, Values}) ->
    concat(record_name_to_algebra(Meta, Name), container(Meta, Values, <<"{">>, <<"}">>));
do_expr_to_algebra({record, Meta, Expr, Name, Values}) ->
    concat(
        wrap_if_integer(expr_to_algebra(Expr), Expr),
        record_name_to_algebra(Meta, Name),
        container(Meta, Values, <<"{">>, <<"}">>)
    );
do_expr_to_algebra({record_field, _Meta, Key, Value}) ->
    field_to_algebra(<<"=">>, Key, Value);
do_expr_to_algebra({record_index, Meta, Name, Key}) ->
    record_access_to_algebra(Meta, Name, Key);
do_expr_to_algebra({record_field, _Meta, Name}) ->
    expr_to_algebra(Name);
do_expr_to_algebra({record_field, Meta, Expr, Name, Key}) ->
    concat(wrap_if_integer(expr_to_algebra(Expr), Expr), record_access_to_algebra(Meta, Name, Key));
do_expr_to_algebra({record_name, Meta, Name}) ->
    record_name_to_algebra(Meta, Name);
do_expr_to_algebra({lc, _Meta, Expr, LcExprs}) ->
    comprehension_to_algebra(Expr, LcExprs, <<"[">>, <<"]">>);
do_expr_to_algebra({mc, _Meta, Expr, LcExprs}) ->
    comprehension_to_algebra(Expr, LcExprs, <<"#{">>, <<"}">>);
do_expr_to_algebra({bc, _Meta, Expr, LcExprs}) ->
    comprehension_to_algebra(Expr, LcExprs, <<"<<">>, <<">>">>);
do_expr_to_algebra({generate, _Meta, Op, Left, Right}) ->
    field_to_algebra(atom_to_binary(Op), Left, Right);
do_expr_to_algebra({call, Meta, Name, Args}) ->
    concat(expr_to_algebra(Name), call(Meta, Args, <<"(">>, <<")">>));
do_expr_to_algebra({macro_call, _Meta, Name, none}) ->
    concat(<<"?">>, expr_to_algebra(Name));
do_expr_to_algebra({macro_call, Meta, Name, Args}) ->
    concat(<<"?">>, expr_to_algebra(Name), call(Meta, Args, <<"(">>, <<")">>));
do_expr_to_algebra({macro_string, _Meta, Name}) ->
    concat(<<"??">>, expr_to_algebra(Name));
do_expr_to_algebra({remote, _Meta, Left, Right}) ->
    concat(expr_to_algebra(Left), <<":">>, expr_to_algebra(Right));
do_expr_to_algebra({block, Meta, Exprs}) ->
    surround_block(<<"begin">>, Exprs, fun(X) -> block_to_algebra(Meta, X) end, <<"end">>);
do_expr_to_algebra({'fun', _Meta, Expr}) ->
    fun_to_algebra(Expr);
do_expr_to_algebra({args, Meta, Values}) ->
    container(Meta, Values, <<"(">>, <<")">>);
do_expr_to_algebra({'case', _Meta, Expr, Clauses}) ->
    Prefix = surround(<<"case">>, <<" ">>, expr_to_algebra(Expr), <<" ">>, <<"of">>),
    surround_block(Prefix, Clauses, fun clauses_to_algebra/1, <<"end">>);
do_expr_to_algebra({'maybe', _Meta, MaybeExpressions}) ->
    surround_block(<<"maybe">>, MaybeExpressions, fun maybe_expressions_to_algebra/1, <<"end">>);
do_expr_to_algebra({'maybe', _Meta, MaybeExpressions, Else}) ->
    ElseD = expr_to_algebra(Else),
    surround_block(
        <<"maybe">>, MaybeExpressions, fun maybe_expressions_to_algebra/1, line(ElseD, <<"end">>)
    );
do_expr_to_algebra({else_clause, _Meta, Clauses}) ->
    concat(
        force_breaks(),
        group(concat(<<"else">>, nest(concat(line(), clauses_to_algebra(Clauses)), ?INDENT)))
    );
do_expr_to_algebra({'receive', _Meta, Clauses}) ->
    surround_block(<<"receive">>, Clauses, fun expr_to_algebra/1, <<"end">>);
do_expr_to_algebra({'receive', _Meta, Clauses, After}) ->
    AfterD = expr_to_algebra(After),
    surround_block(<<"receive">>, Clauses, fun expr_to_algebra/1, line(AfterD, <<"end">>));
do_expr_to_algebra({after_clause, _Meta, Expr, Body}) ->
    receive_after_to_algebra(Expr, Body);
do_expr_to_algebra({'try', _Meta, Exprs, OfClauses, CatchClauses, After}) ->
    try_to_algebra(Exprs, OfClauses, CatchClauses, After);
do_expr_to_algebra({'catch', _Meta, Exprs}) ->
    ExprsD = lists:map(fun expr_to_algebra/1, Exprs),
    fold_doc(fun(Doc, Acc) -> concat(Doc, <<":">>, Acc) end, ExprsD);
do_expr_to_algebra({'if', _Meta, Clauses}) ->
    surround_block(<<"if">>, Clauses, fun clauses_to_algebra/1, <<"end">>);
do_expr_to_algebra({spec, _Meta, Name, [SingleClause]}) ->
    single_clause_spec_to_algebra(Name, SingleClause);
do_expr_to_algebra({spec, _Meta, Name, Clauses}) ->
    group(nest(line(expr_to_algebra(Name), clauses_to_algebra(Clauses)), ?INDENT));
do_expr_to_algebra({'...', _Meta}) ->
    <<"...">>;
do_expr_to_algebra({bin_size, _Meta, Left, Right}) ->
    concat(expr_to_algebra(Left), <<"*">>, expr_to_algebra(Right));
do_expr_to_algebra({Clause, _, _, _, _} = Expr) when Clause =:= clause; Clause =:= spec_clause ->
    clause_to_algebra(Expr);
do_expr_to_algebra({guard_or, _Meta, Guards}) ->
    guards_to_algebra(Guards, <<";">>);
do_expr_to_algebra({guard_and, _Meta, Guards}) ->
    guards_to_algebra(Guards, <<",">>);
do_expr_to_algebra({fa_group, _Meta, GroupedExports}) ->
    fa_group_to_algebra(GroupedExports);
do_expr_to_algebra({exprs, _Meta, Exprs}) ->
    block_to_algebra(Exprs);
do_expr_to_algebra({clauses, _Meta, Clauses}) ->
    clauses_to_algebra(Clauses);
do_expr_to_algebra({body, _Meta, Exprs}) ->
    block_to_algebra(Exprs);
do_expr_to_algebra({sigil, _Meta, Prefix, {string, SMeta, _}, Suffix}) ->
    PrefixDoc = concat(<<"~">>, do_expr_to_algebra(Prefix)),
    ContentDoc = string(erlfmt_scan:get_anno(text, SMeta)),
    SigilDoc = concat(concat(PrefixDoc, ContentDoc), do_expr_to_algebra(Suffix)),
    case is_multiline_string(SMeta) of
        true -> concat(force_breaks(), SigilDoc);
        false -> SigilDoc
    end;
do_expr_to_algebra({sigil_prefix, _Meta, ''}) ->
    <<"">>;
do_expr_to_algebra({sigil_prefix, _Meta, SigilName}) ->
    atom_to_binary(SigilName, utf8);
do_expr_to_algebra({sigil_suffix, _Meta, []}) ->
    <<>>;
do_expr_to_algebra({sigil_suffix, _Meta, Modifiers}) ->
    list_to_binary(Modifiers);
do_expr_to_algebra(Other) ->
    error(unsupported, [Other]).

surround(Left, LeftSpace, Doc, RightSpace, Right) ->
    group(
        break(
            concat(Left, nest(concat(break(LeftSpace), Doc), ?INDENT, break)),
            RightSpace,
            Right
        )
    ).

surround_block(Left, Empty, _Mapper, Right) when Empty =:= []; Empty =:= empty ->
    line(Left, Right);
surround_block(Left, Clauses, Mapper, Right) ->
    Doc = Mapper(Clauses),
    concat(
        force_breaks(),
        group(line(concat(Left, nest(concat(line(), Doc), ?INDENT)), Right))
    ).

string_to_algebra(Text) ->
    case string:split(Text, "\n", all) of
        ["\"\"\"" ++ _ = Quote | Rest] ->
            tripple_quoted_to_algebra(Quote, Rest);
        [Line] ->
            string(Line);
        [First, "\""] ->
            string([First | "\\n\""]);
        [First | Lines] ->
            FirstD = string([First | "\\n\""]),
            LinesD = string_lines_to_algebra(Lines),
            concat([force_breaks(), FirstD, line(), LinesD])
    end.

%% tripple-quoted string might have actually more than tripple quotes
%% indentation to remove from each line is given from the closing parenteses per documentation
%% see: https://www.erlang.org/doc/system/data_types#string
tripple_quoted_to_algebra(Quote, LinesWithFinalQuote) ->
    {Lines, FinalQuote} = splitlast(LinesWithFinalQuote, []),
    IndentToRemove = calculate_indentation(Quote, FinalQuote, []),
    CleanedLinesD =
        [Quote] ++ [format_tripple_line(Line, IndentToRemove) || Line <- Lines] ++ [Quote],
    join_tripple_lines(CleanedLinesD, 1).

splitlast([T], Acc) -> {lists:reverse(Acc), T};
splitlast([H | T], Acc) -> splitlast(T, [H | Acc]).

calculate_indentation(Quote, Quote, Acc) -> lists:reverse(Acc);
calculate_indentation(Quote, [I | Rest], Acc) -> calculate_indentation(Quote, Rest, [I | Acc]).

%% empty lines have no indentation
format_tripple_line([], _Indent) -> [];
format_tripple_line(Line, Indent) -> remove_indentation(Line, Indent).

remove_indentation(Line, []) -> Line;
remove_indentation([H | LineRest], [H | IndentRest]) -> remove_indentation(LineRest, IndentRest).

join_tripple_lines([Line], 1) ->
    string(Line);
join_tripple_lines([Line, [] | Rest], EmptyLines) ->
    join_tripple_lines([Line | Rest], EmptyLines + 1);
join_tripple_lines([Line | Rest], EmptyLines) ->
    concat(string(Line), line(EmptyLines), join_tripple_lines(Rest, 1)).

string_lines_to_algebra([LastLine]) ->
    string(["\"" | LastLine]);
string_lines_to_algebra([Line, "\""]) ->
    string(["\"", Line | "\\n\""]);
string_lines_to_algebra([Line | Lines]) ->
    LineD = string(["\"", Line | "\\n\""]),
    line(LineD, string_lines_to_algebra(Lines)).

%% there's always at least two elements in concat
concat_to_algebra([Value1, Value2 | _] = Values) ->
    ValuesD = lists:map(fun expr_to_algebra/1, Values),
    case has_break_between(Value1, Value2) of
        true ->
            concat(force_breaks(), group(fold_doc(fun erlfmt_algebra:line/2, ValuesD)));
        false ->
            group(fold_doc(fun erlfmt_algebra:break/2, ValuesD))
    end.

dotted_to_algebra([LastSegment]) ->
    expr_to_algebra(LastSegment);
dotted_to_algebra([Segment | Rest]) ->
    concat(expr_to_algebra(Segment), <<".">>, dotted_to_algebra(Rest)).

unary_op_to_algebra(Op, Expr) ->
    OpD = string(atom_to_binary(Op, utf8)),
    ExprD = expr_to_algebra(Expr),
    NeedsSpace = unary_needs_space(Expr, Op),
    if
        NeedsSpace ->
            space(OpD, ExprD);
        true ->
            concat(OpD, ExprD)
    end.

unary_needs_space(_, Op) when Op =:= 'not'; Op =:= 'bnot'; Op =:= 'catch' ->
    true;
unary_needs_space({op, Meta, _, _}, _) ->
    not erlfmt_scan:get_anno(parens, Meta, false);
unary_needs_space(_, _) ->
    false.

field_to_algebra(Op, Left, Right) ->
    LeftD = expr_to_algebra(Left),
    RightD = expr_to_algebra(Right),
    field_to_algebra(Op, Left, Right, LeftD, RightD, ?INDENT).

binary_op_to_algebra('..', _Meta, Left, Right) ->
    %% .. is special - non-assoc, no spaces around and never breaks
    concat(expr_to_algebra(Left), <<"..">>, expr_to_algebra(Right));
binary_op_to_algebra('/', _Meta, {atom, _, _} = Left, {integer, _, _} = Right) ->
    %% special-case the foo/N syntax in attributes, division with a literal atom
    %% doesn't make sense in normal code, so it's safe to apply it everywhere
    concat(expr_to_algebra(Left), <<"/">>, expr_to_algebra(Right));
binary_op_to_algebra('|', Meta, Left, Right) ->
    %% | does not increase indentation and breaks as a whole
    union_to_algebra(Meta, Left, Right);
binary_op_to_algebra(Op, Meta0, Left0, Right0) ->
    %% don't print parens and comments twice - expr_to_algebra took care of it already
    Meta = erlfmt_scan:delete_annos([parens, pre_comments, post_comments], Meta0),
    {op, _Meta, Op, Left, Right} = rewrite_assoc(Op, Meta, Left0, Right0),
    binary_op_to_algebra(Op, Meta, Left, Right, ?INDENT).

rewrite_assoc('=' = Op, Meta, Left, Right) ->
    {op, Meta, Op, Left, Right};
rewrite_assoc('::' = Op, Meta, Left, Right) ->
    {op, Meta, Op, Left, Right};
rewrite_assoc(Op, MetaABC0, A, {op, MetaBC0, Op, B0, C} = BC) ->
    case erlfmt_scan:get_anno(parens, MetaBC0, false) of
        true ->
            {op, MetaABC0, Op, A, BC};
        _ ->
            {Pre, Post} = comments(MetaBC0),
            MetaBC = erlfmt_scan:delete_annos([pre_comments, post_comments], MetaBC0),
            B = erlfmt_recomment:put_pre_comments(B0, Pre),
            AB = rewrite_assoc(Op, update_meta_location(MetaBC, A, B), A, B),
            MetaABC = erlfmt_recomment:put_post_comments(MetaABC0, Post),
            rewrite_assoc(Op, update_meta_location(MetaABC, AB, C), AB, C)
    end;
rewrite_assoc(Op, Meta, Left, Right) ->
    {op, Meta, Op, Left, Right}.

update_meta_location(Meta, First, Last) ->
    Meta#{
        location := erlfmt_scan:get_anno(location, First),
        end_location := erlfmt_scan:get_anno(end_location, Last)
    }.

binary_operand_to_algebra(Op, {op, Meta, Op, Left, Right}, Indent) ->
    %% Same operator, no parens, means correct side and no repeated nesting
    case erlfmt_scan:get_anno(parens, Meta, false) of
        false -> binary_op_to_algebra(Op, Meta, Left, Right, Indent);
        _ -> binary_op_to_algebra(Op, Meta, Left, Right, ?INDENT)
    end;
binary_operand_to_algebra(_ParentOp, Expr, _Indent) ->
    expr_to_algebra(Expr).

binary_op_to_algebra(Op, Meta, Left, Right, Indent) ->
    LeftD = binary_operand_to_algebra(Op, Left, Indent),
    RightD = binary_operand_to_algebra(Op, Right, 0),
    Doc =
        case Op of
            '::' ->
                field_to_algebra(<<"::">>, Left, Right, LeftD, RightD, Indent);
            '=' ->
                field_to_algebra(<<"=">>, Left, Right, LeftD, RightD, Indent);
            %% when a pattern is in a clause and it breaks we want to prevent issue #211
            {clause_op, '='} ->
                field_to_algebra(<<"=">>, Left, Right, LeftD, RightD, Indent + ?INDENT);
            {clause_op, ClauseOp} ->
                OpD = string(atom_to_binary(ClauseOp, utf8)),
                breakable_binary_op_to_algebra(OpD, Left, Right, LeftD, RightD, Indent + ?INDENT);
            _ ->
                OpD = string(atom_to_binary(Op, utf8)),
                breakable_binary_op_to_algebra(OpD, Left, Right, LeftD, RightD, Indent)
        end,
    combine_comments(Meta, maybe_wrap_in_parens(Meta, Doc)).

field_to_algebra(OpD, Left, Right, LeftD, RightD, Indent) ->
    case
        (is_call(Right) orelse is_next_break_fits(Right)) andalso
            not has_break_between(Left, Right)
    of
        true ->
            with_next_break_fits(true, RightD, fun(R) ->
                concat([group(LeftD), <<" ">>, OpD, <<" ">>, group(R)])
            end);
        false ->
            breakable_binary_op_to_algebra(OpD, Left, Right, LeftD, RightD, Indent)
    end.

is_call({call, _, _, _}) -> true;
is_call({macro_call, _, _, _}) -> true;
is_call(_) -> false.

breakable_binary_op_to_algebra(OpD, Left, Right, LeftD, RightD, Indent) ->
    HasBreak = has_break_between(Left, Right),
    RightOpD = nest(break(concat(<<" ">>, OpD), group(RightD)), Indent, break),
    concat(group(LeftD), group(concat(maybe_force_breaks(HasBreak), RightOpD))).

union_to_algebra(Meta, Left, Right) ->
    Doc = break(expr_to_algebra(Left), fold_unions(Right)),
    group(concat(maybe_force_breaks(is_multiline(Meta)), Doc)).

fold_unions({op, Meta, '|', Left, Right}) ->
    LeftD = expr_to_algebra(Left),
    LeftPipeD = concat(<<"| ">>, LeftD),
    Doc = combine_comments(Meta, LeftPipeD),
    break(Doc, fold_unions(Right));
fold_unions(Expr) ->
    Meta = element(2, Expr),
    ExprD = do_expr_to_algebra(Expr),
    ExprPipeD = concat(<<"| ">>, maybe_wrap_in_parens(Meta, ExprD)),
    combine_comments(Meta, ExprPipeD).

maybe_force_breaks(true) -> force_breaks();
maybe_force_breaks(false) -> empty().

with_next_break_fits(true, Doc, Fun) ->
    next_break_fits(Fun(next_break_fits(Doc, enabled)), disabled);
with_next_break_fits(false, Doc, Fun) ->
    Fun(Doc).

container(Meta, Values, Left, Right) ->
    container_common(Meta, Values, Left, Right, break, last_normal).

flex_container(Meta, Values, Left, Right) ->
    container_common(Meta, Values, Left, Right, flex_break, last_fits).

call(Meta, Values, Left, Right) ->
    container_common(Meta, Values, Left, Right, break, last_fits).

container_common(_Meta, [], Left, Right, _, _) ->
    concat(Left, Right);
container_common(Meta, Values, Left, Right, BreakKind, LastFits) ->
    LastFitsFun = last_fits_fun(LastFits, Values),
    BreakBehaviour = break_behaviour(Meta, Values, BreakKind),
    BreakFun = break_fun(BreakBehaviour),
    ValuesVD = map_inner_values(LastFitsFun, Values),
    Doc = join_inner_values(BreakFun, ValuesVD),
    Surrounded = surround_container(BreakBehaviour, Left, Doc, Right),
    LastFitsFun(Surrounded, disabled).

-record(break, {outer :: boolean(), inner :: boolean()}).

-type break_behaviour() :: flex_break | no_break | #break{}.

break_behaviour(Meta, Values, Break) ->
    HasOpeningLineBreak = has_opening_line_break(Meta, Values),
    HasTrailingComments = has_trailing_comments(Values),
    ForceOuter = HasOpeningLineBreak orelse HasTrailingComments,
    case Break =:= flex_break andalso not ForceOuter of
        true ->
            case Values of
                [{bin_element, _, {string, SMeta, _}, _, _}] ->
                    case is_multiline_string(SMeta) of
                        true -> #break{outer = false, inner = false};
                        false -> no_break
                    end;
                [SoleElement] ->
                    case is_container(SoleElement) of
                        true -> no_break;
                        false -> #break{outer = false, inner = false}
                    end;
                [First | _] ->
                    case is_tag(First) of
                        true -> flex_break;
                        false -> #break{outer = false, inner = false}
                    end;
                _ ->
                    flex_break
            end;
        false ->
            HasInnerBreaks = has_any_break_between(Values),
            ForceInner = HasInnerBreaks orelse HasTrailingComments,
            #break{outer = ForceOuter, inner = ForceInner}
    end.

%% Allow inlining binary literals
is_tag({bin, _, [{bin_element, _, {string, SMeta, _}, _, _}]}) ->
    not is_multiline_string(SMeta);
is_tag({bin, _, []}) ->
    true;
%% Allow inlining argless macros
is_tag({macro_call, _, _, Args}) when Args =:= none; Args =:= [] ->
    true;
is_tag({bin_element, _, Elem, _, _}) ->
    is_tag(Elem);
is_tag(Expr) ->
    lists:member(element(1, Expr), [atom, string, var, integer, float]).

is_container(Expr) ->
    lists:member(element(1, Expr), [tuple, bin, list, map, record, call, macro_call, lc, bc]).

is_multiline_string(Meta) ->
    Text = erlfmt_scan:get_anno(text, Meta),
    case string:split(Text, "\n", all) of
        [_] -> false;
        [_, "\""] -> false;
        _ -> true
    end.

surround_container(no_break, Left, Doc, Right) ->
    concat(Left, Doc, Right);
surround_container(flex_break, Left, Doc, Right) ->
    group(concat(nest(concat(Left, Doc), ?INDENT, break), Right));
surround_container(#break{inner = ForceInner, outer = ForceOuter}, Left, Doc, Right) ->
    InnerDoc = group(concat(maybe_force_breaks(ForceInner), Doc)),
    surround(Left, <<"">>, concat(maybe_force_breaks(ForceOuter), InnerDoc), <<"">>, Right).

%% last_fits_fun returns a fun similar to next_break_fits/2
%% that takes into account the desired fits behaviour.
-spec last_fits_fun(last_normal | last_fits, [erlfmt_parse:any_node()]) ->
    fun((erlfmt_algebra:doc(), disabled | enabled) -> erlfmt_algebra:doc()).
last_fits_fun(last_fits, Values) when Values =/= [] ->
    case is_next_break_fits(lists:last(Values)) of
        true -> fun erlfmt_algebra:next_break_fits/2;
        false -> fun(Doc, _) -> Doc end
    end;
last_fits_fun(_, _) ->
    fun(Doc, _) -> Doc end.

has_trailing_comments([]) ->
    false;
has_trailing_comments(Values) ->
    case lists:last(Values) of
        {comment, _, _} ->
            true;
        LastValue ->
            {_, PostComments} = comments(LastValue),
            PostComments =/= []
    end.

-spec has_opening_line_break(erlfmt_scan:anno(), [erlfmt_parse:any_node()]) -> boolean().
has_opening_line_break(_Meta, []) ->
    false;
has_opening_line_break(Meta, [HeadValue | _]) ->
    has_inner_break(Meta, HeadValue).

has_any_break_between([Head | [Head2 | _] = Tail]) ->
    has_break_between(Head, Head2) orelse has_any_break_between(Tail);
has_any_break_between([{cons, _, Head, Tail}]) ->
    has_break_between(Head, Tail);
has_any_break_between(_) ->
    false.

-spec break_fun(break_behaviour()) -> erlfmt_algebra:append_fun().
break_fun(flex_break) -> fun erlfmt_algebra:flex_break/2;
break_fun(_) -> fun erlfmt_algebra:break/2.

-type value_doc_pair() :: {erlfmt_parse:any_node(), erlfmt_algebra:doc()}.

map_inner_values(_LastFitsFun, []) ->
    [];
map_inner_values(_LastFitsFun, [{comment, _, _} = FirstComment | _] = Comments) ->
    [{FirstComment, comments_to_algebra(Comments)}];
map_inner_values(LastFitsFun, [Value]) ->
    [{Value, LastFitsFun(expr_to_algebra(Value), enabled)}];
map_inner_values(Last, [Value | Values]) ->
    [{Value, expr_to_algebra(Value)} | map_inner_values(Last, Values)].

%% join_inner_values combines pairs of values and their document representation into one document.
%% It preserves empty lines and adds commas between documents.
-spec join_inner_values(erlfmt_algebra:append_fun(), [value_doc_pair()]) -> erlfmt_algebra:doc().
join_inner_values(_BreakFun, [{_, ValueD}]) ->
    ValueD;
join_inner_values(BreakFun, [{Value, ValueD} | [{Value2, _ValueD2} | _] = ValuesVD]) ->
    case has_empty_line_between(Value, Value2) of
        true -> concat([ValueD, <<",">>, line(2), join_inner_values(BreakFun, ValuesVD)]);
        false -> BreakFun(concat(ValueD, <<",">>), join_inner_values(BreakFun, ValuesVD))
    end.

% fa_group_to_algebra, see fa_groups/1 that creates function/arity groups.
fa_group_to_algebra([Value1, Value2 | _] = Values) ->
    ValuesD = lists:map(fun expr_to_algebra/1, Values),
    {MaybeForceBreaks, BreakType} =
        case has_break_between(Value1, Value2) of
            true -> {force_breaks(), line()};
            false -> {empty(), break()}
        end,
    Folder = fun(Value, Acc) -> concat([Value, <<",">>, BreakType, Acc]) end,
    concat(MaybeForceBreaks, group(fold_doc(Folder, ValuesD))).

% fa_groups groups together function/arity for functions with the same name
% for example: -export([a/1, a/2, b/1]) => -export([{fa_group, _, [a/1, a/2]}, b/1])
fa_groups([]) ->
    [];
fa_groups([{op, Meta0, '/', {atom, _, HeadFunction}, _} = Head0 | Tail]) ->
    Splitter = fun
        ({op, _, '/', {atom, _, Function}, _}) -> Function =:= HeadFunction;
        (_) -> false
    end,
    case lists:splitwith(Splitter, Tail) of
        {[], Rest} ->
            [Head0 | fa_groups(Rest)];
        {Group, Rest} ->
            Head = erlfmt_scan:delete_annos([pre_comments, post_comments], Head0),
            Meta = erlfmt_scan:range_anno(Meta0, lists:last(Group)),
            [{fa_group, Meta, [Head | Group]} | fa_groups(Rest)]
    end;
fa_groups([Other | Tail]) ->
    [Other | fa_groups(Tail)].

cons_to_algebra(Head, Tail) ->
    HeadD = expr_to_algebra(Head),
    TailD = concat(<<"| ">>, maybe_wrap_in_parens(Tail, do_expr_to_algebra(Tail))),
    break(HeadD, combine_comments(Tail, TailD)).

bin_element_to_algebra(Expr, Size, Types) ->
    Docs =
        [expr_to_algebra(Expr)] ++
            [bin_size_to_algebra(Size) || Size =/= default] ++
            [bin_types_to_algebra(Types) || Types =/= default],
    concat(Docs).

bin_size_to_algebra(Expr) ->
    concat(<<":">>, expr_to_algebra(Expr)).

bin_types_to_algebra(Types) ->
    TypesD = lists:map(fun expr_to_algebra/1, Types),
    concat(<<"/">>, fold_doc(fun(Doc, Acc) -> concat([Doc, <<"-">>, Acc]) end, TypesD)).

record_access_to_algebra(Meta, Name, Key) ->
    concat(record_name_to_algebra(Meta, Name), <<".">>, expr_to_algebra(Key)).

record_name_to_algebra(Meta, Name) ->
    %% Differentiate between #?FOO{} and ?FOO{}
    case erlfmt_scan:get_anno(macro_record, Meta, false) of
        true -> expr_to_algebra(Name);
        false -> concat(<<"#">>, expr_to_algebra(Name))
    end.

comprehension_to_algebra(Expr, [LcExpr | _] = LcExprs, Left, Right) ->
    ExprD = expr_to_algebra(Expr),
    LcExprsD = lists:map(fun expr_to_algebra/1, LcExprs),
    LcExprD = fold_doc(fun(D, Acc) -> break(concat(D, <<",">>), Acc) end, LcExprsD),
    PostBreak = maybe_force_breaks(has_any_break_between(LcExprs)),
    PreBreak = concat(maybe_force_breaks(has_break_between(Expr, LcExpr)), break(<<"">>)),
    IndentExpr = nest(concat(break(<<"">>), ExprD), ?INDENT, break),
    IdentLcs = nest(group(concat(PostBreak, LcExprD)), ?INDENT),
    group(concat([Left, IndentExpr, PreBreak, <<" || ">>, IdentLcs, break(<<"">>), Right])).

block_to_algebra([Expr]) ->
    expr_to_algebra(Expr);
block_to_algebra(Exprs) ->
    concat(force_breaks(), block_to_algebra_each(Exprs)).

block_to_algebra(Meta, [Expr]) ->
    concat(maybe_force_breaks(has_inner_break(Meta, Expr)), expr_to_algebra(Expr));
block_to_algebra(_Meta, Exprs) ->
    concat(force_breaks(), block_to_algebra_each(Exprs)).

%% standalone comments are always trailing other expressions
block_to_algebra_each([Expr]) ->
    expr_to_algebra(Expr);
block_to_algebra_each([Expr | [Next | _] = Rest]) ->
    ExprD = expr_to_algebra(Expr),
    RestD = block_to_algebra_each(Rest),
    case has_empty_line_between(Expr, Next) of
        true -> concat([ExprD, <<",">>, line(2), RestD]);
        false -> concat([ExprD, <<",">>, line(), RestD])
    end.

fun_to_algebra({function, _Anno, Name, Arity}) ->
    concat([
        <<"fun ">>,
        expr_to_algebra(Name),
        <<"/">>,
        expr_to_algebra(Arity)
    ]);
fun_to_algebra({function, _Anno, Mod, Name, Arity}) ->
    concat([
        <<"fun ">>,
        expr_to_algebra(Mod),
        <<":">>,
        expr_to_algebra(Name),
        <<"/">>,
        expr_to_algebra(Arity)
    ]);
fun_to_algebra({clauses, _Anno, [Clause]}) ->
    ClauseD = concat(maybe_force_breaks(clause_has_break(Clause)), clause_expr_to_algebra(Clause)),
    case Clause of
        {clause, _, {args, _, _}, _, _} -> group(break(concat(<<"fun">>, ClauseD), <<"end">>));
        _ -> group(break(space(<<"fun">>, ClauseD), <<"end">>))
    end;
fun_to_algebra({clauses, _Anno, Clauses}) ->
    surround_block(<<"fun">>, Clauses, fun clauses_to_algebra/1, <<"end">>);
fun_to_algebra(type) ->
    <<"fun()">>;
fun_to_algebra({type, Meta, Args, Result}) ->
    ArgsD = expr_to_algebra(Args),
    Doc0 =
        with_next_break_fits(is_next_break_fits(Result), expr_to_algebra(Result), fun(ResultD) ->
            concat(ArgsD, group(nest(break(<<" ->">>, ResultD), ?INDENT, break)))
        end),
    Doc = combine_comments(Meta, Doc0),
    surround(<<"fun(">>, <<"">>, Doc, <<"">>, <<")">>).

clauses_to_algebra([Clause | _] = Clauses) ->
    ClausesD = fold_clauses_to_algebra(Clauses),
    HasBreak = clause_has_break(Clause),
    group(concat(maybe_force_breaks(HasBreak), ClausesD)).

fold_clauses_to_algebra([Clause]) ->
    clause_expr_to_algebra(Clause);
fold_clauses_to_algebra([Clause | Clauses]) ->
    ClauseD = clause_expr_to_algebra(Clause),
    line(concat(ClauseD, <<";">>), fold_clauses_to_algebra(Clauses)).

clause_has_break({clause, _Meta, empty, Guards, [Body | _]}) ->
    has_break_between(Guards, Body);
clause_has_break({clause, _Meta, Head, _Guards, [Body | _]}) ->
    has_break_between(Head, Body);
clause_has_break({spec_clause, _Meta, Head, Body, _Guards}) ->
    has_break_between(Head, Body);
clause_has_break({macro_call, _Meta, _Name, _Args}) ->
    false.

clause_expr_to_algebra({macro_call, Meta, _, _} = Expr) ->
    ExprD = do_expr_to_algebra(Expr),
    combine_comments_no_force(Meta, ExprD);
clause_expr_to_algebra(Clause) ->
    Meta = element(2, Clause),
    ClauseD = clause_to_algebra(Clause),
    combine_comments_no_force(Meta, ClauseD).

clause_to_algebra({clause, _Meta, Head, empty, Body}) ->
    HeadD = clause_head_to_algebra(Head),
    BodyD = block_to_algebra(Body),
    space(HeadD, nest(break(<<"->">>, BodyD), ?INDENT));
clause_to_algebra({spec_clause, _Meta, Head, Body, empty}) ->
    HeadD = clause_head_to_algebra(Head),
    BodyD = expr_to_algebra(Body),
    MaybeForce = maybe_force_breaks(has_break_between(Head, Body)),
    space(HeadD, group(nest(concat([MaybeForce, <<"->">>, break(<<" ">>), BodyD]), ?INDENT)));
clause_to_algebra({clause, _Meta, empty, Guards, Body}) ->
    GuardsD = expr_to_algebra(Guards),
    BodyD = block_to_algebra(Body),
    space(GuardsD, nest(break(<<"->">>, BodyD), ?INDENT));
clause_to_algebra({clause, Meta, Head, Guards, Body}) ->
    HeadD = clause_head_to_algebra(Head),
    GuardsD = expr_to_algebra(Guards),
    BodyD = block_to_algebra(Meta, Body),
    MaybeForceHeadGuards = maybe_force_breaks(has_break_between(Head, Guards)),
    MaybeForceGuardsBody = maybe_force_breaks(has_break_between(Guards, Body)),
    Nested = fun(Doc) -> nest(concat(break(<<" ">>), Doc), ?INDENT) end,
    GuardsArrowD = group(concat([MaybeForceHeadGuards, Nested(GuardsD), break(<<" ">>), <<"->">>])),
    concat(
        space(HeadD, <<"when">>),
        group(concat(GuardsArrowD, MaybeForceGuardsBody, Nested(BodyD)))
    );
clause_to_algebra({spec_clause, _Meta, Head, Body, Guards}) ->
    HeadD = clause_head_to_algebra(Head),
    GuardsD = spec_clause_gaurds_to_algebra(Guards),
    BodyD = expr_to_algebra(Body),
    MaybeForceHeadBody = maybe_force_breaks(has_break_between(Head, Body)),
    MaybeForceBodyGuards = maybe_force_breaks(has_break_between(Body, Guards)),
    Nested = fun(Doc) -> nest(concat(break(<<" ">>), Doc), ?INDENT) end,
    BodyWhenD = group(concat([MaybeForceHeadBody, Nested(BodyD), break(<<" ">>), <<"when">>])),
    concat(
        space(HeadD, <<"->">>),
        group(concat(BodyWhenD, MaybeForceBodyGuards, Nested(GuardsD)))
    ).

clause_head_to_algebra({op, Meta, Op, Left, Right}) ->
    expr_to_algebra({op, Meta, {clause_op, Op}, Left, Right});
clause_head_to_algebra(Head) ->
    expr_to_algebra(Head).

maybe_expressions_to_algebra([Expression]) ->
    expr_to_algebra(Expression);
maybe_expressions_to_algebra([Expression | [Next | _] = Rest]) ->
    ExprD = expr_to_algebra(Expression),
    RestD = maybe_expressions_to_algebra(Rest),
    case has_empty_line_between(Expression, Next) of
        true -> concat([ExprD, <<",">>, line(2), RestD]);
        false -> line(concat(ExprD, <<",">>), RestD)
    end.

spec_clause_gaurds_to_algebra(Expr) ->
    Meta = element(2, Expr),
    Doc =
        case Expr of
            {guard_or, _Meta, Guards} -> spec_guards_to_algebra(Guards, <<";">>);
            {guard_and, _Meta, Guards} -> spec_guards_to_algebra(Guards, <<",">>);
            Other -> do_expr_to_algebra(Other)
        end,
    combine_comments(Meta, maybe_wrap_in_parens(Meta, Doc)).

spec_guards_to_algebra(Guards, Separator) ->
    guards_to_algebra(Guards, Separator, fun spec_clause_gaurds_to_algebra/1).

guards_to_algebra(Guards, Separator) ->
    guards_to_algebra(Guards, Separator, fun expr_to_algebra/1).

guards_to_algebra(Guards, Separator, GuardToAlgebra) ->
    GuardsD = lists:map(GuardToAlgebra, Guards),
    Doc = fold_doc(fun(GuardD, Acc) -> break(concat(GuardD, Separator), Acc) end, GuardsD),
    group(concat(maybe_force_breaks(has_any_break_between(Guards)), Doc)).

% %% Because the spec syntax is different from regular function syntax,
% %% in the general case we have to indent them differently, but with just
% %% one clause we can indent them like functions, which seems more natural.
single_clause_spec_to_algebra(Name, {spec_clause, CMeta, Head, Body, Guards}) ->
    {args, AMeta, Args} = Head,
    clauses_to_algebra([{spec_clause, CMeta, {call, AMeta, Name, Args}, Body, Guards}]).

receive_after_to_algebra(Expr, Body) ->
    ExprD = expr_to_algebra(Expr),
    BodyD = block_to_algebra(Body),
    Nest = fun(List) -> group(nest(concat(break(), concat(List)), ?INDENT, break)) end,
    case has_comments(Expr) of
        false ->
            MaybeForceBreaks = maybe_force_breaks(has_break_between(Expr, Body)),
            concat([<<"after">>, <<" ">>, ExprD, <<" ->">>, Nest([MaybeForceBreaks, BodyD])]);
        true ->
            concat([<<"after">>, Nest([ExprD, <<" ->">>, Nest([BodyD])])])
    end.

try_to_algebra(Body, OfClauses, CatchClauses, After) ->
    Clauses =
        [try_of_block(Body, OfClauses)] ++
            [try_catch_to_algebra(CatchClauses) || CatchClauses =/= none] ++
            [try_after_to_algebra(After) || After =/= []] ++
            [<<"end">>],
    concat(force_breaks(), (group(fold_doc(fun erlfmt_algebra:line/2, Clauses)))).

try_catch_to_algebra(Clauses) ->
    group(nest(line(<<"catch">>, expr_to_algebra(Clauses)), ?INDENT)).

try_after_to_algebra(Exprs) ->
    ExprsD = block_to_algebra(Exprs),
    group(nest(line(<<"after">>, ExprsD), ?INDENT)).

try_of_block(Body, OfClauses) ->
    BodyD = expr_to_algebra(Body),

    case OfClauses of
        none ->
            group(nest(line(<<"try">>, BodyD), ?INDENT));
        _ ->
            concat(
                surround(<<"try">>, <<" ">>, BodyD, <<" ">>, <<"of">>),
                nest(concat(line(), expr_to_algebra(OfClauses)), ?INDENT)
            )
    end.

is_multiline(Node) ->
    erlfmt_scan:get_inner_line(Node) =/= erlfmt_scan:get_inner_end_line(Node).

has_break_between(Left, [Right | _]) ->
    has_break_between(Left, Right);
has_break_between(Left, Right) ->
    erlfmt_scan:get_end_line(Left) < erlfmt_scan:get_line(Right).

has_empty_line_between(Left, Right) ->
    erlfmt_scan:get_end_line(Left) + 1 < erlfmt_scan:get_line(Right).

has_inner_break(Outer, Inner) ->
    erlfmt_scan:get_inner_line(Outer) < erlfmt_scan:get_line(Inner).

is_next_break_fits({FlexContainer, Meta, Values}) when
    FlexContainer =:= tuple; FlexContainer =:= bin
->
    BreakBehaviour = break_behaviour(Meta, Values, flex_break),
    is_record(BreakBehaviour, break) andalso has_no_comments_or_parens(Meta);
is_next_break_fits(Expr) ->
    lists:member(element(1, Expr), [map, list, record, block, 'fun', lc, bc]) andalso
        has_no_comments_or_parens(Expr).

has_no_comments_or_parens(Meta) ->
    {Pre, Post} = comments(Meta),
    Pre =:= [] andalso Post =:= [] andalso not erlfmt_scan:get_anno(parens, Meta, false).

%% Workaround for misparse if record/map base is a raw integer
%% e.g. 2#{} is illigal, because it tries to scan the Base#Int syntax
wrap_if_integer(Doc, {integer, Meta, _}) ->
    case erlfmt_scan:get_anno(parens, Meta, false) of
        %% This will be wrapped by the generic paren-wrapping mechanism
        true -> Doc;
        false -> concat(<<"(">>, Doc, <<")">>)
    end;
wrap_if_integer(Doc, _Other) ->
    Doc.

maybe_wrap_in_parens(Meta, Doc) ->
    case erlfmt_scan:get_anno(parens, Meta, false) of
        true -> concat(<<"(">>, Doc, <<")">>);
        false -> Doc
    end.

combine_comments_no_force(Meta, Doc) ->
    {Pre, Post} = comments(Meta),
    combine_post_comments(Post, Meta, combine_pre_comments(Pre, Meta, Doc)).

combine_comments(Meta, Doc) ->
    {Pre, Post} = comments(Meta),
    CombinedD = combine_post_comments(Post, Meta, combine_pre_comments(Pre, Meta, Doc)),
    concat(maybe_force_breaks(Pre =/= [] orelse Post =/= []), CombinedD).

combine_comments_with_dot(Meta, Doc) ->
    {Pre, PreDot, Post} = comments_with_pre_dot(Meta),
    PreDotDoc = combine_pre_dot_comments(PreDot, Doc),
    combine_post_comments(Post, Meta, combine_pre_comments(Pre, Meta, PreDotDoc)).

combine_pre_dot_comments([], Doc) ->
    concat(Doc, <<".">>);
combine_pre_dot_comments(Comments, Doc) ->
    concat([Doc, line(), comments_to_algebra(Comments), line(), <<".">>]).

combine_pre_comments([], _Meta, Doc) ->
    Doc;
combine_pre_comments(Comments, Meta, Doc) ->
    case
        erlfmt_scan:get_end_line(lists:last(Comments)) + 1 <
            erlfmt_scan:get_inner_line(Meta)
    of
        true -> concat(comments_to_algebra(Comments), line(2), Doc);
        false -> concat(comments_to_algebra(Comments), line(), Doc)
    end.

combine_post_comments([], _Meta, Doc) ->
    Doc;
combine_post_comments([Comment | _] = Comments, Meta, Doc) ->
    case erlfmt_scan:get_inner_end_line(Meta) + 1 < erlfmt_scan:get_line(Comment) of
        true -> concat(Doc, line(2), comments_to_algebra(Comments));
        false -> concat(Doc, line(), comments_to_algebra(Comments))
    end.

comments_to_algebra(Comments) ->
    CommentsD = lists:map(fun comment_to_algebra/1, Comments),
    fold_doc(fun(C, Acc) -> concat(C, line(2), Acc) end, CommentsD).

comment_to_algebra({comment, _Meta, Lines}) ->
    LinesD = lists:map(fun erlfmt_algebra:string/1, Lines),
    fold_doc(fun erlfmt_algebra:line/2, LinesD).

comments_with_pre_dot(Meta) ->
    {
        erlfmt_scan:get_anno(pre_comments, Meta, []),
        erlfmt_scan:get_anno(pre_dot_comments, Meta, []),
        erlfmt_scan:get_anno(post_comments, Meta, [])
    }.

comments(Meta) ->
    [] = erlfmt_scan:get_anno(pre_dot_comments, Meta, []),
    {erlfmt_scan:get_anno(pre_comments, Meta, []), erlfmt_scan:get_anno(post_comments, Meta, [])}.

has_comments(Expr) ->
    case comments(Expr) of
        {[], []} -> false;
        _ -> true
    end.
