#!/usr/bin/env escript
%%!
%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et:
%%%
%%%------------------------------------------------------------------------
%%% MIT License
%%%
%%% Copyright (c) 2013-2021 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a
%%% copy of this software and associated documentation files (the "Software"),
%%% to deal in the Software without restriction, including without limitation
%%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%%% and/or sell copies of the Software, and to permit persons to whom the
%%% Software is furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%%% DEALINGS IN THE SOFTWARE.
%%%------------------------------------------------------------------------

-author('mjtruog at protonmail dot com').
-vsn("2.0.2").

-mode(compile).

-record(config,
        {
            directories_in = [],
            prefix_ignore = undefined,
            prefix_scope = undefined,
            directory_suffix_old = "_original",
            include_directories = [],
            undo = false,
            replace_usage = false,
            help = false,
            version = false
        }).

-record(state,
        {
            files_source = [],
            files_include = [],
            files_applications = [],
            files_protobuffs = [],
            include_directories = [],
            name_conversions = []
        }).

-record(progress_bar,
        {
            percentage = true,
            width_bar = $+,
            width_space = $ ,
            width_end = $|,
            width = 40,
            percentage_current = 0,
            refresh = true,
            stdout,
            count_i = 0,
            count_x
        }).

-include_lib("kernel/include/file.hrl").

main(Arguments) ->
    handle_operation(main_arguments(Arguments)).

handle_operation(#config{version = true,
                         help = Help}) ->
    io:format("~s~n",[version_info()]),
    if
        Help =:= true ->
            help(),
            exit_code(1);
        Help =:= false ->
            exit_code(0)
    end;
handle_operation(#config{help = true}) ->
    help(),
    exit_code(1);
handle_operation(#config{directories_in = DirectoriesIn,
                         prefix_ignore = PrefixIgnore,
                         prefix_scope = PrefixScope,
                         directory_suffix_old = DirectorySuffixOld,
                         undo = true})
    when DirectoriesIn /= [], is_list(PrefixIgnore), is_list(PrefixScope),
         is_list(DirectorySuffixOld) ->
    ok = undo_mv_old(DirectoriesIn, DirectorySuffixOld),
    ok = undo_rm_scope(DirectoriesIn, PrefixScope),
    exit_code(0);
handle_operation(#config{directories_in = DirectoriesIn,
                         prefix_ignore = PrefixIgnore,
                         prefix_scope = PrefixScope,
                         replace_usage = true})
    when DirectoriesIn /= [], is_list(PrefixIgnore), is_list(PrefixScope) ->
    #state{files_source = FilesSource,
           files_include = FilesInclude,
           files_applications = FilesApplications,
           name_conversions = NameConversions} = files_find(DirectoriesIn,
                                                            PrefixIgnore,
                                                            PrefixScope,
                                                            true),
    Regexes = create_regexes(NameConversions),
    ProgressBar = progress_bar_pid(erlang:length(FilesSource) +
                                   erlang:length(FilesInclude)),
    Fconversion = fun(FilePath) ->
        {ok, FileContents} = file:read_file(FilePath),
        NewFileContents = reduce(Regexes, erlang:binary_to_list(FileContents)),
        ok = file:write_file(FilePath, NewFileContents),
        progress_bar_pid_increment(ProgressBar, 1)
    end,
    lists:foreach(Fconversion, FilesSource),
    lists:foreach(Fconversion, FilesInclude),
    files_update_apps(FilesApplications, orddict:from_list(NameConversions)),
    io:format("~ncheck modified files!~n", []),
    exit_code(0);
handle_operation(#config{directories_in = DirectoriesIn,
                         prefix_ignore = PrefixIgnore,
                         prefix_scope = PrefixScope,
                         directory_suffix_old = DirectorySuffixOld,
                         include_directories = IncludeDirectories0})
    when DirectoriesIn /= [], is_list(PrefixIgnore), is_list(PrefixScope),
         is_list(DirectorySuffixOld) ->
    #state{files_source = FilesSource,
           files_include = FilesInclude,
           files_applications = FilesApplications,
           files_protobuffs = FilesProtobuffs,
           include_directories = IncludeDirectories1,
           name_conversions = NameConversions} = files_find(DirectoriesIn,
                                                            PrefixIgnore,
                                                            PrefixScope,
                                                            false),
    if
        NameConversions == [] ->
            io:format("no files to process!~n", []),
            exit_code(0);
        true ->
            ok
    end,
    % n.b., since only a prefix is added it is always true that no
    % resulting names in the NameConversions list are also source names
    % (so no cycles within the lookup)
    NameLookup = orddict:from_list(NameConversions),
    rename(FilesSource, FilesInclude,
           IncludeDirectories0 ++ IncludeDirectories1,
           NameConversions, NameLookup, DirectorySuffixOld),
    ok = files_rename_apps(FilesApplications, NameLookup),
    ok = files_move_old(FilesApplications, DirectorySuffixOld),
    ok = files_rename_protobuffs(FilesProtobuffs, PrefixScope),
    ok = files_move_old(FilesProtobuffs, DirectorySuffixOld),
    exit_code(0).

