//+============================================================================
//
// file :               tango_admin.cpp
//
// description :        C++ source code for the tango_admin utility
//						This utility is a Tango database command line interface
//						Obviously, not all the database features are interfaced
//						by this tool. Only the features needed for the Debian
//						packaging have been implemented. This means:
//						- ping the database server
//						- check if a device is defined in DB
//						- check if a server is defined in DB
//						- create a server in DB
//						- delete a server from the DB
//						- create a property in DB
//						- delete a property from DB
//
// project :            TANGO
//
// author(s) :          E.Taurel
//
// Copyright (C) :      2004,2005,2006,2007,2008,2009,2010
//						European Synchrotron Radiation Facility
//                      BP 220, Grenoble 38043
//                      FRANCE
//
// This file is part of Tango.
//
// Tango is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Tango 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Tango.  If not, see <http://www.gnu.org/licenses/>.
//
//-============================================================================

#include <iostream>
#include <string>
#include <sstream>
#include <limits>
#include <cmath>
#include <ctime>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <chrono>
#include <functional>
#include <thread>

#ifdef _WIN32
  #include <winsock2.h>
  #include <ws2tcpip.h>
  #pragma comment(lib, "Ws2_32.lib")
#else
  #include <sys/socket.h>
  #include <netdb.h>
  #include <unistd.h>
  #include <sys/types.h>
#endif

#if (TANGO_VERSION_MAJOR >= 10)
  #include <tango/client/DeviceProxy.h>
  #include <tango/client/Database.h>
#else
  #include <tango/tango.h>
#endif

#include "anyoption.h"

// TANGO_MAX_HOSTNAME_LEN is later used to specify the size of a buffer for a
// gethostname call. I define it here in the same way I do it in
// tango/internal/net.h
#if defined(MAXHOSTNAMELEN)
constexpr std::size_t TANGO_MAX_HOSTNAME_LEN{MAXHOSTNAMELEN + 1};
#else
// Windows needs 255 + 1.
// Let's assume that all other OSes also need 255 + 1.
constexpr std::size_t TANGO_MAX_HOSTNAME_LEN{256};
#endif

#ifdef _WIN32
static void setenv(const char *name, const char *value, const int default = 0)
{
    SetEnvironmentVariable(name, value);
}
#endif

// This is a workaround for cppTango versions 9.x.y < 9.4.0
// On cppTango versions before 9.4.0 this operator overload was not yet provided by cppTango
#if ((TANGO_VERSION_MAJOR == 9) && (TANGO_VERSION_MINOR < 4))
namespace Tango
{
static inline std::ostream &operator<<(std::ostream &ss, const Tango::DevFailed &devFailed)
{
    for(CORBA::ULong i = 0; i < devFailed.errors.length(); ++i)
    {
        const DevError &error = devFailed.errors[i];
        ss << "\tOrigin: " << error.origin << std::endl;
        ss << "\tSeverity: " << error.severity << std::endl;
        ss << "\tReason: " << error.reason << std::endl;
        ss << "\tDescription: " << error.desc << std::endl;
    }
    return ss;
}
} // namespace Tango
#endif

int ping_database();
int check_class(const std::string &);
int check_device(const char *);
int check_device_exported(const char *);
int check_device_ready(const std::string &);
int add_server(char *, char *, char *);
void list2vect(std::string &, std::vector<std::string> &);
int check_server(char *);
int server_list();
int server_instance_list(char *);
int delete_server(char *, bool);
int add_device_property(char *, char *, char *);
int delete_device_property(char *, char *);
int add_class_property(char *, char *, char *);
int delete_class_property(char *, char *);
int add_free_property(char *, char *, char *);
int delete_free_property(char *, char *);
int ping_network(bool);
int tac_enabled(void);
int ping_device(const std::string &);
int unexport_device(const char *);

// The min, max and default in seconds for timeouts. Used by the function
// compute_loops_and_sleep_time.
static constexpr double MIN_TIMEOUT{0.001};
static constexpr double MAX_TIMEOUT{std::numeric_limits<unsigned int>::max() * 0.5};
static constexpr double DEFAULT_TIMEOUT{4.0};

static void printUsageAndExit(const std::unique_ptr<AnyOption> &opt)
{
    opt->printUsage();
    std::exit(2);
}

// method: compute_loops_and_sleep_time
//
// descritpion: Compute the number of loops and the sleep time between loop
//              iterations.
// input parameters:
//  - timeout_in_s: Total duration in seconds for all loop iterations combined.
// output parameters:
//  - number_of_loops: Number of times a loop needs to run to reach a total
// run time duration of timeout_in_s with sleeps of sleep_time seconds between
// iteratons.
// - sleep_time: Time to sleep with nanosleep between loop iterations.
// return:  0 on success
//          2 if the timeout_in_s parameter does not fall within the expected
//          range.
static int compute_loops_and_sleep_time(double timeout_in_s,
                                        unsigned int &number_of_loops,
                                        std::chrono::nanoseconds &sleep_time)
{
    if(timeout_in_s > MAX_TIMEOUT)
    {
        std::cerr << "Error: The given timeout (" << timeout_in_s << ") needs to be lower than " << MAX_TIMEOUT
                  << ".\n";
        return 2;
    }

    if(timeout_in_s <= 0.01)
    {
        number_of_loops = 2;
        sleep_time = std::chrono::nanoseconds{static_cast<int64_t>(timeout_in_s / number_of_loops * 1e9)};
    }
    else if(timeout_in_s <= 0.1)
    {
        number_of_loops = 20;
        sleep_time = std::chrono::nanoseconds{static_cast<int64_t>(timeout_in_s / number_of_loops * 1e9)};
    }
    else if(timeout_in_s <= 1.0)
    {
        number_of_loops = 200;
        sleep_time = std::chrono::nanoseconds{static_cast<int64_t>(timeout_in_s / number_of_loops * 1e9)};
    }
    else
    {
        sleep_time = std::chrono::nanoseconds{static_cast<int64_t>(0.5e9)};
        number_of_loops = timeout_in_s / 0.5;
    }

    return 0;
}

