/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   To use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

//==============================================================================
/**
    Holds a list of command-line arguments, and provides useful methods for searching
    and operating on them.

    You can create an ArgumentList manually, or give it some argv/argc values from a
    main() function to parse.

    @see ConsoleApplication

    @tags{Core}
*/
struct ArgumentList
{
    /** Creates an argument list for a given executable. */
    ArgumentList (String executable, StringArray arguments);

    /** Parses a standard argv/argc pair to create an argument list. */
    ArgumentList (int argc, char* argv[]);

    /** Tokenises a string containing all the arguments to create an argument list. */
    ArgumentList (const String& executable, const String& arguments);

    ArgumentList (const ArgumentList&) = default;
    ArgumentList& operator= (const ArgumentList&) = default;

    //==============================================================================
    /**
        One of the arguments in an ArgumentList.

        @tags{Core}
    */
    struct Argument
    {
        /** The original text of this argument. */
        String text;

        /** Resolves this argument as an absolute File, using the current working
            directory as a base for resolving relative paths, and stripping quotes, etc.
        */
        File resolveAsFile() const;

        /** Resolves this argument as an absolute File, using the current working
            directory as a base for resolving relative paths, and also doing a check to
            make sure the file exists.
            If the file doesn't exist, this will call fail() with a suitable error.
            @see resolveAsFile, resolveAsExistingFolder
        */
        File resolveAsExistingFile() const;

        /** Resolves a user-supplied folder name into an absolute File, using the current working
            directory as a base for resolving relative paths, and also doing a check to make
            sure the folder exists.
            If the folder doesn't exist, this will call fail() with a suitable error.
            @see resolveAsFile, resolveAsExistingFile
        */
        File resolveAsExistingFolder() const;

        /** Returns true if this argument starts with a double dash. */
        bool isLongOption() const;

        /** Returns true if this argument starts with a single dash. */
        bool isShortOption() const;

        /** Returns true if this argument starts with a double dash, followed by the given string. */
        bool isLongOption (const String& optionRoot) const;

        /** If this argument is a long option with a value, this returns the value.
            e.g. for "--foo=bar", this would return 'bar'.
        */
        String getLongOptionValue() const;

        /** Returns true if this argument starts with a single dash and then contains the given character somewhere inside it. */
        bool isShortOption (char shortOptionCharacter) const;

        /** Returns true if this argument starts with one or more dashes. */
        bool isOption() const;

        /** Compares this argument against a string.
            The string may be a pipe-separated list of options, e.g. "--help|-h"
        */
        bool operator== (StringRef stringToCompare) const;

        /** Compares this argument against a string.
            The string may be a pipe-separated list of options, e.g. "--help|-h"
        */
        bool operator!= (StringRef stringToCompare) const;
    };

    //==============================================================================
    /** Returns the number of arguments in the list. */
    int size() const;

    /** Returns one of the arguments */
    Argument operator[] (int index) const;

    /** Throws an error unless there are at least the given number of arguments. */
    void checkMinNumArguments (int expectedMinNumberOfArgs) const;

    /** Returns true if the given string matches one of the arguments.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        @see removeOptionIfFound
    */
    bool containsOption (StringRef option) const;

    /** Returns true if the given string matches one of the arguments, and also removes the
        argument from the list if found.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        @see containsOption
    */
    bool removeOptionIfFound (StringRef option);

    /** Returns the index of the given string if it matches one of the arguments, or -1 if it doesn't.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
    */
    int indexOfOption (StringRef option) const;

    /** Throws an error unless the given option is found in the argument list. */
    void failIfOptionIsMissing (StringRef option) const;

    /** Looks for a given argument and returns either its assigned value (for long options) or the
        string that follows it (for short options).
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        If it finds a long option, it will look for an assignment with a '=' sign, e.g. "--file=foo.txt",
        and will return the string following the '='. If there's no '=', it will return an empty string.
        If it finds a short option, it will attempt to return the argument that follows it, unless
        it's another option.
        If the argument isn't found, this returns an empty string.
    */
    String getValueForOption (StringRef option) const;

    /** Looks for a given argument and returns either its assigned value (for long options) or the
        string that follows it (for short options).
        This works like getValueForOption() but also removes the option argument (and any value arguments)
        from the list if they are found.
    */
    String removeValueForOption (StringRef option);

    /** Looks for the value of argument using getValueForOption() and tries to parse that value as a file.
        If the option isn't found, or if the value can't be parsed as a filename, it will throw an error.
    */
    File getFileForOption (StringRef option) const;

    /** Looks for the value of argument using getValueForOption() and tries to parse that value as a file.
        This works like getFileForOption() but also removes the option argument (and any value arguments)
        from the list if they are found.
    */
    File getFileForOptionAndRemove (StringRef option);

    /** Looks for a file argument using getFileForOption() and fails with a suitable error if
        the file doesn't exist.
    */
    File getExistingFileForOption (StringRef option) const;

    /** Looks for a file argument using getFileForOption() and fails with a suitable error if
        the file doesn't exist.
        This works like getExistingFileForOption() but also removes the option argument (and any
        value arguments) from the list if they are found.
    */
    File getExistingFileForOptionAndRemove (StringRef option);

