// Copyright 2020 Bloomberg Finance L.P
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef INCLUDED_BUILDBOXCOMMON_COMMANDLINETYPES
#define INCLUDED_BUILDBOXCOMMON_COMMANDLINETYPES

#include <any>
#include <map>
#include <optional>
#include <sstream>
#include <string>
#include <variant>
#include <vector>

#include <buildboxcommon_exception.h>

namespace buildboxcommon {

struct CommandLineTypes {

    enum DataType {
        COMMANDLINE_DT_STRING,
        COMMANDLINE_DT_INT,
        COMMANDLINE_DT_DOUBLE,
        COMMANDLINE_DT_BOOL,
        COMMANDLINE_DT_STRING_ARRAY,
        COMMANDLINE_DT_STRING_PAIR_ARRAY,

        COMMANDLINE_DT_UNKNOWN
    };

    struct Type {
        typedef std::vector<std::string> VectorOfString;
        typedef std::pair<std::string, std::string> PairOfString;
        typedef std::vector<PairOfString> VectorOfPairOfString;
    };

    struct DefaultValue {
        DataType d_dataType;
        DefaultValue()
            : d_dataType(COMMANDLINE_DT_UNKNOWN), d_value(std::nullopt)
        {
        }

        DefaultValue(const char *defaultValue)
            : d_dataType(COMMANDLINE_DT_STRING),
              d_value(std::string(defaultValue))
        {
        }
        DefaultValue(const std::string &defaultValue)
            : d_dataType(COMMANDLINE_DT_STRING), d_value(defaultValue)
        {
        }
        DefaultValue(const int defaultValue)
            : d_dataType(COMMANDLINE_DT_INT), d_value(defaultValue)
        {
        }
        DefaultValue(const double defaultValue)
            : d_dataType(COMMANDLINE_DT_DOUBLE), d_value(defaultValue)
        {
        }
        DefaultValue(const bool defaultValue)
            : d_dataType(COMMANDLINE_DT_BOOL), d_value(defaultValue)
        {
        }

        std::optional<std::any> d_value;

        DataType dataType() const { return d_dataType; }

        void print(std::ostream &out, const DataType dataType) const;

        bool hasValue() const { return d_value.has_value(); }

        template <typename T> T value() const;
        std::string getString() const;
        int getInt() const;
        double getDouble() const;
        bool getBool() const;
    };

    struct TypeInfo {
        TypeInfo(const DataType dataType)
            : d_dataType(dataType), d_variable(nullptr)
        {
        }
        TypeInfo(std::string *var)
            : d_dataType(COMMANDLINE_DT_STRING), d_variable(var)
        {
        }
        TypeInfo(int *var) : d_dataType(COMMANDLINE_DT_INT), d_variable(var) {}
        TypeInfo(double *var)
            : d_dataType(COMMANDLINE_DT_DOUBLE), d_variable(var)
        {
        }
        TypeInfo(bool *var) : d_dataType(COMMANDLINE_DT_BOOL), d_variable(var)
        {
        }
        TypeInfo(Type::VectorOfString *var)
            : d_dataType(COMMANDLINE_DT_STRING_ARRAY), d_variable(var)
        {
        }
        TypeInfo(Type::VectorOfPairOfString *var)
            : d_dataType(COMMANDLINE_DT_STRING_PAIR_ARRAY), d_variable(var)
        {
        }

        DataType dataType() const { return d_dataType; }
        bool isBindable() const { return (d_variable != nullptr); }
        void *getBindable() const { return d_variable; }

        DataType d_dataType;
        void *d_variable;
    };

    struct ArgumentSpec {
        enum Occurrence { O_OPTIONAL, O_REQUIRED };
        enum Constraint { C_WITH_ARG, C_WITHOUT_ARG, C_WITH_REST_OF_ARGS };

        ArgumentSpec(const std::string &name, const std::string &desc,
                     const TypeInfo typeInfo,
                     const Occurrence occurrence = O_OPTIONAL,
                     const Constraint constraint = C_WITHOUT_ARG,
                     const DefaultValue value = DefaultValue())
            : d_name(name), d_desc(desc), d_typeInfo(typeInfo),
              d_occurrence(occurrence), d_constraint(constraint),
              d_defaultValue(value)
        {
            if (d_constraint == C_WITH_REST_OF_ARGS) {
                if (d_typeInfo.dataType() !=
                    DataType::COMMANDLINE_DT_STRING_ARRAY) {
                    std::stringstream error;
                    // TODO prettify string representation of enums
                    error << "Argument [" << d_name << "] has constraint:["
                          << "C_WITH_REST_OF_ARGS"
                          << "] and requires data type:["
                          << "COMMANDLINE_DT_STRING_ARRAY"
                          << "]. Current type [" << d_typeInfo.dataType()
                          << "] is incompatible." << std::endl;
                    BUILDBOXCOMMON_THROW_EXCEPTION(std::logic_error,
                                                   error.str());
                }
            }
        }

        bool isOptional() const { return d_occurrence == O_OPTIONAL; }
        bool isRequired() const { return !isOptional(); }
        bool hasArgument() const
        {
            return d_constraint == C_WITH_ARG ||
                   d_constraint == C_WITH_REST_OF_ARGS;
        }
        bool absorbRestOfArgs() const
        {
            return d_constraint == C_WITH_REST_OF_ARGS;
        }
        bool isPositional() const { return d_name.empty(); }
        DataType dataType() const { return d_typeInfo.dataType(); }
        bool hasDefaultValue() const { return d_defaultValue.hasValue(); }
        const DefaultValue &defaultValue() const { return d_defaultValue; }

        std::string d_name;
        std::string d_desc;
        TypeInfo d_typeInfo;
        Occurrence d_occurrence;
        Constraint d_constraint;
        DefaultValue d_defaultValue;
    };

    typedef std::variant<std::string, int, double, bool,
                         CommandLineTypes::Type::VectorOfString,
                         CommandLineTypes::Type::VectorOfPairOfString>
        ArgumentValue;
};

std::ostream &operator<<(std::ostream &out,
                         const CommandLineTypes::TypeInfo &obj);
std::ostream &operator<<(std::ostream &out,
                         const CommandLineTypes::ArgumentSpec &obj);

} // namespace buildboxcommon

#endif
