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

   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
{

File ArgumentList::Argument::resolveAsFile() const
{
    return File::getCurrentWorkingDirectory().getChildFile (text.unquoted());
}

File ArgumentList::Argument::resolveAsExistingFile() const
{
    auto f = resolveAsFile();

    if (! f.exists())
        ConsoleApplication::fail ("Could not find file: " + f.getFullPathName());

    return f;
}

File ArgumentList::Argument::resolveAsExistingFolder() const
{
    auto f = resolveAsFile();

    if (! f.isDirectory())
        ConsoleApplication::fail ("Could not find folder: " + f.getFullPathName());

    return f;
}

bool ArgumentList::Argument::isLongOption() const     { return text[0] == '-' && text[1] == '-' && text[2] != '-'; }
bool ArgumentList::Argument::isShortOption() const    { return text[0] == '-' && text[1] != '-'; }

bool ArgumentList::Argument::isLongOption (const String& option) const
{
    if (option.startsWith ("--"))
        return text == option;

    jassert (! option.startsWithChar ('-')); // this will always fail to match

    return text == "--" + option;
}

bool ArgumentList::Argument::isShortOption (char option) const
{
    jassert (option != '-'); // this is probably not what you intended to pass in

    return isShortOption() && text.containsChar (option);
}

static bool compareOptionStrings (StringRef s1, StringRef s2)
{
    if (s1 == s2)
        return true;

    auto toks1 = StringArray::fromTokens (s1, "|", {});
    auto toks2 = StringArray::fromTokens (s2, "|", {});

    for (auto& part1 : toks1)
        for (auto& part2 : toks2)
            if (part1.trim() == part2.trim())
                return true;

    return false;
}

bool ArgumentList::Argument::operator== (StringRef s) const   { return compareOptionStrings (text, s); }
bool ArgumentList::Argument::operator!= (StringRef s) const   { return ! operator== (s); }

//==============================================================================
ArgumentList::ArgumentList (String exeName, StringArray args)
    : executableName (std::move (exeName))
{
    args.trim();

    for (auto& a : args)
        arguments.add ({ a });
}

ArgumentList::ArgumentList (int argc, char* argv[])
    : ArgumentList (argv[0], StringArray (argv + 1, argc - 1))
{
}

ArgumentList::ArgumentList (const String& exeName, const String& args)
    : ArgumentList (exeName, StringArray::fromTokens (args, true))
{
}

int ArgumentList::size() const                                      { return arguments.size(); }
ArgumentList::Argument ArgumentList::operator[] (int index) const   { return arguments[index]; }

void ArgumentList::checkMinNumArguments (int expectedMinNumberOfArgs) const
{
    if (size() < expectedMinNumberOfArgs)
        ConsoleApplication::fail ("Not enough arguments!");
}

int ArgumentList::indexOfOption (StringRef option) const
{
    jassert (option == String (option).trim()); // passing non-trimmed strings will always fail to find a match!

    for (int i = 0; i < arguments.size(); ++i)
        if (arguments.getReference(i) == option)
            return i;

    return -1;
}

bool ArgumentList::containsOption (StringRef option) const
{
    return indexOfOption (option) >= 0;
}

void ArgumentList::failIfOptionIsMissing (StringRef option) const
{
    if (! containsOption (option))
        ConsoleApplication::fail ("Expected the option " + option);
}

ArgumentList::Argument ArgumentList::getArgumentAfterOption (StringRef option) const
{
    for (int i = 0; i < arguments.size() - 1; ++i)
        if (arguments.getReference(i) == option)
            return arguments.getReference (i + 1);

    return {};
}

File ArgumentList::getFileAfterOption (StringRef option) const
{
    failIfOptionIsMissing (option);
    auto arg = getArgumentAfterOption (option);

    if (arg.text.isEmpty() || arg.text.startsWithChar ('-'))
        ConsoleApplication::fail ("Expected a filename after the " + option + " option");

    return arg.resolveAsFile();
}

File ArgumentList::getExistingFileAfterOption (StringRef option) const
{
    failIfOptionIsMissing (option);
    auto arg = getArgumentAfterOption (option);

    if (arg.text.isEmpty())
        ConsoleApplication::fail ("Expected a filename after the " + option + " option");

    return arg.resolveAsExistingFile();
}

File ArgumentList::getExistingFolderAfterOption (StringRef option) const
{
    failIfOptionIsMissing (option);
    auto arg = getArgumentAfterOption (option);

    if (arg.text.isEmpty())
        ConsoleApplication::fail ("Expected a folder name after the " + option + " option");

    return arg.resolveAsExistingFolder();
}

//==============================================================================
struct ConsoleAppFailureCode
{
    String errorMessage;
    int returnCode;
};

void ConsoleApplication::fail (String errorMessage, int returnCode)
{
    throw ConsoleAppFailureCode { std::move (errorMessage), returnCode };
}

int ConsoleApplication::invokeCatchingFailures (std::function<int()>&& f)
{
    int returnCode = 0;

    try
    {
        returnCode = f();
    }
    catch (const ConsoleAppFailureCode& error)
    {
        std::cout << error.errorMessage << std::endl;
        returnCode = error.returnCode;
    }

    return returnCode;
}

int ConsoleApplication::findAndRunCommand (const ArgumentList& args) const
{
    for (auto& c : commands)
        if (args.containsOption (c.commandOption))
            return invokeCatchingFailures ([&] { c.command (args); return 0; });

    if (commandIfNoOthersRecognised.isNotEmpty())
        for (auto& c : commands)
            if (compareOptionStrings (c.commandOption, commandIfNoOthersRecognised))
                return invokeCatchingFailures ([&] { c.command (args); return 0; });

    fail ("Unrecognised arguments");
    return 0;
}

int ConsoleApplication::findAndRunCommand (int argc, char* argv[]) const
{
    return findAndRunCommand (ArgumentList (argc, argv));
}

void ConsoleApplication::addCommand (Command c)
{
    commands.emplace_back (std::move (c));
}

void ConsoleApplication::addHelpCommand (String arg, String helpMessage, bool invokeIfNoOtherCommandRecognised)
{
    addCommand ({ arg, arg, "Prints this message",
                  [this, helpMessage] (const ArgumentList& args) { printHelp (helpMessage, args); }});

    if (invokeIfNoOtherCommandRecognised)
        commandIfNoOthersRecognised = arg;
}

void ConsoleApplication::addVersionCommand (String arg, String versionText)
{
    addCommand ({ arg, arg, "Prints the current version number",
                  [versionText] (const ArgumentList&)
                  {
                      std::cout << versionText << std::endl;
                  }});
}

void ConsoleApplication::printHelp (const String& preamble, const ArgumentList& args) const
{
    std::cout << preamble << std::endl;

    auto exeName = args.executableName.fromLastOccurrenceOf ("/", false, false)
                                      .fromLastOccurrenceOf ("\\", false, false);

    StringArray namesAndArgs;
    int maxLength = 0;

    for (auto& c : commands)
    {
        auto nameAndArgs = exeName + " " + c.argumentDescription;
        namesAndArgs.add (nameAndArgs);
        maxLength = std::max (maxLength, nameAndArgs.length());
    }

    for (size_t i = 0; i < commands.size(); ++i)
        std::cout << " " << namesAndArgs[(int) i].paddedRight (' ', maxLength + 2)
                  << commands[i].commandDescription << std::endl;

    std::cout << std::endl;
}

} // namespace juce