    /** Looks for a filename argument using getFileForOption() and fails with a suitable error if
        the file isn't a folder that exists.
    */
    File getExistingFolderForOption (StringRef option) const;

    /** Looks for a filename argument using getFileForOption() and fails with a suitable error if
        the file isn't a folder that exists.
        This works like getExistingFolderForOption() but also removes the option argument (and any
        value arguments) from the list if they are found.
    */
    File getExistingFolderForOptionAndRemove (StringRef option);

    /** The name or path of the executable that was invoked, as it was specified on the command-line. */
    String executableName;

    /** The list of arguments (not including the name of the executable that was invoked). */
    Array<Argument> arguments;
};


//==============================================================================
/**
    Represents a the set of commands that a console app can perform, and provides
    helper functions for performing them.

    When using these helper classes to implement a console app, you probably want to
    do something along these lines:

    @code
    int main (int argc, char* argv[])
    {
        ConsoleApplication app;

        app.addHelpCommand ("--help|-h", "Usage:", true);
        app.addVersionCommand ("--version|-v", "MyApp version 1.2.3");

        app.addCommand ({ "--foo",
                          "--foo filename",
                          "Performs a foo operation on the given file",
                          [] (const auto& args) { doFoo (args); }});

        return app.findAndRunCommand (argc, argv);
    }
    @endcode

    @see ArgumentList

    @tags{Core}
*/
struct ConsoleApplication
{
    //==============================================================================
    /**
        Represents a command that can be executed if its command-line arguments are matched.

        @see ConsoleApplication::addCommand(), ConsoleApplication::findAndRunCommand()

        @tags{Core}
    */
    struct Command
    {
        /** The option string that must appear in the argument list for this command to be invoked.
            This can also be a list of different versions separated by pipes, e.g. "--help|-h"
        */
        String commandOption;

        /** A description of the command-line arguments needed for this command, which will be
            printed as part of the help text.
        */
        String argumentDescription;

        /** A short (one line) description of this command, which can be printed by
            ConsoleApplication::printCommandList().
        */
        String shortDescription;

        /** A longer description of this command, for use in extended help. */
        String longDescription;

        /** The actual command that should be invoked to perform this action. */
        std::function<void (const ArgumentList&)> command;
    };

    //==============================================================================
    /** Adds a command to the list. */
    void addCommand (Command);

    /** Adds a command to the list, and marks it as one which is invoked if no other
        command matches.
    */
    void addDefaultCommand (Command);

    /** Adds a command that will print the given text in response to the "--version" option. */
    void addVersionCommand (String versionArgument, String versionText);

    /** Adds a help command to the list.
        This command will print the user-supplied message that's passed in here as an
        argument, followed by a list of all the registered commands.
    */
    void addHelpCommand (String helpArgument, String helpMessage, bool makeDefaultCommand);

    /** Prints out the list of commands and their short descriptions in a format that's
        suitable for use as help.
    */
    void printCommandList (const ArgumentList&) const;

    /** Prints out a longer description of a particular command, based on its
        longDescription member.
    */
    void printCommandDetails (const ArgumentList&, const Command&) const;

    //==============================================================================
    /** Throws a failure exception to cause a command-line app to terminate.
        This is intended to be called from code in a Command, so that the
        exception will be automatically caught and turned into a printed error message
        and a return code which will be returned from main().
        @see ConsoleApplication::invokeCatchingFailures()
    */
    static void fail (String errorMessage, int returnCode = 1);

    /** Invokes a function, catching any fail() calls that it might trigger, and handling
        them by printing their error message and returning their error code.
        @see ConsoleApplication::fail()
    */
    static int invokeCatchingFailures (std::function<int()>&& functionToCall);

    //==============================================================================
    /** Looks for the first command in the list which matches the given arguments, and
        tries to invoke it.

        If no command is found, and if there is no default command to run, it fails with
        a suitable error message.
        If the command calls the fail() function, this will throw an exception that gets
        automatically caught and handled, and this method will return the error code that
        was passed into the fail() call.

        If optionMustBeFirstArg is true, then only the first argument will be looked at
        when searching the available commands - this lets you do 'git' style commands where
        the executable name is followed by a verb.
    */
    int findAndRunCommand (const ArgumentList&,
                           bool optionMustBeFirstArg = false) const;

    /** Creates an ArgumentList object from the argc and argv variablrs, and invokes
        findAndRunCommand() using it.
    */
    int findAndRunCommand (int argc, char* argv[]) const;

    /** Looks for the first command in the list which matches the given arguments.
        If none is found, this returns either the default command (if one is set)
        or nullptr.
        If optionMustBeFirstArg is true, then only the first argument will be looked at
        when searching the available commands - this lets you do 'git' style commands where
        the executable name is followed by a verb.
    */
    const Command* findCommand (const ArgumentList&, bool optionMustBeFirstArg) const;

    /** Gives read-only access to the list of registered commands. */
    const std::vector<Command>& getCommands() const;

private:
    //==============================================================================
    std::vector<Command> commands;
    int commandIfNoOthersRecognised = -1;
};

} // namespace juce