// method: convert_string_to_double
//
// descritpion: Convert a std::string to a double. The input is checked to be valid.
// input parameters:
//  - value_string: String representation of a double
// output parameters:
//  - value: The value represented by a double
// return:  0 on success
//          2 if the timeout_in_s parameter does not fall within the expected
//          range.
static int convert_string_to_double(const std::string &value_as_string, double &value)
{
    try
    {
        auto converted_value{std::stod(value_as_string)};
        if(!std::isnan(converted_value))
        {
            // The value is within range, everything is OK.
            value = converted_value;
        }
        else
        {
            std::cerr << "The parameter's value (" << value_as_string << ") cannot be \"Not a Number\" (NaN).\n";
            return 2;
        }
    }
    catch(std::invalid_argument &ex)
    {
        std::cerr << "The parameter's value (" << value_as_string << ") cannot be converted to a numerical value.\n";
        return 2;
    }
    catch(std::out_of_range &ex)
    {
        std::cerr << "The parameter's value (" << value_as_string
                  << ") cannot be converted to a numerical value that lies within "
                     "the value range of the used data type (double).\n";
        return 2;
    }

    return 0;
}

// method: call_func
//
// description: Generic wrapper for a callable. The wrapper runs for a specified
//               duration with computed pauses that are based on the duration.
// input parameters:
//  - callable: Function to be called.
//  - timeout_in_s: A timeout or duration which limits how long this function
//                  runs. The value also determines how often the callable is
//                  executed:
//                  - timeout_in_s < 0.0: run forever until success
//                  - 0.0 <= timeout_in_s < MIN_TIMEOUT: Run once
//                  - MIN_TIMEOUT <= timeout_in_s < MAX_TIMEOUT: Run a number of
//                    times. How often is calculated in the function
//                    compute_loops_and_sleep_time
// return:  0 if the callable returned 0 itself which means success.
//          1 if the callable did not finish successfully.
//          2 if the timeout_in_s parameter does not fall within the
//          expected range.
static int call_func(std::function<int()> callable, double timeout_in_s)
{
    std::chrono::nanoseconds sleep_time{};
    unsigned int loops;
    bool run_forever{false};
    if(timeout_in_s >= MIN_TIMEOUT)
    {
        // Run loops number of times.
        auto err{compute_loops_and_sleep_time(timeout_in_s, loops, sleep_time)};
        if(err != 0)
        {
            return err;
        }
    }
    else if(timeout_in_s >= 0.0)
    {
        loops = 1U;
    }
    else
    {
        // In case the Tango DB check runs forever, set the time to wait between
        // retires to 0.5s.
        sleep_time = std::chrono::nanoseconds{static_cast<int64_t>(0.5e9)};
        loops = 1U;
        timeout_in_s = 1.0;
        run_forever = true;
        std::cerr << "Will try forever until successful.\n";
    }

    const double sleep_time_in_s = std::chrono::duration_cast<std::chrono::duration<double>>(sleep_time).count();
    int ret{};
    while((loops > 0U) && (timeout_in_s >= 0.0))
    {
        ret = callable();
        // Return on success
        // or if we were asked to run the function only a single time. Then
        // sleep_time has intentionally been set to 0LL above.
        if((ret == 0) || (timeout_in_s == 0LL))
        {
            return ret;
        }

        std::this_thread::sleep_for(sleep_time);

        if(!run_forever)
        {
            timeout_in_s -= sleep_time_in_s;
            --loops;
        }
    };

    std::cerr << "Running the command timed out!\n";
    return 1;
}

static void print_error(const Tango::DevFailed &e)
{
    static std::ostringstream previous_exception;
    std::ostringstream current_exception;
    current_exception << e;
    if(previous_exception.str() != current_exception.str())
    {
        previous_exception.str(current_exception.str());
        Tango::Except::print_exception(e);
    }

    // Bail out early if there is no chance that we can continue.
    // This is the case when API_TangoHostNotSet is raised. This means that the
    // user did not set TANGO_HOST. There is no point in trying again.
    if((std::strncmp(e.errors[0].reason, "API_TangoHostNotSet", sizeof(e.errors[0].reason)) == 0) ||
       ((e.errors.length() == 2) &&
        (std::strncmp(e.errors[1].reason, "API_TangoHostNotSet", sizeof(e.errors[1].reason)) == 0)))
    {
        std::cerr << "Please set TANGO_HOST!\n";
        std::exit(2);
    }
}

static void print_error(const Tango::DevFailed &e, const std::string &msg)
{
    static std::string previous_message;
    if((!msg.empty()) && (previous_message != msg))
    {
        previous_message = msg;
        std::cerr << msg << ":\n";
    }

    print_error(e);
}

