%% 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).

%% API exports
-export([
    main/1,
    init/1,
    format_file/2,
    format_string/2,
    format_nodes/2,
    read_nodes/1,
    read_nodes_string/2,
    format_error/1,
    format_error_info/1
]).

% For unit tests
-export([
    format_file_range/4,
    format_string_range/4
]).

-export_type([error_info/0, config_option/0, config/0, pragma/0]).

-type error_info() :: {
    file:name_all(), erl_anno:location() | erlfmt_scan:anno(), module(), Reason :: any()
}.
-type pragma() :: require | insert | delete | ignore.
-type config_option() :: {pragma, pragma()} | {print_width, pos_integer()} | verbose.
-type config() :: [config_option()].

%% needed because of getopt being weird
-dialyzer({nowarn_function, [init/1, main/1]}).

-define(DEFAULT_WIDTH, 100).

%% escript entry point
-spec main([string()]) -> no_return().
main(Argv) ->
    application:ensure_all_started(erlfmt),
    %% operate stdio in purely unicode mode
    io:setopts([binary, {encoding, unicode}]),
    Opts = erlfmt_cli:opts(),
    case getopt:parse(Opts, Argv) of
        {ok, {ArgOpts, []}} ->
            erlfmt_cli:do("erlfmt", ArgOpts);
        {ok, {ArgOpts, ExtraFiles}} ->
            erlfmt_cli:do("erlfmt", [{files, ExtraFiles} | ArgOpts]);
        {error, Error} ->
            io:put_chars(standard_error, [getopt:format_error(Opts, Error), "\n\n"]),
            getopt:usage(Opts, "erlfmt"),
            erlang:halt(2)
    end.

%% rebar3 plugin entry point
-spec init(term()) -> {ok, term()}.
init(State) ->
    rebar3_fmt_prv:init(State).

%% API entry point
-spec format_file(file:name_all() | stdin, config()) ->
    {ok, [unicode:chardata()], [error_info()]} | {skip, string()} | {error, error_info()}.
format_file(FileName, Options) ->
    Range = proplists:get_value(range, Options),
    case Range of
        undefined ->
            % Whole file (default).
            format_file_full(FileName, Options);
        {Start, End} ->
            % Remove 'range' property: when applicable we pass explicitly the range instead.
            % Also, match specifition of format_string_range.
            Options2 = proplists:delete(range, Options),
            format_file_range(FileName, {Start, 1}, {End, ?DEFAULT_WIDTH}, Options2)
    end.

-spec format_file_full(file:name_all() | stdin, config()) ->
    {ok, [unicode:chardata()], [error_info()]} | {skip, string()} | {error, error_info()}.
format_file_full(FileName, Options) ->
    PrintWidth = proplists:get_value(print_width, Options, ?DEFAULT_WIDTH),
    Pragma = proplists:get_value(pragma, Options, ignore),
    try
        case file_read_nodes(FileName, Pragma) of
            {ok, Nodes0, Warnings} ->
                Nodes =
                    case Pragma of
                        insert -> insert_pragma_nodes(Nodes0);
                        delete -> remove_pragma_nodes(Nodes0);
                        _ -> Nodes0
                    end,
                Formatted = format_nodes(Nodes, PrintWidth),
                verify_nodes(FileName, Nodes, Formatted),
                VerboseWarnings =
                    case proplists:get_bool(verbose, Options) of
                        true -> check_line_lengths(FileName, PrintWidth, Formatted);
                        false -> []
                    end,
                {ok, Formatted, Warnings ++ VerboseWarnings};
            {skip, RawString} ->
                {skip, RawString}
        end
    catch
        {error, Error} -> {error, Error}
    end.

-spec format_string(string(), [
    config_option() | {filename, string()} | {range, erlfmt_scan:location()}
]) ->
    {ok, string(), [error_info()]} | {skip, string()} | {error, error_info()}.
format_string(String, Options) ->
    Range = proplists:get_value(range, Options),
    case Range of
        undefined ->
            % Whole file (default).
            format_string_full(String, Options);
        {Start, End} ->
            % Remove 'range' property: when applicable we pass explicitly the range instead.
            % Also, match specifition of format_string_range.
            Options2 = proplists:delete(range, Options),
            format_string_range(String, {Start, 1}, {End, ?DEFAULT_WIDTH}, Options2)
    end.