undo_mv_old([], _) ->
    ok;
undo_mv_old([Directory | DirectoriesIn], DirectorySuffixOld) ->
    true = filelib:is_dir(Directory),
    case lists:suffix("/src", Directory) of
        true ->
            [$c, $r, $s, $/ | DirectoryPrefix] = lists:reverse(Directory),
            IncludeDirectory = filename:join(lists:reverse(DirectoryPrefix),
                                             "include"),
            OldIncludeDirectory = IncludeDirectory ++ DirectorySuffixOld,
            case filelib:is_dir(OldIncludeDirectory) of
                true ->
                    {ok, IncludeFilePaths} = file:list_dir(OldIncludeDirectory),
                    ok = undo_mv_old_dir(IncludeFilePaths,
                                         OldIncludeDirectory,
                                         IncludeDirectory),
                    ok = file:del_dir(OldIncludeDirectory);
                false ->
                    ok
            end;
        false ->
            ok
    end,
    OldDirectory = Directory ++ DirectorySuffixOld,
    case filelib:is_dir(OldDirectory) of
        true ->
            {ok, FilePaths} = file:list_dir(OldDirectory),
            ok = undo_mv_old_dir(FilePaths, OldDirectory, Directory),
            ok = file:del_dir(OldDirectory),
            ok;
        false ->
            ok
    end,
    undo_mv_old(DirectoriesIn, DirectorySuffixOld).

undo_mv_old_dir([], _, _) ->
    ok;