int main(int argc, char *argv[])
{
    auto opt{std::make_unique<AnyOption>()};

    //
    // Add usage menu
    //
    opt->addUsage("Usage: ");
    opt->addUsage(" --add-class-property <class> <prop_name> <prop_value (comma separated for array)>    Add a class "
                  "property in DB");
    opt->addUsage(" --add-free-property <object> <prop_name> <prop_value (comma separated for array)>    Add a free "
                  "property in DB");
    opt->addUsage(
        " --add-property <dev> <prop_name> <prop_value (comma separated for array)>    Add a device property in DB");
    opt->addUsage(" --add-server <exec/inst> <class> <dev list (comma separated)>   Add a server in DB");
    opt->addUsage(" --check-class <class>    Check if the device class is defined in the DB");
    opt->addUsage(" --check-device <dev>    Check if the device is defined in DB");
    opt->addUsage(" --check-device-exported <dev>    Check if the device is exported in DB");
    opt->addUsage(" --check-device-ready <dev>    Check if the device is ready, i.e. if it replies to Tango commands.");
    opt->addUsage(" --check-server <exec/inst>   Check if a device server is defined in DB");
    opt->addUsage(" --delete-class-property <class> <prop_name>   Delete a class property from DB ");
    opt->addUsage(" --delete-free-property <object> <prop_name>   Delete a free property from DB ");
    opt->addUsage(" --delete-property <dev> <prop_name>   Delete a device property from DB ");
    opt->addUsage(" --delete-server <exec/inst> [--with-properties]   Delete a server from DB");
    opt->addUsage(" --help  		Prints this help ");
    std::array<char, 256> buffer1{};
    std::snprintf(buffer1.data(),
                  buffer1.size() - 1,
                  " --ping-database	[max_time (s), < %f for single shot, < 0.0 for forever] Ping database",
                  MIN_TIMEOUT);
    opt->addUsage(buffer1.data());
    std::array<char, 256> buffer2{};
    std::snprintf(
        buffer2.data(),
        buffer2.size() - 1,
        " --ping-device <dev> [max_time (s), < %f for single shot, < 0.0 for forever] Check if the device is running",
        MIN_TIMEOUT);
    opt->addUsage(buffer2.data());
    std::array<char, 256> buffer3{};
    std::snprintf(buffer3.data(),
                  buffer3.size() - 1,
                  " --ping-network [max_time (s), < %f for single shot, < 0.0 for forever] [-v] Ping network",
                  MIN_TIMEOUT);
    opt->addUsage(buffer3.data());
    opt->addUsage(" --server-instance-list <exec>   Display list of server instances for the given server name");
    opt->addUsage(" --server-list  Display list of server names");
    opt->addUsage(" --tac-enabled Check if the TAC (Tango Access Control) is enabled");
    opt->addUsage(" --unexport-device <dev> Unexport a device from DB.  USE WITH CARE.");

    //
    // Define the command line options
    //

    struct Flag
    {
        const char *long_name;
        char short_name;
    };

    struct Option
    {
        const char *name;
    };

    std::vector<Flag> cmd_flags = {
        Flag{"help", 'h'},
        Flag{"ping-database", '\0'},
        Flag{"server-list", '\0'},
        Flag{"ping-network", '\0'},
        Flag{"tac-enabled", '\0'},
    };

    std::vector<Flag> other_flags = {
        Flag{"with-properties", '\0'},
    };

    std::vector<Option> commands = {
        Option{"add-server"},
        Option{"delete-server"},
        Option{"add-property"},
        Option{"delete-property"},
        Option{"add-class-property"},
        Option{"delete-class-property"},
        Option{"add-free-property"},
        Option{"delete-free-property"},
        Option{"check-class"},
        Option{"check-device"},
        Option{"check-device-exported"},
        Option{"check-device-ready"},
        Option{"check-server"},
        Option{"server-instance-list"},
        Option{"ping-device"},
        Option{"unexport-device"},
    };

    for(auto &f : cmd_flags)
    {
        if(f.short_name != '\0')
        {
            opt->setFlag(f.long_name, f.short_name);
        }
        else
        {
            opt->setFlag(f.long_name);
        }
    }

    for(auto &f : other_flags)
    {
        if(f.short_name != '\0')
        {
            opt->setFlag(f.long_name, f.short_name);
        }
        else
        {
            opt->setFlag(f.long_name);
        }
    }

    for(auto &o : commands)
    {
        opt->setOption(o.name);
    }

    //
    // Process cmd line
    //

    opt->processCommandArgs(argc, argv);

    int cmd_count = 0;
    for(auto &f : cmd_flags)
    {
        if(opt->getFlag(f.long_name) || (f.short_name != '\0' && opt->getFlag(f.short_name)))
        {
            cmd_count += 1;
        }
    }

    for(auto &o : commands)
    {
        if(opt->getValue(o.name) != nullptr)
        {
            cmd_count += 1;
        }
    }

    if(cmd_count != 1)
    {
        if(cmd_count > 1)
        {
            std::cerr << "Can't mix commands! Exiting.\n";
        }
        else if(argc > 1)
        {
            std::cerr << "The option \"" << argv[1] << "\" requires (at least) one parameter! Exiting.\n";
        }

        printUsageAndExit(opt);
    }

    //
    // --help option
    //
    if(opt->getFlag("help") || opt->getFlag('h'))
    {
        opt->printUsage();
        return 0;
    }

    //
    // --ping-database option
    //
    if(opt->getFlag("ping-database") == true)
    {
        auto timeout{DEFAULT_TIMEOUT};
        switch(argc)
        {
        case 3:
        {
            if(convert_string_to_double(argv[2], timeout) != 0)
            {
                std::cerr << "Bad argument for option --ping-database\n";
                printUsageAndExit(opt);
            }

            break;
        }
        case 2:
        {
            // Use the default value for timeout.
            break;
        }
        default:
        {
            std::cerr << "Bad argument for option --ping-database\n";
            printUsageAndExit(opt);
            break;
        }
        }

        setenv("SUPER_TANGO", "true", 1);

        return call_func(ping_database, timeout);
    }

    //
    // --check-class option
    //
    else if(opt->getValue("check-class") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --check-class" << std::endl;
            printUsageAndExit(opt);
        }

        return check_class(opt->getValue("check-class"));
    }

    //
    // --check-device option
    //
    else if(opt->getValue("check-device") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --check-device" << std::endl;
            printUsageAndExit(opt);
        }

        return check_device(opt->getValue("check-device"));
    }

    //
    // --check-device-exported option
    //
    else if(opt->getValue("check-device-exported") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --check-device-exported" << std::endl;
            printUsageAndExit(opt);
        }

        return check_device_exported(opt->getValue("check-device-exported"));
    }

    //
    // --check-device-ready option
    //
    else if(opt->getValue("check-device-ready") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --check-device-ready" << std::endl;
            printUsageAndExit(opt);
        }

        return check_device_ready(opt->getValue("check-device-ready"));
    }

    //
    // --add-server option
    //

    else if(opt->getValue("add-server") != nullptr)
    {
        if(argc != 5)
        {
            std::cerr << "Bad argument number for option --add-server" << std::endl;
            printUsageAndExit(opt);
        }

        return add_server(opt->getValue("add-server"), opt->getArgv(0), opt->getArgv(1));
    }

    //
    // --check-server option
    //

    else if(opt->getValue("check-server") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --check_server" << std::endl;
            printUsageAndExit(opt);
        }

        return check_server(opt->getValue("check-server"));
    }

    //
    // --server-list option
    //

    else if(opt->getFlag("server-list") == true)
    {
        if(argc != 2)
        {
            std::cerr << "Bad argument number for option --server-list" << std::endl;
            printUsageAndExit(opt);
        }

        return server_list();
    }

    //
    // --server-instance-list option
    //

    else if(opt->getValue("server-instance-list") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --server-instance-list" << std::endl;
            printUsageAndExit(opt);
        }

        return server_instance_list(opt->getValue("server-instance-list"));
    }

    //
    // --delete-server option
    //

    else if(opt->getValue("delete-server") != nullptr)
    {
        if((argc < 3 || argc > 4) || (argc == 3 && strcmp(argv[2], "--with-properties") == 0) ||
           (strcmp(opt->getValue("delete-server"), "--with-properties") == 0))
        {
            std::cerr << "Bad option delete-server usage" << std::endl;
            printUsageAndExit(opt);
        }

        if(opt->getFlag("with-properties") == true)
        {
            return delete_server(opt->getValue("delete-server"), true);
        }
        else
        {
            return delete_server(opt->getValue("delete-server"), false);
        }
    }

    //
    // --add-property option
    //

    else if(opt->getValue("add-property") != nullptr)
    {
        if(argc != 5)
        {
            std::cerr << "Bad argument number for option --add-property" << std::endl;
            printUsageAndExit(opt);
        }

        return add_device_property(opt->getValue("add-property"), opt->getArgv(0), opt->getArgv(1));
    }

    //
    // --delete-property option
    //

    else if(opt->getValue("delete-property") != nullptr)
    {
        if(argc != 4)
        {
            std::cerr << "Bad argument number for option --delete-property" << std::endl;
            printUsageAndExit(opt);
        }

        return delete_device_property(opt->getValue("delete-property"), opt->getArgv(0));
    }

    // --add-class-property option
    else if(opt->getValue("add-class-property") != nullptr)
    {
        if(argc != 5)
        {
            std::cerr << "Bad argument number for option --add-class-property" << std::endl;
            printUsageAndExit(opt);
        }

        return add_class_property(opt->getValue("add-class-property"), opt->getArgv(0), opt->getArgv(1));
    }

    // --delete-class-property option
    else if(opt->getValue("delete-class-property") != nullptr)
    {
        if(argc != 4)
        {
            std::cerr << "Bad argument number for option --delete-class-property" << std::endl;
            printUsageAndExit(opt);
        }

        return delete_class_property(opt->getValue("delete-class-property"), opt->getArgv(0));
    }

    // --add-free-property option
    else if(opt->getValue("add-free-property") != nullptr)
    {
        if(argc != 5)
        {
            std::cerr << "Bad argument number for option --add-free-property" << std::endl;
            printUsageAndExit(opt);
        }

        return add_free_property(opt->getValue("add-free-property"), opt->getArgv(0), opt->getArgv(1));
    }

    // --delete-free-property option
    else if(opt->getValue("delete-free-property") != nullptr)
    {
        if(argc != 4)
        {
            std::cerr << "Bad argument number for option --delete-free-property" << std::endl;
            printUsageAndExit(opt);
        }

        return delete_free_property(opt->getValue("delete-free-property"), opt->getArgv(0));
    }

    //
    // --ping-network option
    //
    if(opt->getFlag("ping-network") == true)
    {
        auto verbose{false};
        auto timeout{DEFAULT_TIMEOUT};
        switch(argc)
        {
        case 4:
        {
            if(convert_string_to_double(argv[2], timeout) != 0)
            {
                std::cerr << "Bad argument for option --ping-network\n";
                printUsageAndExit(opt);
            }

            if(strncmp(argv[3], "-v", 2) == 0)
            {
                verbose = true;
            }
            else
            {
                std::cerr << "Bad argument for option --ping-network\n";
                printUsageAndExit(opt);
            }

            break;
        }
        case 3:
        {
            // Keep the order of the checks, first -v and then numerical
            // value, because convert_string_to_double will print an
            // explanation why a conversion failed.
            // By first checking for -v and then for a numeric param the
            // explanation becomes part of the error message that one of
            // the args was wrong.
            if(strncmp(argv[2], "-v", 2) == 0)
            {
                verbose = true;
            }
            else if(convert_string_to_double(argv[2], timeout) != 0)
            {
                std::cerr << "Bad argument for option --ping-network\n";
                printUsageAndExit(opt);
            }

            break;
        }
        case 2:
        {
            // Use the default values for verbose and timeout.
            break;
        }
        default:
        {
            std::cerr << "Bad argument number for option --ping-network\n";
            printUsageAndExit(opt);
            break;
        }
        }

        auto func{std::bind(ping_network, verbose)};
        return call_func(func, timeout);
    }

    //
    // --tac-enabled option
    //

    if(opt->getFlag("tac-enabled") == true)
    {
        if(argc > 2)
        {
            std::cerr << "Bad argument number for option --tac-enabled" << std::endl;
            printUsageAndExit(opt);
        }

        return tac_enabled();
    }

    //
    // --ping-device option
    //
    if(opt->getValue("ping-device") != nullptr)
    {
        auto timeout{DEFAULT_TIMEOUT};
        switch(argc)
        {
        case 4:
        {
            if(convert_string_to_double(argv[3], timeout) != 0)
            {
                std::cerr << "Bad argument for option --ping-device\n";
                printUsageAndExit(opt);
            }
            break;
        }
        case 3:
        {
            break;
        }
        default:
        {
            std::cerr << "Bad argument number for option --ping-device\n";
            printUsageAndExit(opt);
            break;
        }
        }

        auto func{std::bind(ping_device, opt->getValue("ping-device"))};
        return call_func(func, timeout);
    }

    //
    // --unexport-device option
    //

    if(opt->getValue("unexport-device") != nullptr)
    {
        if(argc != 3)
        {
            std::cerr << "Bad argument number for option --unexport-device\n";
            printUsageAndExit(opt);
        }

        return unexport_device(opt->getValue("unexport-device"));
    }

    return 2;
}