-spec format_string_full(string(), [config_option() | {filename, string()}]) ->
    {ok, string(), [error_info()]} | {skip, string()} | {error, error_info()}.
format_string_full(String, Options) ->
    Filename = proplists:get_value(filename, Options, "nofile"),
    PrintWidth = proplists:get_value(print_width, Options, ?DEFAULT_WIDTH),
    Pragma = proplists:get_value(pragma, Options, ignore),
    try
        case read_nodes_string(Filename, String, Pragma) of
            {ok, Nodes0, Warnings} ->
                Nodes =
                    case Pragma of
                        insert -> insert_pragma_nodes(Nodes0);
                        delete -> remove_pragma_nodes(Nodes0);
                        _ -> Nodes0
                    end,
                Formatted = format_nodes(Nodes, PrintWidth),
                verify_nodes(Filename, Nodes, Formatted),
                VerboseWarnings =
                    case proplists:get_bool(verbose, Options) of
                        true -> check_line_lengths(Filename, PrintWidth, Formatted);
                        false -> []
                    end,
                {ok, unicode:characters_to_list(Formatted), Warnings ++ VerboseWarnings};
            {skip, RawString} ->
                {skip, RawString}
        end
    catch
        {error, Error} -> {error, Error}
    end.

which_pragma_node(Node) ->
    {PreComments, _, PostComments} = erlfmt_format:comments_with_pre_dot(Node),
    Comments = PreComments ++ PostComments,
    case lists:any(fun contains_noformat_pragma/1, Comments) of
        true ->
            noformat;
        false ->
            case lists:any(fun contains_format_pragma/1, Comments) of
                true -> format;
                false -> nopragma
            end
    end.

contains_format_pragma({comment, _Loc, Comments}) ->
    string:find(Comments, "@format") =/= nomatch.

contains_noformat_pragma({comment, _Loc, Comments}) ->
    string:find(Comments, "@noformat") =/= nomatch.

%% insert_pragma_nodes only inserts an @format comment,
%% if one has not already been inserted.
insert_pragma_nodes([]) ->
    [];
insert_pragma_nodes([{shebang, _, _} = Node | Nodes]) ->
    case which_pragma_node(Node) of
        format -> [replace_pragma_node(Node) | Nodes];
        noformat -> [Node | Nodes];
        nopragma -> [Node | insert_pragma_nodes(Nodes)]
    end;
insert_pragma_nodes([Node | Nodes]) ->
    case which_pragma_node(Node) of
        format -> [replace_pragma_node(Node) | Nodes];
        noformat -> [Node | Nodes];
        nopragma -> [insert_pragma_node(Node) | Nodes]
    end.

