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

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   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
*/
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.
    */
    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;

        /** Returns true if this argument starts with a single dash and then contains the given character somewhere inside it. */
        bool isShortOption (char shortOptionCharacter) 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"
    */
    bool containsOption (StringRef option) const;

    /** 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 the given argument and returns the one that follows it in the list.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        If the argument isn't found, this returns an empty string.
    */
    Argument getArgumentAfterOption (StringRef option) const;

    /** Looks for a given argument and tries to parse the following argument as a file.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        If the option isn't found, or if the next argument isn't a filename, it will throw
        an error.
    */
    File getFileAfterOption (StringRef option) const;

    /** Looks for a given argument and tries to parse the following argument as a file
        which must exist for this to succeed.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        If the option isn't found, or if the next argument isn't a filename, or if the file
        doesn't exist, or if it's a folder rather than a file, then it will throw a suitable error.
    */
    File getExistingFileAfterOption (StringRef option) const;

    /** Looks for a given argument and tries to parse the following argument as a folder
        which must exist for this to succeed.
        The option can also be a list of different versions separated by pipes, e.g. "--help|-h"
        If the option isn't found, or if the next argument isn't a filename, or if it doesn't
        point to a folder, then it will throw a suitable error.
    */
    File getExistingFolderAfterOption (StringRef option) const;

    /** 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
*/
struct ConsoleApplication
{
    //==============================================================================
    /**
        Represents a command that can be executed if its command-line arguments are matched.
        @see ConsoleApplication::addCommand(), ConsoleApplication::findAndRunCommand()
    */
    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 description of the meaning of this command, for use in the help text. */
        String commandDescription;

        /** 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 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 invokeIfNoOtherCommandRecognised);

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

    //==============================================================================
    /** 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, it prints a help message listing the available commands.
        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.
    */
    int findAndRunCommand (const ArgumentList&) const;

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

private:
    //==============================================================================
    std::vector<Command> commands;
    String commandIfNoOthersRecognised;

    void printHelp (const String& preamble, const ArgumentList&) const;
};

} // namespace juce