//+-------------------------------------------------------------------------
//
// method : 		ping_database
//
// description : 	This function connect to the database and executes
//					one of its command in order to check the database
//					connectivity.
//
// return:  0 on success
//          1 on failure
//
//--------------------------------------------------------------------------
int ping_database()
{
    try
    {
        Tango::Database db;
        const std::string db_info{db.get_info()};
        return 0;
    }
    catch(const Tango::DevFailed &e)
    {
        // Print the exception and try again if the exception is not
        // API_TangoHostNotSet (handled on print_error).
        print_error(e);
    }

    return 1;
}

//+-------------------------------------------------------------------------
// method : 		check_class
// description : 	This function checks if a device class is defined in the DB
// argument : in : 	- name : The class name
// return:  0 if the class is defined
//          1 otherwise
//--------------------------------------------------------------------------
int check_class(const std::string &class_name)
{
    Tango::Database db;
    // workaround 9.3 non-const reference constructor
    std::string class_name_local{class_name};
    Tango::DbDatum dbd{db.get_class_list(class_name_local)};
    if(!dbd.is_empty())
    {
        return 0;
    }

    return 1;
}

//+-------------------------------------------------------------------------
//
// method : 		check_device
//
// description : 	This function checks if a device is defined in the DB
//
// argument : in : 	- name : The device name
//
// return:  0 if the device is defined
//          1 otherwise
//
//--------------------------------------------------------------------------