undo_mv_old_dir([FilePath | FilePaths], OldDirectory, Directory) ->
    OldFilePath = filename:join(OldDirectory, FilePath),
    NewFilePath = filename:join(Directory, FilePath),
    case file:read_file_info(NewFilePath, [mtime]) of
        {ok, #file_info{mtime = NewFileMtime}} ->
            {ok, #file_info{mtime = OldFileMtime}} =
                file:read_file_info(OldFilePath, [mtime]),
            if
                NewFileMtime > OldFileMtime ->
                    ok = file:delete(OldFilePath);
                true ->
                    ok = file:rename(OldFilePath, NewFilePath)
            end;
        {error, _} ->
            ok = file:rename(OldFilePath, NewFilePath)
    end,
    undo_mv_old_dir(FilePaths, OldDirectory, Directory).

undo_rm_scope([], _) ->
    ok;
undo_rm_scope([Directory | DirectoriesIn], PrefixScope) ->
    case lists:suffix("/src", Directory) of
        true ->
            [$c, $r, $s, $/ | DirectoryPrefix] = lists:reverse(Directory),
            IncludeDirectory = filename:join(lists:reverse(DirectoryPrefix),
                                             "include"),
            case filelib:is_dir(IncludeDirectory) of
                true ->
                    {ok, IncludeFilePaths} = file:list_dir(IncludeDirectory),
                    ok = undo_rm_scope_dir(IncludeFilePaths,
                                           IncludeDirectory, PrefixScope);
                false ->
                    ok
            end;
        false ->
            ok
    end,
    true = filelib:is_dir(Directory),
    {ok, FilePaths} = file:list_dir(Directory),
    ok = undo_rm_scope_dir(FilePaths, Directory, PrefixScope),
    undo_rm_scope(DirectoriesIn, PrefixScope).

undo_rm_scope_dir([], _, _) ->
    ok;
undo_rm_scope_dir([FilePath | FilePaths], Directory, PrefixScope) ->
    case lists:prefix(PrefixScope, FilePath) of
        true ->
            FullFilePath = filename:join(Directory, FilePath),
            ok = file:delete(FullFilePath);
        false ->
            ok
    end,
    undo_rm_scope_dir(FilePaths, Directory, PrefixScope).

files_find(DirectoriesIn,
           PrefixIgnore, PrefixScope, ReplaceUsage) ->
    files_find(DirectoriesIn,
               PrefixIgnore, PrefixScope, ReplaceUsage, #state{}).
    
files_find([], _, _, _,
           #state{files_source = FilesSource,
                  files_include = FilesInclude,
                  files_applications = FilesApplications,
                  files_protobuffs = FilesProtobuffs,
                  include_directories = IncludeDirectories,
                  name_conversions = NameConversions} = State) ->
    State#state{files_source = lists:reverse(FilesSource),
                files_include = lists:reverse(FilesInclude),
                files_applications = lists:reverse(FilesApplications),
                files_protobuffs = lists:reverse(FilesProtobuffs),
                include_directories = lists:reverse(IncludeDirectories),
                name_conversions = lists:reverse(NameConversions)};
           
files_find([Directory | DirectoriesIn],
           PrefixIgnore, PrefixScope, ReplaceUsage,
           #state{include_directories = IncludeDirectories} = State0) ->
    true = filelib:is_dir(Directory),
    NextState = case lists:suffix("/src", Directory) of
        true ->
            [$c, $r, $s, $/ | DirectoryPrefix] = lists:reverse(Directory),
            IncludeDirectory = filename:join(lists:reverse(DirectoryPrefix),
                                             "include"),
            case filelib:is_dir(IncludeDirectory) of
                true ->
                    {ok, IncludeFilePaths} =
                        file:list_dir(IncludeDirectory),
                    State1 = case lists:member(IncludeDirectory,
                                               IncludeDirectories) of
                        true ->
                            State0;
                        false ->
                            State0#state{include_directories =
                                             [IncludeDirectory |
                                              IncludeDirectories]}
                    end,
                    files_find_dir_include(IncludeFilePaths, IncludeDirectory,
                                           PrefixIgnore, PrefixScope,
                                           ReplaceUsage, State1);
                false ->
                    State0
            end;
        false ->
            State0
    end,
    {ok, FilePaths} = file:list_dir(Directory),
    files_find(DirectoriesIn, PrefixIgnore, PrefixScope, ReplaceUsage,
               files_find_dir_src(FilePaths, Directory,
                                  PrefixIgnore, PrefixScope,
                                  ReplaceUsage, NextState)).

files_find_dir_include([], _, _, _, _, State) ->
    State;
files_find_dir_include([FilePath | FilePaths], Directory,
                       PrefixIgnore, PrefixScope, ReplaceUsage,
                       #state{files_include = FilesInclude,
                              name_conversions = NameConversions} = State) ->
    FileExtension = filename:extension(FilePath),
    FileName = filename:basename(FilePath, FileExtension),
    NewState = if
        FileExtension == ".hrl" ->
            case lists:prefix(PrefixIgnore, FileName) of
                true ->
                    NewFilesInclude = if
                        ReplaceUsage ->
                            [filename:join(Directory, FilePath) |
                             FilesInclude];
                        true ->
                            FilesInclude
                    end,
                    State#state{files_include = NewFilesInclude};
                false ->
                    case lists:prefix(PrefixScope, FileName) of
                        true ->
                            State;
                        false ->
                            NewFilesInclude = if
                                ReplaceUsage ->
                                    FilesInclude;
                                true ->
                                    [filename:join(Directory, FilePath) |
                                     FilesInclude]
                            end,
                            Convert = {erlang:list_to_atom(FileName),
                                       erlang:list_to_atom(PrefixScope ++
                                                           FileName)},
                            State#state{
                                files_include = NewFilesInclude,
                                name_conversions =
                                    lists:umerge(NameConversions,
                                                 [Convert])}
                    end
            end;
        true ->
            State
    end,
    files_find_dir_include(FilePaths, Directory,
                           PrefixIgnore, PrefixScope, ReplaceUsage,
                           NewState).

files_find_dir_src([], _, _, _, _, State) ->
    State;