insert_pragma_node(Node) ->
    PreComments = erlfmt_scan:get_anno(pre_comments, Node, []),
    NewPreComments =
        case PreComments of
            [] ->
                Loc = erlfmt_scan:get_anno(location, Node),
                [{comment, #{location => Loc, end_location => Loc}, ["%%% % @format", ""]}];
            _ ->
                {comment, Loc, LastComments} = lists:last(PreComments),
                {Prefix, _} = string:take(lists:last(LastComments), "%"),
                lists:droplast(PreComments) ++
                    [{comment, Loc, LastComments ++ [Prefix ++ " % @format"]}]
        end,
    erlfmt_scan:put_anno(pre_comments, NewPreComments, Node).

remove_pragma_nodes([]) ->
    [];
remove_pragma_nodes([{shebang, _, _} = Node | Nodes]) ->
    case which_pragma_node(Node) of
        format -> [remove_pragma_node(Node) | Nodes];
        noformat -> [Node | Nodes];
        nopragma -> [Node | remove_pragma_nodes(Nodes)]
    end;
remove_pragma_nodes([Node | Nodes]) ->
    case which_pragma_node(Node) of
        format -> [remove_pragma_node(Node) | Nodes];
        _ -> [Node | Nodes]
    end.

remove_pragma_node(Node0) ->
    {PreComments0, _, PostComments0} = erlfmt_format:comments_with_pre_dot(Node0),
    PreComments = remove_pragma_comment_blocks(PreComments0),
    PostComments = remove_pragma_comment_blocks(PostComments0),
    Node = erlfmt_scan:put_anno(pre_comments, PreComments, Node0),
    erlfmt_scan:put_anno(post_comments, PostComments, Node).

remove_pragma_comment_blocks([]) ->
    [];
remove_pragma_comment_blocks([{comment, Loc, Comments} | Rest]) ->
    case remove_pragma_comment_block(Comments) of
        [] -> remove_pragma_comment_block(Rest);
        CleanComments -> [{comment, Loc, CleanComments} | remove_pragma_comment_block(Rest)]
    end.

remove_pragma_comment_block([]) ->
    [];
remove_pragma_comment_block([Head | Tail]) ->
    case string:find(Head, "@format") of
        nomatch -> [Head | remove_pragma_comment_block(Tail)];
        _ -> Tail
    end.

replace_pragma_node(Node0) ->
    {PreComments0, _, PostComments0} = erlfmt_format:comments_with_pre_dot(Node0),
    PreComments = replace_pragma_comment_blocks("%%%", PreComments0),
    PostComments = replace_pragma_comment_blocks("%%%", PostComments0),
    Node = erlfmt_scan:put_anno(pre_comments, PreComments, Node0),
    erlfmt_scan:put_anno(post_comments, PostComments, Node).

replace_pragma_comment_blocks(_Prefix, []) ->
    [];
replace_pragma_comment_blocks(Prefix, [{comment, Loc, Comments} | Rest]) ->
    CleanComments = replace_pragma_comment_block(Prefix, Comments),
    {Prefix0, _} = string:take(lists:last(CleanComments), "%"),
    [{comment, Loc, CleanComments} | replace_pragma_comment_block(Prefix0, Rest)].

replace_pragma_comment_block(_Prefix, []) ->
    [];
replace_pragma_comment_block(Prefix, ["%% @format" | Tail]) ->
    [(Prefix ++ " % @format") | Tail];
replace_pragma_comment_block(_Prefix, [("%" ++ _) = Head | Tail]) ->
    {Prefix, _} = string:take(Head, "%"),
    [Head | replace_pragma_comment_block(Prefix, Tail)];
replace_pragma_comment_block(Prefix, [Head | Tail]) ->
    [Head | replace_pragma_comment_block(Prefix, Tail)].

% Format the minimum number of top-level forms
% that cover the passed range.
% Rationale: top-level forms is the smallest
%            granularity we support now.
-spec format_file_range(
    file:name_all(),
    erlfmt_scan:location(),
    erlfmt_scan:location(),
    [{print_width, pos_integer()}]
) ->
    {ok, string(), [error_info()]}
    | {skip, string()}
    | {error, error_info()}.
format_file_range(FileName, StartLocation, EndLocation, Options) ->
    String = read_file_or_stdin(FileName),
    format_string_range(FileName, String, StartLocation, EndLocation, Options).

-spec format_string_range(
    string(),
    erlfmt_scan:location(),
    erlfmt_scan:location(),
    [{print_width, pos_integer()} | {filename, string()}]
) ->
    {ok, string(), [error_info()]}
    | {skip, string()}
    | {error, error_info()}.
format_string_range(Original, StartLocation, EndLocation, Options) ->
    Filename = proplists:get_value(filename, Options, "nofile"),
    format_string_range(Filename, Original, StartLocation, EndLocation, Options).
format_string_range(FileName, Original, StartLocation, EndLocation, Options) ->
    Pragma = proplists:get_value(pragma, Options, ignore),
    case read_nodes_string(FileName, Original, Pragma) of
        {ok, Nodes, Warnings} ->
            {{StartLine, _}, {EndLine, _}, Result} = format_enclosing_range(
                FileName,
                StartLocation,
                EndLocation,
                Options,
                Nodes,
                Warnings
            ),
            case Result of
                {ok, Formatted, Info} ->
                    Whole = inject_range(Original, StartLine, EndLine, Formatted),
                    {ok, Whole, Info};
                Other ->
                    % Error or skip.
                    Other
            end;
        {skip, String} ->
            % Happens with `noformat` pragma.
            {skip, String}
    end.

% Reinject formatted extract into whole string.
inject_range(Original, StartLine, EndLine, Formatted) ->
    AsList = string:split(unicode:characters_to_binary(Original), "\n", all),
    FormattedAsList0 = string:split(Formatted, "\n", all),
    % Remove spurious empty line introduced by last \n separator.
    FormattedAsList =
        case lists:last(FormattedAsList0) of
            <<>> ->
                lists:droplast(FormattedAsList0);
            _ ->
                FormattedAsList0
        end,
    true = EndLine >= StartLine,
    Len = EndLine - StartLine + 1,
    New = replace_slice(AsList, StartLine, Len, FormattedAsList),
    lists:join("\n", New).

format_enclosing_range(FileName, StartLocation, EndLocation, Options, Nodes, Warnings) ->
    case format_range(FileName, StartLocation, EndLocation, Options, Nodes, Warnings) of
        {options, []} ->
            % Nothing to format, due to noformat pragma or range outside of file,
            % return empty change (no warning should be emitted if noformat).
            {StartLocation, EndLocation, {skip, []}};
        {options, PossibleRanges} ->
            % Pick the largest range, so all intersected forms are covered.
            {Starts, Ends} = lists:unzip(PossibleRanges),
            Start = lists:min(Starts),
            End = lists:max(Ends),
            Res = format_range(FileName, Start, End, Options, Nodes, Warnings),
            % Poor man's assert (to avoid including assert.hrl)
            % This time we must have the formatted result.
            {ok, _, _} = Res,
            {Start, End, Res};
        X ->
            % Already ok or error: pass as is.
            {StartLocation, EndLocation, X}
    end.

format_range(FileName, StartLocation, EndLocation, Options, Nodes, Warnings) ->
    PrintWidth = proplists:get_value(print_width, Options, ?DEFAULT_WIDTH),
    try
        case verify_ranges(Nodes, StartLocation, EndLocation) of
            {ok, NodesInRange} ->
                Result = format_nodes(NodesInRange, PrintWidth),
                verify_nodes(FileName, NodesInRange, Result),
                VerboseWarnings =
                    case proplists:get_bool(verbose, Options) of
                        true -> check_line_lengths(FileName, PrintWidth, Result, StartLocation);
                        false -> []
                    end,
                {ok, unicode:characters_to_binary(Result), Warnings ++ VerboseWarnings};
            {options, PossibleRanges} ->
                {options, PossibleRanges}
        end
    catch
        {error, Error} -> {error, Error}
    end.

%% API entry point
-spec read_nodes(file:name_all()) ->
    {ok, [erlfmt_parse:abstract_node()], [error_info()]} | {error, error_info()}.
read_nodes(FileName) ->
    try
        file_read_nodes(FileName, ignore)
    catch
        {error, Error} -> {error, Error}
    end.

file_read_nodes(FileName, Pragma) ->
    read_file(FileName, fun(File) ->
        read_nodes(erlfmt_scan:io_node(File), FileName, Pragma)
    end).

% Apply 'Action' to file.
read_file(stdin, Action) ->
    Action(standard_io);
read_file(FileName, Action) ->
    case file:open(FileName, [read, binary, {encoding, unicode}]) of
        {ok, File} ->
            try
                Action(File)
            after
                file:close(File)
            end;
        {error, Reason} ->
            throw({error, {FileName, 0, file, Reason}})
    end.

% Return file as one big string (with '\n' as line separator).
-if(?OTP_RELEASE >= 27).
read_file_or_stdin(stdin) ->
    read_stdin([]);
read_file_or_stdin(FileName) ->
    {ok, Bin} = file:read_file(FileName, [raw]),
    unicode:characters_to_list(Bin).
-else.
read_file_or_stdin(stdin) ->
    read_stdin([]);
read_file_or_stdin(FileName) ->
    {ok, Bin} = file:read_file(FileName),
    unicode:characters_to_list(Bin).
-endif.

read_stdin(Acc) ->
    case io:get_line("") of
        eof ->
            lists:flatten(lists:reverse(Acc, []));
        Line ->
            read_stdin([unicode:characters_to_list(Line) | Acc])
    end.

%% API entry point
-spec read_nodes_string(file:name_all(), string()) ->
    {ok, [erlfmt_parse:abstract_node()], [error_info()]} | {error, error_info()}.
read_nodes_string(FileName, String) ->
    try
        read_nodes_string(FileName, String, ignore)
    catch
        {error, Error} -> {error, Error}
    end.

read_nodes_string(FileName, String, Pragma) ->
    read_nodes(erlfmt_scan:string_node(String), FileName, Pragma).

read_nodes(State, FileName, Pragma) ->
    read_nodes(State, FileName, Pragma, [], [], []).

read_nodes({ok, Tokens, Comments, Cont}, FileName, PragmaFlag, [], Warnings0, TextAcc) ->
    {Node, Warnings, Ignore} = parse_node(Tokens, Comments, FileName, Cont, Warnings0, false),
    case {which_pragma_node(Node), Node} of
        {_, {shebang, _, _}} ->
            {LastString, _Anno} = erlfmt_scan:last_node_string(Cont),
            read_nodes(
                erlfmt_scan:continue(Cont),
                FileName,
                PragmaFlag,
                [Node],
                Warnings,
                TextAcc ++ LastString
            );
        {nopragma, _} when PragmaFlag =:= require; PragmaFlag =:= delete ->
            skip_nodes(Cont, FileName, TextAcc);
        {noformat, _} ->
            skip_nodes(Cont, FileName, TextAcc);
        _ ->
            read_nodes_loop(erlfmt_scan:continue(Cont), FileName, [Node], Warnings, Ignore)
    end;
read_nodes(
    {ok, Tokens, Comments, Cont},
    FileName,
    PragmaFlag,
    [{shebang, _, _} = ShebangNode],
    Warnings0,
    TextAcc
) ->
    {Node, Warnings, Ignore} = parse_node(Tokens, Comments, FileName, Cont, Warnings0, false),
    case which_pragma_node(Node) of
        nopragma when PragmaFlag =:= require; PragmaFlag =:= delete ->
            skip_nodes(Cont, FileName, TextAcc);
        noformat ->
            skip_nodes(Cont, FileName, TextAcc);
        _ ->
            read_nodes_loop(
                erlfmt_scan:continue(Cont), FileName, [Node, ShebangNode], Warnings, Ignore
            )
    end;
read_nodes(Other, FileName, _PragmaFlag, NodeAcc, Warnings, _TextAcc) ->
    read_nodes_loop(Other, FileName, NodeAcc, Warnings, false).

skip_nodes(Cont, FileName, TextAcc) ->
    {LastString, _Anno} = erlfmt_scan:last_node_string(Cont),
    case erlfmt_scan:read_rest(Cont) of
        {ok, Rest} ->
            {skip, [TextAcc, LastString | Rest]};
        {error, {ErrLoc, Mod, Reason}} ->
            throw({error, {FileName, ErrLoc, Mod, Reason}})
    end.

read_nodes_loop({ok, Tokens, Comments, Cont}, FileName, Acc, Warnings0, Ignore0) ->
    {Node, Warnings, Ignore} = parse_node(Tokens, Comments, FileName, Cont, Warnings0, Ignore0),
    read_nodes_loop(erlfmt_scan:continue(Cont), FileName, [Node | Acc], Warnings, Ignore);
read_nodes_loop({eof, _Loc}, _FileName, Acc, Warnings, _Ignore0) ->
    {ok, lists:reverse(Acc), lists:reverse(Warnings)};
read_nodes_loop({error, {ErrLoc, Mod, Reason}, _Loc}, FileName, _Acc, _Warnings, _Ignore0) ->
    throw({error, {FileName, ErrLoc, Mod, Reason}}).

parse_node([], _Comments, _FileName, Cont, Warnings, Ignore) ->
    {node_string(Cont), Warnings, Ignore};
parse_node([{shebang, Meta, String}], Comments, _FileName, _Cont, Warnings, Ignore) ->
    Node = {shebang, erlfmt_recomment:put_post_comments(Meta, Comments), String},
    {Node, Warnings, Ignore};
parse_node([Token | _] = Tokens, Comments, FileName, Cont, Warnings, Ignore0) ->
    {PreComments, _} = erlfmt_recomment:take_comments(erlfmt_scan:get_line(Token), Comments),
    Ignore = ignore_state_pre(PreComments, FileName, Ignore0),
    NextIgnore = ignore_state_post([], FileName, Ignore),
    case Ignore of
        false ->
            case parse(Tokens) of
                {ok, Node} ->
                    {erlfmt_recomment:recomment(Node, Comments), Warnings, NextIgnore};
                {error, {ErrLoc, Mod, Reason}} ->
                    Warning = {FileName, ErrLoc, Mod, Reason},
                    {node_string(Cont), [Warning | Warnings], NextIgnore}
            end;
        _ ->
            {node_string(Cont), Warnings, NextIgnore}
    end.

parse(Tokens) ->
    case erlfmt_parse:parse_node(Tokens) of
        {ok, Node} ->
            {ok, Node};
        {error, Error} ->
            TokensWithoutMaybe = erlfmt_scan:downgrade_maybe(Tokens),
            case erlfmt_parse:parse_node(TokensWithoutMaybe) of
                {ok, Node} -> {ok, Node};
                _ -> {error, Error}
            end
    end.

ignore_state_pre(PreComments, FileName, Acc) ->
    ignore_state(PreComments, FileName, Acc).

ignore_state_post(PostComments, FileName, Acc) ->
    case ignore_state(PostComments, FileName, Acc) of
        ignore -> false;
        Other -> Other
    end.

ignore_state([{comment, Loc, Comments} | Rest], FileName, Acc) ->
    ignore_state(Comments, FileName, Loc, Rest, Acc);
ignore_state([], _FileName, Acc) ->
    Acc.

-define(IS_IGNORE_REASON(S), (S =:= [] orelse hd(S) =:= 32 orelse hd(S) =:= $%)).

ignore_state([Line | Lines], FileName, Loc, Rest, Acc0) ->
    Acc =
        case string:trim(Line, leading, "% ") of
            % Old-style erlfmt-ignore
            "erlfmt-ignore" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= false ->
                ignore;
            "erlfmt-ignore" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, ignore, Acc0}}});
            "erlfmt-ignore-begin" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= false ->
                'begin';
            "erlfmt-ignore-begin" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, 'begin', Acc0}}});
            "erlfmt-ignore-end" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= 'begin' ->
                false;
            "erlfmt-ignore-end" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, 'end', Acc0}}});
            % New-style erlfmt:ignore
            "erlfmt:ignore" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= false ->
                ignore;
            "erlfmt:ignore" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, ignore, Acc0}}});
            "erlfmt:ignore-begin" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= false ->
                'begin';
            "erlfmt:ignore-begin" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, 'begin', Acc0}}});
            "erlfmt:ignore-end" ++ R when ?IS_IGNORE_REASON(R), Acc0 =:= 'begin' ->
                false;
            "erlfmt:ignore-end" ++ R when ?IS_IGNORE_REASON(R) ->
                throw({error, {FileName, Loc, ?MODULE, {invalid_ignore, 'end', Acc0}}});
            _ ->
                Acc0
        end,
    ignore_state(Lines, FileName, Loc, Rest, Acc);