int check_device(const char *name)
{
    try
    {
        Tango::Database db;

        std::string d_name(name);
        Tango::DbDevImportInfo dii = db.import_device(d_name);
    }
    catch(const Tango::DevFailed &e)
    {
        const char *dev_not_defined{"DB_DeviceNotDefined"};
        if(std::strncmp(e.errors[0].reason.in(), dev_not_defined, std::strlen(dev_not_defined)) != 0)
        {
            print_error(e, "Database query failed");
            return 1;
        }

        print_error(e);
        return 1;
    }

    return 0;
}

/**
 * @fn check_device_ready
 *
 * @brief Check if a Tango device is ready.
 *
 * @retval 0 The device is ready.
 * @retval 1 Tango error
 *
 * @param trl Tango Resource Locator of the device
 */
int check_device_ready(const std::string &trl)
{
    try
    {
        Tango::DeviceProxy dev(trl.c_str());
        dev.ping();
        dev.command_inout("status");
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		check_device_exported
//
// description : 	This function checks if a device is exported in the DB
//
// argument : in : 	- name : The device name
//
// return:  0 on success
//          1 otherwise
//
//--------------------------------------------------------------------------

int check_device_exported(const char *name)
{
    std::string d_name(name);
    try
    {
        Tango::Database db;
        Tango::DbDevImportInfo dii = db.import_device(d_name);

        if(dii.exported == 1)
        {
            return 0;
        }
        else
        {
            return 1;
        }
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e, "Database query failed");
        return 1;
    }
}

//+-------------------------------------------------------------------------
//
// method : 		add_server
//
// description : 	This function adds a server definition in the DB
//
// argument : in : 	- d_name : The device server name (exec/inst)
//					- c_name : The class name
//					- d_list : The device list
//
// return:  0 on success
//          1 otherwise
//
//--------------------------------------------------------------------------

int add_server(char *d_name, char *c_name, char *d_list)
{
    //
    // Check ds name syntax
    //

    std::string ds_name(d_name);
    std::string::size_type pos;

    pos = ds_name.find('/');
    if((count(ds_name.begin(), ds_name.end(), '/') != 1) || pos == 0 || pos == (ds_name.size() - 1))
    {
        std::cerr << "Wrong syntax for ds name\n";
        return 2;
    }

    //
    // Check class name syntax
    //

    std::string class_name(c_name);
    if(count(class_name.begin(), class_name.end(), '/') != 0)
    {
        std::cerr << "Wrong syntax for class name\n";
        return 2;
    }

    //
    // Check device list and device syntax
    //

    std::string dev_list(d_list);
    std::vector<std::string> dev_names;

    list2vect(dev_list, dev_names);

    for(unsigned int loop = 0; loop < dev_names.size(); ++loop)
    {
        if(count(dev_names[loop].begin(), dev_names[loop].end(), '/') != 2)
        {
            std::cerr << "Wrong syntax for device " << dev_names[loop] << "\n";
            return 2;
        }

        std::string::size_type pos1, pos2;
        pos1 = dev_names[loop].find('/');
        pos2 = dev_names[loop].rfind('/');

        if(pos1 == 0 || pos2 == dev_names[loop].length() - 1 || pos2 == pos1 + 1)
        {
            std::cerr << "Wrong syntax for device " << dev_names[loop] << "\n";
            return 2;
        }
    }

    //
    // Create server in DB
    // Dont forget to add the admin device
    //
    setenv("SUPER_TANGO", "true", 1);

    try
    {
        Tango::Database db;

        Tango::DbDevInfos ddi;
        Tango::DbDevInfo tmp_dbi;

        for(unsigned int loop = 0; loop < dev_names.size(); ++loop)
        {
            tmp_dbi.name = dev_names[loop];
            tmp_dbi._class = class_name;
            tmp_dbi.server = ds_name;
            ddi.push_back(tmp_dbi);
        }
        tmp_dbi.name = "dserver/" + ds_name;
        tmp_dbi._class = "DServer";
        tmp_dbi.server = ds_name;

        ddi.push_back(tmp_dbi);

        db.add_server(ds_name, ddi);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		check_server
//
// description : 	This function checks if a device server is defined in the DB
//
// argument : in : 	- d_name : The device server name
//
// The function returns 0 is the device is defined. Otherwise, it returns 1
//
//--------------------------------------------------------------------------

int check_server(char *d_name)
{
    std::string dev_name = "dserver/";
    std::string ds_name = d_name;

    dev_name = dev_name + ds_name;

    return check_device((char *) dev_name.c_str());
}

//+-------------------------------------------------------------------------
//
// method : 		server_list
//
// description : 	This function lists all server names
//
// The function returns 0 if at least one server is defined. Otherwise returns 1
//
//--------------------------------------------------------------------------

int server_list()
{
    try
    {
        Tango::Database db;

        std::vector<std::string> servers;
        db.get_server_name_list() >> servers;
        for(size_t idx = 0; idx < servers.size(); ++idx)
        {
            std::cout << servers[idx] << " ";
        }
        std::cout << std::endl;
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		server_instance_list
//
// description : 	This function lists all server instances for the given
//                      server name
//
// argument : in : 	- s_name : The server name
//
// The function returns 0 is the server name is defined. Otherwise returns 1
//
//--------------------------------------------------------------------------

int server_instance_list(char *s_name)
{
    try
    {
        Tango::Database db;

        std::string server_name = s_name;
        size_t start_pos = server_name.size() + 1;
        server_name += "/*";

        std::vector<std::string> servers;
        db.get_server_list(server_name) >> servers;
        if(!servers.empty())
        {
            for(size_t idx = 0; idx < servers.size(); ++idx)
            {
                std::cout << servers[idx].substr(start_pos) << " ";
            }
            std::cout << "\n";
        }
        else
        {
            std::cerr << "No server with a name that starts with \"" << s_name << "\" is defined in TangoDB.\n";
            return 1;
        }
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		delete_server
//
// description : 	This function deletes a device server from the DB
//
// argument : in : 	- d_name : The device server name
//					- with_res : If true, also delte device properties
//
// The function returns 0 is everything is fine. Otherwise, it returns 1
//
//--------------------------------------------------------------------------

int delete_server(char *d_name, bool with_res)
{
    std::string ds_name(d_name);

    //
    // Check device server name syntax
    //

    std::string::size_type pos;
    pos = ds_name.find('/');

    if(pos == 0 || pos == ds_name.size() - 1 || count(ds_name.begin(), ds_name.end(), '/') != 1)
    {
        return 1;
    }

    int ret = check_server(d_name);
    if(ret != 0)
    {
        return ret;
    }

    try
    {
        Tango::Database db;

        //
        // If we need to remove prop
        //

        if(with_res == true)
        {
            //
            //	First get the ds class list
            //

            Tango::DbDatum db_res = db.get_device_class_list(ds_name);
            std::vector<std::string> dev_list;
            db_res >> dev_list;

            //
            // Get device property name for each device
            //

            for(unsigned int loop = 0; loop < dev_list.size(); ++loop)
            {
                std::vector<std::string> prop_list;

                db.get_device_property_list(dev_list[loop], "*", prop_list);

                //
                // Delete all device properties
                //

                if(prop_list.empty() == false)
                {
                    Tango::DbData dbd;

                    for(unsigned int ctr = 0; ctr < prop_list.size(); ++ctr)
                    {
                        dbd.push_back(Tango::DbDatum(prop_list[ctr]));
                    }

                    db.delete_device_property(dev_list[loop], dbd);
                }

                ++loop;
            }
        }

        //
        // Delete device server from db
        //

        db.delete_server(ds_name);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		add_device_property
//
// description : 	This function adds a device property in the DB
//
// argument : in : 	- d_name : The device name
//					- p_name : The property name
//					- p_val : The property value
//
// The function returns 0 is everything is fine, 1 in case of a Tango error and
// 2 when there is an error caused by the user.
//
//--------------------------------------------------------------------------

int add_device_property(char *d_name, char *p_name, char *p_val)
{
    //
    // Check dev name syntax
    //

    std::string dev_name(d_name);
    std::string::size_type pos1, pos2;

    pos1 = dev_name.find('/');
    pos2 = dev_name.rfind('/');

    if((count(dev_name.begin(), dev_name.end(), '/') != 2) || pos1 == 0 || pos2 == (dev_name.size() - 1) ||
       pos2 == pos1 + 1)
    {
        std::cerr << "Wrong syntax for device name" << std::endl;
        return 2;
    }

    //
    // Check if the device is defined
    //

    if(check_device(d_name) != 0)
    {
        return 1;
    }

    //
    // Convert prop value(s) into a vector
    //

    std::string prop_val(p_val);
    std::vector<std::string> prop_val_list;

    list2vect(prop_val, prop_val_list);

    //
    // Create server in DB
    // Dont forget to add the admin device
    //

    try
    {
        Tango::Database db;

        Tango::DbData dbd;
        Tango::DbDatum db_s(p_name);

        db_s << prop_val_list;
        dbd.push_back(db_s);

        db.put_device_property(dev_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		delete_device_property
//
// description : 	This function deletes a device property from the DB
//
// argument : in : 	- d_name : The device name
//					- p_name : The property name
//
// The function returns 0 is everything is fine, 1 in case of a Tango error and
// 2 when there is an error caused by the user.
//
//--------------------------------------------------------------------------

int delete_device_property(char *d_name, char *p_name)
{
    //
    // Check dev name syntax
    //

    std::string dev_name(d_name);
    std::string::size_type pos1, pos2;

    pos1 = dev_name.find('/');
    pos2 = dev_name.rfind('/');

    if((count(dev_name.begin(), dev_name.end(), '/') != 2) || pos1 == 0 || pos2 == (dev_name.size() - 1) ||
       pos2 == pos1 + 1)
    {
        std::cerr << "Wrong syntax for device name" << std::endl;
        return 2;
    }

    //
    // Check if the device is defined
    //

    if(check_device(d_name) != 0)
    {
        return 1;
    }

    //
    // Create server in DB
    // Dont forget to add the admin device
    //

    try
    {
        Tango::Database db;

        Tango::DbData dbd;
        dbd.push_back(Tango::DbDatum(p_name));

        db.delete_device_property(dev_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		add_class_property
//
// description : 	This function adds a class property in the DB
//
// argument : in : 	- c_name : The class name
//					- p_name : The property name
//					- p_val : The property value
//
// The function returns 0 if everything is fine, 1 in case of a Tango error.
//
//--------------------------------------------------------------------------
int add_class_property(char *c_name, char *p_name, char *p_val)
{
    // Check if the class is defined
    const std::string class_name{c_name};
    if(check_class(class_name) != 0)
    {
        return 1;
    }

    // Convert prop value(s) to a vector
    std::string prop_val(p_val);
    std::vector<std::string> prop_val_list;
    list2vect(prop_val, prop_val_list);

    try
    {
        // Create a DB object.
        Tango::Database db;
        // Now create a DB data object that contains just one entry for the
        // property value.
        Tango::DbData dbd;
        Tango::DbDatum db_s(p_name);
        db_s << prop_val_list;
        dbd.push_back(db_s);

        // Send the property value to the DB.
        db.put_class_property(class_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		delete_class_property
//
// description : 	This function deletes a class property from the DB
//
// argument : in : 	- c_name : The class name
//					- p_name : The property name
//
// The function returns 0 if everything is fine, 1 in case of a Tango error.
//
//--------------------------------------------------------------------------
int delete_class_property(char *c_name, char *p_name)
{
    // Check if the class is defined
    const std::string class_name{c_name};
    if(check_class(class_name) != 0)
    {
        return 1;
    }

    try
    {
        // Create a DB object.
        Tango::Database db;
        // Now create a DB data object that contains just the class name.
        // Leave the value empty.
        Tango::DbData dbd;
        dbd.push_back(Tango::DbDatum(p_name));

        // This will delete the class property in the DB.
        db.delete_class_property(class_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		add_free_property
//
// description : 	This function adds a free property in the DB
//
// argument : in : 	- o_name : The object name
//					- p_name : The property name
//					- p_val : The property value
//
// The function returns 0 if everything is fine, 1 in case of a Tango error.
//
//--------------------------------------------------------------------------
int add_free_property(char *o_name, char *p_name, char *p_val)
{
    // Convert prop value(s) to a vector
    std::string prop_val(p_val);
    std::vector<std::string> prop_val_list;
    list2vect(prop_val, prop_val_list);

    try
    {
        // Create a DB object.
        Tango::Database db;
        // Now create a DB data object that contains just one entry for the
        // property value.
        Tango::DbData dbd;
        Tango::DbDatum db_s(p_name);
        db_s << prop_val_list;
        dbd.push_back(db_s);

        // Send the property value to the DB.
        db.put_property(o_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		delete_free_property
//
// description : 	This function deletes a free property from the DB
//
// argument : in : 	- o_name : The object name
//					- p_name : The property name
//
// The function returns 0 if everything is fine, 1 in case of a Tango error.
//
//--------------------------------------------------------------------------
int delete_free_property(char *o_name, char *p_name)
{
    try
    {
        // Create a DB object.
        Tango::Database db;
        // Now create a DB data object that contains just the property name.
        // Leave the value empty.
        Tango::DbData dbd;
        dbd.push_back(Tango::DbDatum(p_name));

        // This will delete the free property in the DB.
        db.delete_property(o_name, dbd);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		list2vect
//
// description : 	This function converts a comma separated
//					device list into a vector of strings with one
//					element for each device
//
// argument : in : 	- dev_list : The device list
//					- dev_names : The device vector
//
//--------------------------------------------------------------------------

void list2vect(std::string &dev_list, std::vector<std::string> &dev_names)
{
    std::string::size_type beg, end;

    bool end_loop = false;
    beg = 0;

    while(end_loop == false)
    {
        end = dev_list.find(',', beg);
        if(end == beg)
        {
            ++beg;
            continue;
        }

        if(end == std::string::npos)
        {
            end = dev_list.length();
            end_loop = true;
        }

        std::string one_dev;
        one_dev = dev_list.substr(beg, end - beg);
        dev_names.push_back(one_dev);

        beg = end + 1;
        if(beg == dev_list.size())
        {
            end_loop = true;
        }
    }
}

//+-------------------------------------------------------------------------
//
// method : 		ping_network
//
// description : 	This function checks the network avaibility.
//
// Parameter
//      verbose: Boolean flag set to true if some printing is required
//
// The function returns 0 on success, 1 otherwise.
//--------------------------------------------------------------------------
int ping_network(bool verbose)
{
#ifdef _WIN32
    WSADATA wsaData;
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        if(verbose)
        {
            std::cerr << "WSAStartup failed\n";
        }
        return 1;
    }
#endif

    // Get hostname
    char buffer[TANGO_MAX_HOSTNAME_LEN]{};
    if(gethostname(buffer, TANGO_MAX_HOSTNAME_LEN) != 0)
    {
        if(verbose)
        {
            std::cerr << "Unable to retrieve the host name\n";
        }
        return 1;
    }

    // Make certin that the hostname is 0x00 terminated in case gethostname
    // returns a host name that fills the entire buffer.
    buffer[TANGO_MAX_HOSTNAME_LEN - 1] = 0x0;

    const std::string hostname{buffer};
    if(verbose)
    {
        std::cout << "Host name returned by gethostname function = \"" << hostname << "\"\n";
    }

    // Support both IPv4 and IPv6
    struct addrinfo hints{};
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    struct addrinfo *info{nullptr};
    int result{getaddrinfo(buffer, nullptr, &hints, &info)};

    if(result != 0)
    {
        if(verbose)
        {
            std::cout << "getaddrinfo() failed, returned value = " << gai_strerror(result) << "\n";
        }

#ifdef _WIN32
        WSACleanup();
#endif

        return 1;
    }

    struct addrinfo *ptr = info;
    bool found_valid_address = false;

    while(ptr != nullptr)
    {
        char tmp_host[512]{};

        if(getnameinfo(ptr->ai_addr, ptr->ai_addrlen, tmp_host, sizeof(tmp_host), nullptr, 0, NI_NAMEREQD) == 0)
        {
            if(verbose)
            {
                std::cout << "Host name returned by getnameinfo call = " << tmp_host << "\n";
            }

            found_valid_address = true;
        }
        else if(verbose)
        {
            std::cout << "getnameinfo() failed\n";
        }

        ptr = ptr->ai_next;
    }

    freeaddrinfo(info);

#ifdef _WIN32
    WSACleanup();
#endif

    if(!found_valid_address)
    {
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		tac_enabled
//
// description : 	This function check in DB if the TAC is enabled
//
// The function returns 0 if the TAC is disabled. Otherwise, it returns 1
//
//--------------------------------------------------------------------------

int tac_enabled(void)
{
    setenv("SUPER_TANGO", "true", 1);

    try
    {
        Tango::Database db;

        std::string servicename("AccessControl");
        std::string instname("tango");
        Tango::DbDatum db_datum = db.get_services(servicename, instname);
        std::vector<std::string> service_list;
        db_datum >> service_list;

        return !service_list.empty();
    }
    catch(const Tango::DevFailed &e)
    {
        // NOTE:
        // Here we do not print the exception here because we expected it.
        return 0;
    }

    return 1;
}

//+-------------------------------------------------------------------------
//
// method : 		ping_device
//
// description : 	This function checks a device's avaibility
//
// Parameter dev_name: The device name
//
// return:  0 on sucess, 1 otherwise.
//
//--------------------------------------------------------------------------
int ping_device(const std::string &dev_name)
{
    try
    {
        Tango::DeviceProxy dev(dev_name.c_str());
        dev.ping();
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e);
        return 1;
    }

    return 0;
}

//+-------------------------------------------------------------------------
//
// method : 		unexport_device
//
// description : 	Unexport a device from the database
//
// Parameter dev_name: The device name
//
// return:  0 on success, 1 otherwise.
//
//--------------------------------------------------------------------------
int unexport_device(const char *dev_name)
{
    int ret = check_device(dev_name);
    if(ret != 0)
    {
        if(ret == 1)
        {
            std::cerr << "Device \"" << dev_name << "\" not defined in the database. Cannot unexport.\n";
        }
        return 1;
    }

    try
    {
        Tango::Database db;

        db.unexport_device(dev_name);
    }
    catch(const Tango::DevFailed &e)
    {
        print_error(e, "Unexport device failed");
        return 1;
    }

    return 0;
}