files_find_dir_src([FilePath | FilePaths], Directory,
                   PrefixIgnore, PrefixScope, ReplaceUsage,
                   #state{files_source = FilesSource,
                          files_include = FilesInclude,
                          files_applications = FilesApplications,
                          files_protobuffs = FilesProtobuffs,
                          name_conversions = NameConversions} = State) ->
    FileExtension = filename:extension(FilePath),
    FileName = filename:basename(FilePath, FileExtension),
    ApplicationFile = if
        FileExtension == ".app" ->
            true;
        FileExtension == ".src" ->
            filename:extension(FileName) == ".app";
        true ->
            false
    end,
    NextState = if
        ApplicationFile ->
            case lists:prefix(PrefixIgnore, FileName) of
                true ->
                    NewFilesApplications = if
                        ReplaceUsage ->
                            [filename:join(Directory, FilePath) |
                             FilesApplications];
                        true ->
                            FilesApplications
                    end,
                    State#state{files_applications = NewFilesApplications};
                false ->
                    case lists:prefix(PrefixScope, FileName) of
                        true ->
                            State;
                        false ->
                            BaseFileName = if
                                FileExtension == ".src" ->
                                    filename:basename(FileName, ".app");
                                true ->
                                    FileName
                            end,
                            NewFilesApplications = if
                                ReplaceUsage ->
                                    FilesApplications;
                                true ->
                                    [filename:join(Directory, FilePath) |
                                     FilesApplications]
                            end,
                            Convert = {erlang:list_to_atom(BaseFileName),
                                       erlang:list_to_atom(PrefixScope ++
                                                           BaseFileName)},
                            State#state{
                                files_applications = NewFilesApplications,
                                name_conversions =
                                    lists:umerge(NameConversions, [Convert])}
                    end
            end;
        FileExtension == ".erl" ->
            FileNameContents = case lists:prefix("Elixir.", FileName) of
                true ->
                    [_, _, _, _, _, _, _ | FileNameSuffix] = FileName,
                    lowercase(FileNameSuffix);
                false ->
                    FileName
            end,
            case lists:prefix(PrefixIgnore, FileNameContents) of
                true ->
                    NewFilesSource = if
                        ReplaceUsage ->
                            [filename:join(Directory, FilePath) |
                             FilesSource];
                        true ->
                            FilesSource
                    end,
                    State#state{files_source = NewFilesSource};
                false ->
                    case lists:prefix(PrefixScope, FileName) of
                        true ->
                            State;
                        false ->
                            Convert = {erlang:list_to_atom(FileName),
                                       erlang:list_to_atom(PrefixScope ++
                                                           FileName)},
                            NewFilesSource = if
                                ReplaceUsage ->
                                    FilesSource;
                                true ->
                                    [filename:join(Directory, FilePath) |
                                     FilesSource]
                            end,
                            State#state{
                                files_source = NewFilesSource,
                                name_conversions =
                                    lists:umerge(NameConversions, [Convert])}
                    end
            end;
        FileExtension == ".hrl" ->
            case lists:prefix(PrefixIgnore, FileName) of
                true ->
                    NewFilesInclude = if
                        ReplaceUsage ->
                            [filename:join(Directory, FilePath) |
                             FilesInclude];
                        true ->
                            FilesInclude
                    end,
                    State#state{files_include = NewFilesInclude};
                false ->
                    case lists:prefix(PrefixScope, FileName) of
                        true ->
                            State;
                        false ->
                            Convert = {erlang:list_to_atom(FileName),
                                       erlang:list_to_atom(PrefixScope ++
                                                           FileName)},
                            NewFilesInclude = if
                                ReplaceUsage ->
                                    FilesInclude;
                                true ->
                                    [filename:join(Directory, FilePath) |
                                     FilesInclude]
                            end,
                            State#state{
                                files_include = NewFilesInclude,
                                name_conversions =
                                    lists:umerge(NameConversions, [Convert])}
                    end
            end;
        FileExtension == ".proto" ->
            case lists:prefix(PrefixIgnore, FileName) of
                true ->
                    State;
                false ->
                    case lists:prefix(PrefixScope, FileName) of
                        true ->
                            State;
                        false ->
                            OutputFileName = FileName ++ "_pb",
                            Convert = {erlang:list_to_atom(OutputFileName),
                                       erlang:list_to_atom(PrefixScope ++
                                                           OutputFileName)},
                            NewFilesProtobuffs = if
                                ReplaceUsage ->
                                    FilesProtobuffs;
                                true ->
                                    [filename:join(Directory, FilePath) |
                                     FilesProtobuffs]
                            end,
                            State#state{
                                files_protobuffs = NewFilesProtobuffs,
                                name_conversions =
                                    lists:umerge(NameConversions, [Convert])}
                    end
            end;
        true ->
            State
    end,
    files_find_dir_src(FilePaths, Directory,
                       PrefixIgnore, PrefixScope, ReplaceUsage,
                       NextState).

files_rename_apps([], _) ->
    ok;