ignore_state([], FileName, _Loc, Rest, Acc) ->
    ignore_state(Rest, FileName, Acc).

node_string(Cont) ->
    {String, Anno} = erlfmt_scan:last_node_string_trimmed(Cont),
    {raw_string, Anno, String}.

-spec format_nodes([erlfmt_parse:abstract_node()], pos_integer()) -> [unicode:chardata()].
format_nodes([], _PrintWidth) ->
    [];
format_nodes(Nodes, PrintWidth) ->
    [$\n | Formatted] = format_nodes_loop(Nodes, PrintWidth),
    Formatted.

format_nodes_loop([Node | [Next | _] = Rest], PrintWidth) ->
    [
        $\n,
        format_node(Node, PrintWidth),
        maybe_empty_line(Node, Next)
        | format_nodes_loop(Rest, PrintWidth)
    ];
format_nodes_loop([Node], PrintWidth) ->
    [$\n, format_node(Node, PrintWidth), $\n];
format_nodes_loop([], _PrintWidth) ->
    [].

maybe_empty_line(Node, Next) ->
    case has_empty_line_between(Node, Next) of
        true -> "\n";
        false -> ""
    end.

-spec format_node(erlfmt_parse:abstract_node(), pos_integer()) -> unicode:chardata().
format_node({raw_string, _Anno, String}, _PrintWidth) ->
    String;
