/**
 * @file llcommandlineparser.cpp
 * @brief The LLCommandLineParser class definitions
 *
 * $LicenseInfo:firstyear=2007&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"
#include "llcommandlineparser.h"
#include "llexception.h"

#include <boost/program_options.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/bind.hpp>
#include <boost/tokenizer.hpp>
#include <boost/assign/list_of.hpp>

#include "llsdserialize.h"
#include "llerror.h"
#include "stringize.h"
#include "llexception.h"
#include <string>
#include <set>
#include <iostream>
#include <sstream>
#include <typeinfo>

#include "llcontrol.h"

namespace po = boost::program_options;

// *NOTE:MEP - Currently the boost object reside in file scope.
// This has a couple of negatives, they are always around and
// there can be only one instance of each.
// The plus is that the boost-ly-ness of this implementation is
// hidden from the rest of the world.
// Its importatnt to realize that multiple LLCommandLineParser objects
// will all have this single repository of option escs and parsed options.
// This could be good or bad, and probably won't matter for most use cases.
namespace
{
    // List of command-line switches that can't map-to settings variables.
    // Going forward, we want every new command-line switch to map-to some
    // settings variable. This list is used to validate that.
    const std::set<std::string> unmapped_options = boost::assign::list_of
        ("help")
        ("set")
        ("setdefault")
        ("settings")
        ("sessionsettings")
        ("usersessionsettings")
    ;

    po::options_description gOptionsDesc;
    po::positional_options_description gPositionalOptions;
    po::variables_map gVariableMap;

    const LLCommandLineParser::token_vector_t gEmptyValue;

    void read_file_into_string(std::string& str, const std::basic_istream < char >& file)
    {
        std::ostringstream oss;
        oss << file.rdbuf();
        str = oss.str();
    }

    bool gPastLastOption = false;
}

class LLCLPError : public LLException {
public:
    LLCLPError(const std::string& what) : LLException(what) {}
};

class LLCLPLastOption : public LLException {
public:
    LLCLPLastOption(const std::string& what) : LLException(what) {}
};

class LLCLPValue : public po::value_semantic_codecvt_helper<char>
{
    unsigned mMinTokens;
    unsigned mMaxTokens;
    bool mIsComposing;
    typedef boost::function1<void, const LLCommandLineParser::token_vector_t&> notify_callback_t;
    notify_callback_t mNotifyCallback;
    bool mLastOption;

public:
    LLCLPValue() :
        mMinTokens(0),
        mMaxTokens(0),
        mIsComposing(false),
        mLastOption(false)
        {}

    virtual ~LLCLPValue() {};

    void setMinTokens(unsigned c)
    {
        mMinTokens = c;
    }

    void setMaxTokens(unsigned c)
    {
        mMaxTokens = c;
    }

    void setComposing(bool c)
    {
        mIsComposing = c;
    }

    void setLastOption(bool c)
    {
        mLastOption = c;
    }

    void setNotifyCallback(notify_callback_t f)
    {
        mNotifyCallback = f;
    }

    // Overrides to support the value_semantic interface.
    virtual std::string name() const
    {
        const std::string arg("arg");
        const std::string args("args");
        return (max_tokens() > 1) ? args : arg;
    }

    virtual unsigned min_tokens() const
    {
        return mMinTokens;
    }

    virtual unsigned max_tokens() const
    {
        return mMaxTokens;
    }

    virtual bool is_composing() const
    {
        return mIsComposing;
    }

    // Needed for boost 1.42
    virtual bool is_required() const
    {
        return false; // All our command line options are optional.
    }

    virtual bool apply_default(boost::any& value_store) const
    {
        return false; // No defaults.
    }

    virtual void notify(const boost::any& value_store) const
    {
        const LLCommandLineParser::token_vector_t* value =
            boost::any_cast<const LLCommandLineParser::token_vector_t>(&value_store);
        if(mNotifyCallback)
        {
           mNotifyCallback(*value);
        }
    }

protected:
    void xparse(boost::any& value_store,
         const std::vector<std::string>& new_tokens) const
    {
        if(gPastLastOption)
        {
            LLTHROW(LLCLPLastOption("Don't parse no more!"));
        }

        // Error checks. Needed?
        if (!value_store.empty() && !is_composing())
        {
            LLTHROW(LLCLPError("Non composing value with multiple occurences."));
        }
        if (new_tokens.size() < min_tokens() || new_tokens.size() > max_tokens())
        {
            LLTHROW(LLCLPError("Illegal number of tokens specified."));
        }

        if(value_store.empty())
        {
            value_store = boost::any(LLCommandLineParser::token_vector_t());
        }
        LLCommandLineParser::token_vector_t* tv =
            boost::any_cast<LLCommandLineParser::token_vector_t>(&value_store);

        for(unsigned i = 0; i < new_tokens.size() && i < mMaxTokens; ++i)
        {
            tv->push_back(new_tokens[i]);
        }

        if(mLastOption)
        {
            gPastLastOption = true;
        }
    }
};

//----------------------------------------------------------------------------
// LLCommandLineParser defintions
//----------------------------------------------------------------------------
void LLCommandLineParser::addOptionDesc(const std::string& option_name,
                                        boost::function1<void, const token_vector_t&> notify_callback,
                                        unsigned int token_count,
                                        const std::string& description,
                                        const std::string& short_name,
                                        bool composing,
                                        bool positional,
                                        bool last_option)
{
    // Compose the name for boost::po.
    // It takes the format "long_name, short name"
    const std::string comma(",");
    std::string boost_option_name = option_name;
    if(short_name != LLStringUtil::null)
    {
        boost_option_name += comma;
        boost_option_name += short_name;
    }

    LLCLPValue* value_desc = new LLCLPValue();
    value_desc->setMinTokens(token_count);
    value_desc->setMaxTokens(token_count);
    value_desc->setComposing(composing);
    value_desc->setLastOption(last_option);

    boost::shared_ptr<po::option_description> d(
            new po::option_description(boost_option_name.c_str(),
                                    value_desc,
                                    description.c_str()));

    if(!notify_callback.empty())
    {
        value_desc->setNotifyCallback(notify_callback);
    }

    gOptionsDesc.add(d);

    if(positional)
    {
        gPositionalOptions.add(boost_option_name.c_str(), token_count);
    }
}

bool LLCommandLineParser::parseAndStoreResults(po::command_line_parser& clp)
{
    try
    {
        clp.options(gOptionsDesc);
        clp.positional(gPositionalOptions);
        // SNOW-626: Boost 1.42 erroneously added allow_guessing to the default style
        // (see http://groups.google.com/group/boost-list/browse_thread/thread/545d7bf98ff9bb16?fwc=2&pli=1)
        // Remove allow_guessing from the default style, because that is not allowed
        // when we have options that are a prefix of other options (aka, --help and --helperuri).
        clp.style((po::command_line_style::default_style & ~po::command_line_style::allow_guessing)
                  | po::command_line_style::allow_long_disguise);
        if(mExtraParser)
        {
            clp.extra_parser(mExtraParser);
        }

        po::basic_parsed_options<char> opts = clp.run();
        po::store(opts, gVariableMap);
    }
    catch(po::error& e)
    {
        LL_WARNS() << "Caught Error:" << e.what() << LL_ENDL;
        mErrorMsg = e.what();
        return false;
    }
    catch(LLCLPError& e)
    {
        LL_WARNS() << "Caught Error:" << e.what() << LL_ENDL;
        mErrorMsg = e.what();
        return false;
    }
    catch(LLCLPLastOption&)
    {
        // This exception means a token was read after an option
        // that must be the last option was reached (see url and slurl options)

        // boost::po will have stored a malformed option.
        // All such options will be removed below.
        // The last option read, the last_option option, and its value
        // are put into the error message.
        std::string last_option;
        std::string last_value;
        for(po::variables_map::iterator i = gVariableMap.begin(); i != gVariableMap.end();)
        {
            po::variables_map::iterator tempI = i++;
            if(tempI->second.empty())
            {
                gVariableMap.erase(tempI);
            }
            else
            {
                last_option = tempI->first;
                LLCommandLineParser::token_vector_t* tv =
                    boost::any_cast<LLCommandLineParser::token_vector_t>(&(tempI->second.value()));
                if(!tv->empty())
                {
                    last_value = (*tv)[tv->size()-1];
                }
            }
        }

        // Continue without parsing.
        std::ostringstream msg;
        msg << "Caught Error: Found options after last option: "
            << last_option << " "
            << last_value;

        LL_WARNS() << msg.str() << LL_ENDL;
        mErrorMsg = msg.str();
        return false;
    }
    return true;
}

bool LLCommandLineParser::parseCommandLine(int argc, char **argv)
{
    po::command_line_parser clp(argc, argv);
    return parseAndStoreResults(clp);
}

// TODO:
// - Break out this funky parsing logic into separate method
// - Unit-test it with tests like LLStringUtil::getTokens() (the command-line
//   overload that supports quoted tokens)
// - Unless this logic offers significant semantic benefits, replace it with
//   LLStringUtil::getTokens(). This would fix a known bug: you cannot --set a
//   string-valued variable to the empty string, because empty strings are
//   eliminated below.

bool LLCommandLineParser::parseCommandLineString(const std::string& str)
{
    std::string cmd_line_string("");
    if (!str.empty())
    {
        bool add_last_c = true;
        auto last_c_pos = str.size() - 1; //don't get out of bounds on pos+1, last char will be processed separately
        for (size_t pos = 0; pos < last_c_pos; ++pos)
        {
            cmd_line_string.append(&str[pos], 1);
            if (str[pos] == '\\')
            {
                cmd_line_string.append("\\", 1);
                if (str[pos + 1] == '\\')
                {
                    ++pos;
                    add_last_c = (pos != last_c_pos);
                }
            }
        }
        if (add_last_c)
        {
            cmd_line_string.append(&str[last_c_pos], 1);
            if (str[last_c_pos] == '\\')
            {
                cmd_line_string.append("\\", 1);
            }
        }
    }

    std::vector<std::string> tokens;
    try
    {
        // Split the string content into tokens
        const char* escape_chars = "\\";
        const char* separator_chars = "\r\n ";
        const char* quote_chars = "\"'";
        boost::escaped_list_separator<char> sep(escape_chars, separator_chars, quote_chars);
        boost::tokenizer< boost::escaped_list_separator<char> > tok(cmd_line_string, sep);
        // std::copy(tok.begin(), tok.end(), std::back_inserter(tokens));
        for (boost::tokenizer< boost::escaped_list_separator<char> >::iterator i = tok.begin();
            i != tok.end();
            ++i)
        {
            if (0 != i->size())
            {
                tokens.push_back(*i);
            }
        }
    }
    catch (...)
    {
        CRASH_ON_UNHANDLED_EXCEPTION(STRINGIZE("Unexpected crash while parsing: " << str));
    }

    po::command_line_parser clp(tokens);
    return parseAndStoreResults(clp);

}

bool LLCommandLineParser::parseCommandLineFile(const std::basic_istream < char >& file)
{
    std::string args;
    read_file_into_string(args, file);

    return parseCommandLineString(args);
}

bool LLCommandLineParser::notify()
{
    try
    {
        po::notify(gVariableMap);
        return true;
    }
    catch (const LLCLPError& e)
    {
        LL_WARNS() << "Caught Error: " << e.what() << LL_ENDL;
        mErrorMsg = e.what();
        return false;
    }
}

void LLCommandLineParser::printOptions() const
{
    for(po::variables_map::iterator i = gVariableMap.begin(); i != gVariableMap.end(); ++i)
    {
        std::string name = i->first;
        token_vector_t values = i->second.as<token_vector_t>();
        std::ostringstream oss;
        oss << name << ": ";
        for(token_vector_t::iterator t_itr = values.begin(); t_itr != values.end(); ++t_itr)
        {
            oss << t_itr->c_str() << " ";
        }
        LL_INFOS() << oss.str() << LL_ENDL;
    }
}

std::ostream& LLCommandLineParser::printOptionsDesc(std::ostream& os) const
{
    return os << gOptionsDesc;
}

bool LLCommandLineParser::hasOption(const std::string& name) const
{
    return gVariableMap.count(name) > 0;
}

const LLCommandLineParser::token_vector_t& LLCommandLineParser::getOption(const std::string& name) const
{
    if(hasOption(name))
    {
        return gVariableMap[name].as<token_vector_t>();
    }

    return gEmptyValue;
}

//----------------------------------------------------------------------------
// LLControlGroupCLP defintions
//----------------------------------------------------------------------------
namespace {
LLCommandLineParser::token_vector_t::value_type
onevalue(const std::string& option,
         const LLCommandLineParser::token_vector_t& value)
{
    if (value.empty())
    {
        // What does it mean when the user specifies a command-line switch
        // that requires a value, but omits the value? Complain.
        LLTHROW(LLCLPError(STRINGIZE("No value specified for --" << option << "!")));
    }
    else if (value.size() > 1)
    {
        LL_WARNS() << "Ignoring extra tokens specified for --"
                << option << "." << LL_ENDL;
    }
    return value[0];
}

void badvalue(const std::string& option,
              const std::string& varname,
              const std::string& type,
              const std::string& value)
{
    // If the user passes an unusable value for a command-line switch, it
    // seems like a really bad idea to just ignore it, even with a log
    // warning.
    LLTHROW(LLCLPError(STRINGIZE("Invalid value specified by command-line switch '" << option
                                 << "' for variable '" << varname << "' of type " << type
                                 << ": '" << value << "'")));
}

template <typename T>
T convertTo(const std::string& option,
            const std::string& varname,
            const LLCommandLineParser::token_vector_t::value_type& value)
{
    try
    {
        return boost::lexical_cast<T>(value);
    }
    catch (const boost::bad_lexical_cast&)
    {
        badvalue(option, varname, typeid(T).name(), value);
        // bogus return; compiler unaware that badvalue() won't return
        return T();
    }
}

void setControlValueCB(const LLCommandLineParser::token_vector_t& value,
                       const std::string& option,
                       LLControlVariable* ctrl)
{
    // *FIX: Do semantic conversion here.
    // LLSD (ImplString) Is no good for doing string to type conversion for...
    // booleans
    // compound types
    // ?...

    if(NULL != ctrl)
    {
        switch(ctrl->type())
        {
        case TYPE_BOOLEAN:
            if (value.empty())
            {
                // Boolean-valued command-line switches are unusual. If you
                // simply specify the switch without an explicit value, we can
                // infer you mean 'true'.
                ctrl->setValue(LLSD(true), false);
            }
            else
            {
                // Only call onevalue() AFTER handling value.empty() case!
                std::string token(onevalue(option, value));

                // There's a token. check the string for true/false/1/0 etc.
                bool result = false;
                bool gotSet = LLStringUtil::convertToBOOL(token, result);
                if (gotSet)
                {
                    ctrl->setValue(LLSD(result), false);
                }
                else
                {
                    badvalue(option, ctrl->getName(), "bool", token);
                }
            }
            break;

        case TYPE_U32:
        {
            std::string token(onevalue(option, value));
            // To my surprise, for an unsigned target, lexical_cast() doesn't
            // complain about an input string such as "-17". In that case, you
            // get a very large positive result. So for U32, make sure there's
            // no minus sign!
            if (token.find('-') == std::string::npos)
            {
                ctrl->setValue(LLSD::Integer(convertTo<U32>(option, ctrl->getName(), token)),
                               false);
            }
            else
            {
                badvalue(option, ctrl->getName(), "unsigned", token);
            }
            break;
        }

        case TYPE_S32:
            ctrl->setValue(convertTo<S32>(option, ctrl->getName(),
                                          onevalue(option, value)), false);
            break;

        case TYPE_F32:
            ctrl->setValue(convertTo<F32>(option, ctrl->getName(),
                                          onevalue(option, value)), false);
            break;

        // It appears that no one has yet tried to define a command-line
        // switch mapped to a settings variable of TYPE_VEC3, TYPE_VEC3D,
        // TYPE_RECT, TYPE_COL4, TYPE_COL3. Such types would certainly seem to
        // call for a bit of special handling here...
        default:
            {
                // For the default types, let llsd do the conversion.
                if(value.size() > 1 && ctrl->isType(TYPE_LLSD))
                {
                    // Assume its an array...
                    LLSD llsdArray;
                    for(unsigned int i = 0; i < value.size(); ++i)
                    {
                        LLSD llsdValue;
                        llsdValue.assign(LLSD::String(value[i]));
                        llsdArray.set(i, llsdValue);
                    }

                    ctrl->setValue(llsdArray, false);
                }
                else
                {
                    ctrl->setValue(onevalue(option, value), false);
                }
            }
            break;
        }
    }
    else
    {
        // This isn't anything a user can affect -- it's a misconfiguration on
        // the part of the coder. Rub the coder's nose in the problem right
        // away so even preliminary testing will surface it.
        LL_ERRS() << "Command Line option --" << option
               << " maps to unknown setting!" << LL_ENDL;
    }
}
} // anonymous namespace

void LLControlGroupCLP::configure(const std::string& config_filename, LLControlGroup* controlGroup)
{
    // This method reads the llsd based config file, and uses it to set
    // members of a control group.
    LLSD clpConfigLLSD;

    llifstream input_stream;
    input_stream.open(config_filename.c_str(), std::ios::in | std::ios::binary);

    if(input_stream.is_open())
    {
        LLSDSerialize::fromXML(clpConfigLLSD, input_stream);
        for(LLSD::map_iterator option_itr = clpConfigLLSD.beginMap();
            option_itr != clpConfigLLSD.endMap();
            ++option_itr)
        {
            LLSD::String long_name = option_itr->first;
            LLSD option_params = option_itr->second;

            std::string desc("n/a");
            if(option_params.has("desc"))
            {
                desc = option_params["desc"].asString();
            }

            std::string short_name = LLStringUtil::null;
            if(option_params.has("short"))
            {
                short_name = option_params["short"].asString();
            }

            unsigned int token_count = 0;
            if(option_params.has("count"))
            {
                token_count = option_params["count"].asInteger();
            }

            bool composing = false;
            if(option_params.has("compose"))
            {
                composing = option_params["compose"].asBoolean();
            }

            bool positional = false;
            if(option_params.has("positional"))
            {
                positional = option_params["positional"].asBoolean();
            }

            bool last_option = false;
            if(option_params.has("last_option"))
            {
                last_option = option_params["last_option"].asBoolean();
            }

            boost::function1<void, const token_vector_t&> callback;
            if (! option_params.has("map-to"))
            {
                // If this option isn't mapped to a settings variable, is it
                // one of the ones for which that's unreasonable, or did
                // someone carelessly add a new option? (Make all these
                // configuration errors fatal so a maintainer will catch them
                // right away.)
                std::set<std::string>::const_iterator found = unmapped_options.find(long_name);
                if (found == unmapped_options.end())
                {
                    LL_ERRS() << "New command-line option " << long_name
                           << " should map-to a variable in settings.xml" << LL_ENDL;
                }
            }
            else                    // option specifies map-to
            {
                std::string controlName = option_params["map-to"].asString();
                if (! controlGroup)
                {
                    LL_ERRS() << "Must pass gSavedSettings to LLControlGroupCLP::configure() for "
                           << long_name << " (map-to " << controlName << ")" << LL_ENDL;
                }

                LLControlVariable* ctrl = controlGroup->getControl(controlName);
                if (! ctrl)
                {
                    LL_ERRS() << "Option " << long_name << " specifies map-to " << controlName
                           << " which does not exist" << LL_ENDL;
                }

                callback = boost::bind(setControlValueCB, _1, long_name, ctrl);
            }

            this->addOptionDesc(
                long_name,
                callback,
                token_count,
                desc,
                short_name,
                composing,
                positional,
                last_option);
        }
    }
}