files_rename_apps([FilePath | FilesApplications], NameLookup) ->
    FileExtension = filename:extension(FilePath),
    FileName = filename:basename(FilePath, FileExtension),
    ApplicationName = if
        FileExtension == ".app" ->
            erlang:list_to_atom(FileName);
        FileExtension == ".src" ->
            erlang:list_to_atom(filename:basename(FileName, ".app"))
    end,
    NewApplicationName = orddict:fetch(ApplicationName, NameLookup),
    NewFileName = if
        FileExtension == ".app" ->
            erlang:atom_to_list(NewApplicationName) ++ ".app";
        FileExtension == ".src" ->
            erlang:atom_to_list(NewApplicationName) ++ ".app.src"
    end,
    NewFilePath = filename:join(filename:dirname(FilePath), NewFileName),
    % process the application file for renamed modules and applications
    true = filelib:is_regular(FilePath),
    {ok, [{application,
           ApplicationNameFile,
           ApplicationEnv0}]} = file:consult(FilePath),
    true = (ApplicationNameFile =:= ApplicationName),
    ApplicationEnv2 = case lists:keytake(modules, 1, ApplicationEnv0) of
        {value,
         {modules, ModulesList},
         ApplicationEnv1} ->
            NewModulesList = lists:map(fun(M) ->
                % all source file directories must have been provided
                orddict:fetch(M, NameLookup)
            end, ModulesList),
            [{modules, NewModulesList} | ApplicationEnv1];
        false ->
            ApplicationEnv0
    end,
    ApplicationEnv4 = case lists:keytake(mod, 1, ApplicationEnv2) of
        false ->
            ApplicationEnv2;
        {value,
         {mod, {ApplicationModule, ApplicationModuleList}}, ApplicationEnv3} ->
            NewApplicationModule = orddict:fetch(ApplicationModule, NameLookup),
            [{mod, {NewApplicationModule,
                    ApplicationModuleList}} | ApplicationEnv3]
    end,
    ApplicationEnv6 = case lists:keytake(registered, 1, ApplicationEnv4) of
        false ->
            ApplicationEnv4;
        {value, {registered, RegisteredNames}, ApplicationEnv5} ->
            NewRegisteredNames = lists:map(fun(M) ->
                case orddict:find(M, NameLookup) of
                    {ok, NewM} ->
                        NewM;
                    error ->
                        M
                end
            end, RegisteredNames),
            [{registered, NewRegisteredNames} | ApplicationEnv5]
    end,
    ApplicationEnv8 = case lists:keytake(applications, 1, ApplicationEnv6) of
        false ->
            ApplicationEnv6;
        {value, {applications, ApplicationNames}, ApplicationEnv7} ->
            NewApplicationNames = lists:map(fun(M) ->
                case orddict:find(M, NameLookup) of
                    {ok, NewM} ->
                        NewM;
                    error ->
                        M
                end
            end, ApplicationNames),
            [{applications, NewApplicationNames} | ApplicationEnv7]
    end,
    ApplicationEnv10 = case lists:keytake(included_applications, 1,
                                          ApplicationEnv8) of
        false ->
            ApplicationEnv8;
        {value, {included_applications, IncludedApplicationNames},
         ApplicationEnv9} ->
            NewIncludedApplicationNames = lists:map(fun(M) ->
                case orddict:find(M, NameLookup) of
                    {ok, NewM} ->
                        NewM;
                    error ->
                        M
                end
            end, IncludedApplicationNames),
            [{included_applications, NewIncludedApplicationNames} |
             ApplicationEnv9]
    end,
    ApplicationEnvN = ApplicationEnv10,
    ok = file:write_file(NewFilePath, 
                         io_lib:format("~p.~n",
                                       [{application,
                                         NewApplicationName,
                                         ApplicationEnvN}])),
    files_rename_apps(FilesApplications, NameLookup).

files_update_apps([], _) ->
    ok;
files_update_apps([FilePath | FilesApplications], NameLookup) ->
    FileExtension = filename:extension(FilePath),
    FileName = filename:basename(FilePath, FileExtension),
    ApplicationName = if
        FileExtension == ".app" ->
            erlang:list_to_atom(FileName);
        FileExtension == ".src" ->
            erlang:list_to_atom(filename:basename(FileName, ".app"))
    end,
    % process the application file for renamed applications
    true = filelib:is_regular(FilePath),
    {ok, [{application,
           ApplicationNameFile,
           ApplicationEnv0}]} = file:consult(FilePath),
    true = (ApplicationNameFile =:= ApplicationName),
    ApplicationEnv2 = case lists:keytake(applications, 1, ApplicationEnv0) of
        false ->
            ApplicationEnv0;
        {value, {applications, ApplicationNames}, ApplicationEnv1} ->
            NewApplicationNames = lists:map(fun(M) ->
                case orddict:find(M, NameLookup) of
                    {ok, NewM} ->
                        NewM;
                    error ->
                        M
                end
            end, ApplicationNames),
            [{applications, NewApplicationNames} | ApplicationEnv1]
    end,
    ApplicationEnvN = ApplicationEnv2,
    ok = file:write_file(FilePath, 
                         io_lib:format("~p.~n",
                                       [{application,
                                         ApplicationName,
                                         ApplicationEnvN}])),
    files_update_apps(FilesApplications, NameLookup).