format_node(Node, PrintWidth) ->
    Doc = erlfmt_format:to_algebra(Node),
    erlfmt_algebra:format(Doc, PrintWidth).

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

verify_nodes(FileName, Nodes, Formatted) ->
    Flattened = unicode:characters_to_list(Formatted),
    case read_nodes_string(FileName, Flattened) of
        {ok, Nodes2, _} ->
            try
                equivalent_list(Nodes, Nodes2)
            catch
                {not_equivalent, Left, Right} ->
                    Location = try_location(Left, Right),
                    Msg = {not_equivalent, Left, Right, Flattened},
                    throw({error, {FileName, Location, ?MODULE, Msg}})
            end;
        {error, _} ->
            throw({error, {FileName, 0, ?MODULE, could_not_reparse}})
    end.

equivalent(Element, Element) ->
    true;
equivalent({raw_string, _AnnoL, RawL}, {raw_string, _AnnoR, RawR}) ->
    string:equal(RawL, RawR) orelse throw({not_equivalent, RawL, RawR});
equivalent({Type, _}, {Type, _}) ->
    true;
equivalent({concat, _, Left} = L, {concat, _, Right} = R) ->
    concat_equivalent(Left, Right) orelse throw({not_equivalent, L, R});
equivalent({string, _, String} = L, {concat, _, Values} = R) ->
    string_concat_equivalent(String, Values) orelse throw({not_equivalent, L, R});