files_rename_protobuffs([], _) ->
    ok;
files_rename_protobuffs(FilesProtobuffs, PrefixScope) ->
    Regexes = lists:map(fun(FilePath) ->
        FileName = filename:basename(FilePath),
        {ok, R} = re:compile("import \"" ++ FileName ++ "\";"),
        fun (S) ->
            re:replace(S, R,
                       "import \"" ++ PrefixScope ++ FileName ++ "\";",
                       [global, {return, list}])
        end
    end, FilesProtobuffs),
    pforeach(fun(FilePath) ->
        {ok, FileContents} = file:read_file(FilePath),
        NewFileContents = reduce(Regexes, erlang:binary_to_list(FileContents)),
        FileName = filename:basename(FilePath),
        NewFilePath = filename:join(filename:dirname(FilePath),
                                    PrefixScope ++ FileName),
        ok = file:write_file(NewFilePath, NewFileContents)
    end, FilesProtobuffs).

files_move_old([], _) ->
    ok;
files_move_old([FilePath | FilePaths], DirectorySuffixOld) ->
    Directory = filename:dirname(FilePath),
    NewDirectory = Directory ++ DirectorySuffixOld,
    case filelib:is_dir(NewDirectory) of
        true ->
            ok;
        false ->
            ok = file:make_dir(NewDirectory)
    end,
    NewFilePath = filename:join(NewDirectory, filename:basename(FilePath)),
    ok = file:rename(FilePath, NewFilePath),
    true = filelib:is_regular(NewFilePath),
    files_move_old(FilePaths, DirectorySuffixOld).

create_regexes(NameConversions) ->
    OrderedNameConversions = lists:sort(lists:map(fun({Name1, Name2}) ->
        {erlang:length(erlang:atom_to_list(Name1)), Name1, Name2}
    end, NameConversions)),
    % make sure regexes will be processed with the longest names first
    % to avoid erroneous replacements
    % (not necessary, just a way of making bugs simpler)
    lists:map(fun({_, Name1, Name2}) ->
        {ok, R} = re:compile("([^a-z0-9_])" ++
                             erlang:atom_to_list(Name1) ++
                             "([^a-z0-9_])"),
        fun (S) ->
            re:replace(S, R,
                       "\\1" ++ erlang:atom_to_list(Name2) ++ "\\2",
                       [global, {return, list}])
        end
    end, lists:reverse(OrderedNameConversions)).

reduce([], V) ->
    V;
reduce([H | T], V) ->
    reduce(T, H(V)).

rename(FilesSource, FilesInclude, IncludeDirectories,
       NameConversions, NameLookup, DirectorySuffixOld) ->
    rename_regex(FilesSource, FilesInclude, IncludeDirectories,
                 NameConversions, NameLookup, DirectorySuffixOld).

rename_regex(FilesSource, FilesInclude, _IncludeDirectories,
             NameConversions, NameLookup, DirectorySuffixOld) ->
    Regexes = create_regexes(NameConversions),
    ProgressBar = progress_bar_pid(erlang:length(FilesSource) +
                                   erlang:length(FilesInclude)),
    Fconversion = fun(FileExtension) -> fun(FilePath) ->
        {ok, FileContents} = file:read_file(FilePath),
        NewFileContents = reduce(Regexes, erlang:binary_to_list(FileContents)),
        Name1 = filename:basename(FilePath, FileExtension),
        Name2 = erlang:atom_to_list(orddict:fetch(erlang:list_to_atom(Name1),
                                                  NameLookup)),
        NewFilePath = filename:join(filename:dirname(FilePath),
                                    Name2 ++ FileExtension),
        ok = file:write_file(NewFilePath, NewFileContents),
        progress_bar_pid_increment(ProgressBar, 1)
    end end,
    pforeach(Fconversion(".erl"), FilesSource),
    pforeach(Fconversion(".hrl"), FilesInclude),
    ok = files_move_old(FilesSource, DirectorySuffixOld),
    ok = files_move_old(FilesInclude, DirectorySuffixOld),
    ok.

pforeach(F, L) when length(L) =< 4 ->
    lists:foreach(F, L);
pforeach(F, L) ->
    N = erlang:system_info(schedulers),
    Pids = lists:map(fun(_) ->
        erlang:spawn_link(fun pforeach_pid/0)
    end, lists:seq(1, N)),
    pforeach_run([], Pids, L, F).

pforeach_pid() ->
    receive
        {execute, F, Arg1} ->
            F(Arg1),
            pforeach_pid();
        {exit, Pid} ->
            Pid ! exit,
            ok
    end.