equivalent({op, _, Op1, _, _} = L, {op, _, Op2, _, _} = R) when Op1 =/= Op2 ->
    throw({not_equivalent, L, R});
equivalent({Type, _, L}, {Type, _, R}) ->
    equivalent(L, R);
equivalent({Type, _, L1, L2}, {Type, _, R1, R2}) ->
    equivalent(L1, R1) andalso equivalent(L2, R2);
equivalent({Type, _, L1, L2, L3}, {Type, _, R1, R2, R3}) ->
    equivalent(L1, R1) andalso equivalent(L2, R2) andalso equivalent(L3, R3);
equivalent({Type, _, L1, L2, L3, L4}, {Type, _, R1, R2, R3, R4}) ->
    equivalent(L1, R1) andalso
        equivalent(L2, R2) andalso equivalent(L3, R3) andalso equivalent(L4, R4);
equivalent(Ls, Rs) when is_list(Ls), is_list(Rs) ->
    equivalent_list(Ls, Rs);
equivalent(L, R) ->
    throw({not_equivalent, L, R}).

string_concat_equivalent(String, Values) ->
    string:equal(String, [Value || {string, _, Value} <- Values]).

concat_equivalent(ValuesL, ValuesR) ->
    string:equal([Value || {string, _, Value} <- ValuesL], [Value || {string, _, Value} <- ValuesR]).

equivalent_list([L | Ls], [R | Rs]) ->
    equivalent(L, R) andalso equivalent_list(Ls, Rs);
equivalent_list([], []) ->
    true;
equivalent_list(Ls, Rs) ->
    throw({not_equivalent, Ls, Rs}).

try_location(Node, _) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location([Node | _], _) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, Node) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, [Node | _]) when is_tuple(Node) -> erlfmt_scan:get_anno(location, Node);
try_location(_, _) -> 0.

-spec format_error_info(error_info()) -> unicode:chardata().
format_error_info({FileName, Anno, Mod, Reason}) ->
    io_lib:format("~ts~s: ~ts", [FileName, format_loc(Anno), Mod:format_error(Reason)]).

format_loc(0) -> "";
format_loc({Line, Col}) -> io_lib:format(":~B:~B", [Line, Col]);
format_loc(#{location := {Line, Col}}) -> io_lib:format(":~B:~B", [Line, Col]);
format_loc(Line) when is_integer(Line) -> io_lib:format(":~B", [Line]).

format_error({not_equivalent, Node1, Node2, Formatted}) ->
    io_lib:format(
        "formatter result not equivalent. Please report this bug.~n~n~ts~n~n~p~n~n~p",
        [Formatted, Node1, Node2]
    );
format_error(could_not_reparse) ->
    "formatter result invalid, could not reparse";
format_error({long_line, Length, Width}) ->
    io_lib:format("line too long (~p > ~p)", [Length, Width]);
format_error({invalid_ignore, ignore, 'begin'}) ->
    "invalid erlfmt:ignore while in erlfmt:ignore-begin section";
format_error({invalid_ignore, Same, Same}) ->
    "duplicate ignore comment";
format_error({invalid_ignore, 'end', false}) ->
    "invalid erlfmt:ignore-end while outside of erlfmt:ignore-begin section";
format_error({invalid_ignore, Given, Previous}) ->
    io_lib:format("invalid ignore specification ~ts while in ~ts state", [Given, Previous]).

verify_ranges(Nodes, StartLocation, EndLocation) ->
    ApplicableNodes = nodes_in_range(Nodes, StartLocation, EndLocation),
    case possible_ranges(ApplicableNodes, StartLocation, EndLocation) of
        [{StartLocation, EndLocation}] ->
            {ok, ApplicableNodes};
        Options ->
            {options, Options}
    end.

% Returns ranges which starts with the start of a node and ends with the end of node
possible_ranges(Nodes, StartLocation, EndLocation) ->
    case Nodes of
        [] ->
            [];
        [OnlyNode] ->
            [get_location_range(OnlyNode)];
        MultipleNodes ->
            combine(
                get_possible_locations(MultipleNodes, StartLocation, fun get_location/1),
                get_possible_locations(
                    lists:reverse(MultipleNodes),
                    EndLocation,
                    fun get_end_location/1
                )
            )
    end.

nodes_in_range(Nodes, StartLocation, EndLocation) ->
    [Node || Node <- Nodes, node_intersects_range(Node, StartLocation, EndLocation)].

node_intersects_range(Node, StartLocation, EndLocation) ->
    {Start, End} = get_location_range(Node),
    ((Start < StartLocation) and (End > StartLocation)) or
        ((Start >= StartLocation) and (Start < EndLocation)).

get_possible_locations([Option1, Option2 | _], Location, GetLoc) ->
    case GetLoc(Option1) of
        Location ->
            [Location];
        OptionalLocation ->
            [OptionalLocation, GetLoc(Option2)]
    end.

combine(L1, L2) ->
    [{X1, X2} || X1 <- L1, X2 <- L2].

get_location_range(Node) ->
    {get_location(Node), get_end_location(Node)}.

get_location(Node) ->
    erlfmt_scan:get_anno(location, Node).

get_end_location(Node) ->
    erlfmt_scan:get_anno(end_location, Node).

check_line_lengths(FileName, Width, String) ->
    check_line_lengths(FileName, Width, String, {1, 0}).

check_line_lengths(FileName, Width, String, {FirstLineNo, _}) ->
    Lines = string:split(String, "\n", all),
    LastLineNo = FirstLineNo + length(Lines) - 1,
    [
        {FileName, LineNo, ?MODULE, {long_line, string:length(Line), Width}}
     || {LineNo, Line} <- lists:zip(
            lists:seq(FirstLineNo, LastLineNo),
            Lines
        ),
        string:length(Line) > Width
    ].

% Replace the sublist(Target, Start, Len) by Injected.
% E.g. if Injected is empty it will just remove the sublist.
-spec replace_slice(list(), pos_integer(), non_neg_integer(), list()) -> list().
replace_slice(Target, Start, Len, Injected) ->
    Res =
        lists:sublist(Target, Start - 1) ++
            Injected ++
            lists:sublist(Target, Start + Len, length(Target)),
    true = length(Target) - Len + length(Injected) =:= length(Res),
    Res.