pforeach_run(OldPids, NewPids, [], _F) ->
    Self = self(),
    Pids = lists:reverse(OldPids) ++ NewPids,
    lists:foreach(fun(Pid) ->
        Pid ! {exit, Self}
    end, Pids),
    lists:foreach(fun(_) ->
        receive exit -> ok end
    end, Pids),
    ok;
pforeach_run(OldPids, [], L, F) ->
    pforeach_run([], lists:reverse(OldPids), L, F);
pforeach_run(OldPids, [Pid | NewPids], [Arg1 | L], F) ->
    Pid ! {execute, F, Arg1},
    pforeach_run([Pid | OldPids], NewPids, L, F).

main_arguments(Arguments) ->
    Config0 = main_arguments(main_arguments_split(Arguments), #config{}),
    Config1 = if
        Config0#config.prefix_ignore =:= undefined ->
            Config0#config{prefix_ignore = Config0#config.prefix_scope};
        Config0#config.prefix_scope =:= undefined ->
            Config0#config{prefix_scope = Config0#config.prefix_ignore};
        true ->
            Config0
    end,
    Config2 = Config1#config{
        directories_in = lists:reverse(Config1#config.directories_in)},
    ConfigN = Config2,
    true = (((if ConfigN#config.help -> 1; true -> 0 end) +
             (if ConfigN#config.undo -> 1; true -> 0 end) +
             (if ConfigN#config.replace_usage -> 1; true -> 0 end)) =< 1),
    ConfigN.

main_arguments([], Config) ->
    Config;
main_arguments(["-d", Directory | Arguments],
               #config{directories_in = Directories} = Config) ->
    false = lists:member(Directory, Directories),
    NewConfig = Config#config{directories_in = [Directory | Directories]},
    main_arguments(Arguments, NewConfig);
main_arguments(["-p", PrefixIgnore | Arguments],
               #config{prefix_ignore = undefined} = Config) ->
    NewConfig = Config#config{prefix_ignore = PrefixIgnore},
    main_arguments(Arguments, NewConfig);
main_arguments(["-s", PrefixScope | Arguments],
               #config{prefix_scope = undefined} = Config) ->
    NewConfig = Config#config{prefix_scope = PrefixScope},
    main_arguments(Arguments, NewConfig);
main_arguments(["-b", DirectorySuffixOld | Arguments],
               Config) ->
    NewConfig = Config#config{directory_suffix_old = DirectorySuffixOld},
    main_arguments(Arguments, NewConfig);
main_arguments(["-c", Directory | Arguments],
               #config{include_directories = Directories} = Config) ->
    false = lists:member(Directory, Directories),
    NewConfig = Config#config{include_directories = [Directory | Directories]},
    main_arguments(Arguments, NewConfig);
main_arguments(["-r" | Arguments], Config) ->
    NewConfig = Config#config{replace_usage = true},
    main_arguments(Arguments, NewConfig);
main_arguments(["-u" | Arguments], Config) ->
    NewConfig = Config#config{undo = true},
    main_arguments(Arguments, NewConfig);
main_arguments(["-h" | Arguments], Config) ->
    NewConfig = Config#config{help = true},
    main_arguments(Arguments, NewConfig);
main_arguments(["-V" | Arguments], Config) ->
    NewConfig = Config#config{version = true},
    main_arguments(Arguments, NewConfig);
main_arguments([Unknown | _], _) ->
    erlang:exit({badarg, {command_line, Unknown}}).

main_arguments_split([] = Arguments) ->
    Arguments;
main_arguments_split([[$-, _] = Argument | Arguments]) ->
    [Argument | main_arguments_split(Arguments)];
main_arguments_split([[$-, C | L] | Arguments]) ->
    [[$-, C] | main_arguments_split([[$- | L] | Arguments])];
main_arguments_split([Argument | Arguments]) ->
    [Argument | main_arguments_split(Arguments)].

version_info() ->
    {ok, Data} = file:read_file(?FILE),
    ByteSize = erlang:byte_size(Data),
    <<B01:32, B02:32, B03:32, B04:32, B05:32, B06:32, B07:32, B08:32,
      B09:32, B10:32, B11:32, B12:32, B13:32, B14:32, B15:32, B16:32>> =
        crypto:hash(sha512, Data),
    Info = ?MODULE:module_info(),
    {attributes, Attributes} = lists:keyfind(attributes, 1, Info),
    {vsn, Version} = lists:keyfind(vsn, 1, Attributes),
    io_lib:format("scope v~s~n"
                  "~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b"
                  "~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b~8.16.0b"
                  "~8.16.0b~8.16.0b sha512sum, ~6w bytes -------",
                  [Version,
                   B01, B02, B03, B04, B05, B06, B07, B08,
                   B09, B10, B11, B12, B13, B14, B15, B16,
                   ByteSize]).

progress_bar_new(CountX)
    when is_integer(CountX), CountX >= 0 ->
    #progress_bar{stdout = erlang:open_port({fd, 0, 1}, [out, {line, 256}]),
                  count_x = CountX}.

progress_bar_increment(I, #progress_bar{percentage_current = PercentageCurrent,
                                        refresh = Refresh,
                                        count_i = CountI,
                                        count_x = CountX} = ProgressBar)
    when is_integer(I), I > 0 ->
    NewCountI = CountI + I,
    NewPercentageCurrent = erlang:min(erlang:round((NewCountI /
                                                    CountX) * 100.0), 100),
    NewRefresh = Refresh orelse (PercentageCurrent /= NewPercentageCurrent),
    ProgressBar#progress_bar{percentage_current = NewPercentageCurrent,
                             refresh = NewRefresh,
                             count_i = NewCountI}.

progress_bar_display(#progress_bar{refresh = false} = ProgressBar) ->
    ProgressBar;
progress_bar_display(#progress_bar{percentage = Percentage,
                                   width_bar = WidthBar,
                                   width_space = WidthSpace,
                                   width_end = WidthEnd,
                                   width = Width,
                                   percentage_current = PercentageCurrent,
                                   refresh = true,
                                   stdout = STDOUT} = ProgressBar) ->
    WidthCurrent = erlang:min(erlang:round(Width *
                                           (PercentageCurrent / 100.0)), Width),
    Line = ["\r", (if
        WidthCurrent == Width ->
            lists:duplicate(WidthCurrent, WidthBar);
        WidthCurrent < Width ->
            [lists:duplicate(WidthCurrent, WidthBar),
             lists:duplicate(Width - (WidthCurrent + 1), WidthSpace),
             [WidthEnd]]
    end), (if
        Percentage =:= true ->
            io_lib:format(" ~3.. w%", [PercentageCurrent]);
        Percentage =:= false ->
            []
    end)],
    erlang:port_command(STDOUT, Line),
    ProgressBar#progress_bar{refresh = false}.

progress_bar_destroy(#progress_bar{stdout = STDOUT}) ->
    erlang:port_command(STDOUT, "\n"),
    erlang:port_close(STDOUT),
    ok.

progress_bar_pid(CountX) ->
    erlang:spawn_link(fun() ->
        progress_bar(progress_bar_new(CountX))
    end).

progress_bar_pid_increment(Pid, I)
    when is_pid(Pid), is_integer(I), I > 0 ->
    Pid ! {progress_bar_increment, I},
    ok.

progress_bar(#progress_bar{refresh = true} = ProgressBar) ->
    progress_bar(progress_bar_display(ProgressBar));
progress_bar(#progress_bar{count_i = CountI,
                           count_x = CountX} = ProgressBar)
    when CountI >= CountX ->
    progress_bar_destroy(ProgressBar);
progress_bar(#progress_bar{} = ProgressBar) ->
    receive
        {progress_bar_increment, I} ->
            progress_bar(progress_bar_increment(I, ProgressBar))
    end.

-compile({nowarn_deprecated_function, {string, to_lower, 1}}).
lowercase(String) ->
    case erlang:function_exported(string, lowercase, 1) of
        true ->
            string:lowercase(String);
        false ->
            string:to_lower(String)
    end.

exit_code(ExitCode) ->
    erlang:halt(ExitCode, [{flush, true}]).

help() ->
    Config = #config{},
    io:format(
"Usage: ~s [OPTION]~n"
"~n"
"    -d DIR           Source directory of an application for input files~n"
"    -s SCOPE         Scope to apply~n"
"    -p SCOPE         Scope to ignore~n"
"    -b SUFFIX        Set the subdirectory name suffix for file backups~n"
"                     (default=\"~s\")~n"
"    -c DIR           Add a directory to the include path~n"
"    -u               Undo the scope operation mode~n"
"    -r               Replace ignored scope usage of apply scope mode~n"
"                     (done before the apply scope is first created)~n"
"    -V               Display the version information~n"
"    -h               Display command line options (as shown here)~n"
"~n"
" Note1: When processed files are moved to the application source directory~n"
"        with the old suffix (-b option), it is possible an older file will~n"
"        be silently clobbered~n"
" Note2: When using the -r mode, files are modified in-place (i.e., all files~n"
"        matching the ignored scope) and the result may require manual~n"
"        modification so that data structure names do not look obtuse~n"
,
        [escript:script_name(),
         Config#config.directory_suffix_old]).

