//+=============================================================================
//
// file :         StarterUtil.cpp
//
// description :  C++ source for tools used by the Starter device server.
//
// project :      TANGO Device Server
//
// $Author$
//
// Copyright (C) :      2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015
//						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/>.
//
// $Revision$
// $Date$
//
//-=============================================================================

#include <tango/tango.h>

#include <fcntl.h>
#include <sys/stat.h>

#ifndef _TG_WINDOWS_

#	include <sys/time.h>

#endif


#include "StarterUtil.h"

#include "Logging.h"

namespace Starter_ns {

    int StarterUtil::elapsed;
#ifndef _TG_WINDOWS_
    struct timeval    StarterUtil::before, StarterUtil::after;
#else
#endif /* _TG_WINDOWS_ */

//+------------------------------------------------------------------
/**
 *	Contructor - Initialize data members.
 */
//+------------------------------------------------------------------
    StarterUtil::StarterUtil(Tango::DeviceProxy *database,
            const std::vector<std::string>& host_names, const std::string& logHome) {
        dbase = database;
        log_home = logHome;

        //	Remove the Fully Qualify Domain Name for tango less than 5.2 compatibility
        for (const auto & host_name : host_names)
            hostNames.emplace_back(removeFQDN(host_name));

        proc_util = new CheckProcessUtil();
        proc_util->start();
        //	Wait a bit to be sure
        //	that running process list is updated.
#ifdef _TG_WINDOWS_
        _sleep(1000);
#else
        sleep(1);
#endif

        //	Build starter log file name.
        LogPath(starter_log_file, logHome)
        starter_log_file += "/Starter.log";

        LogPath(starter_stat_file, logHome)
        starter_stat_file += "/Starter.stat";

        TANGO_LOG_INFO << "---->  starter_log_file  = " << starter_log_file << std::endl;
        TANGO_LOG_INFO << "---->  starter_stat_file = " << starter_stat_file << std::endl;
    }
//+------------------------------------------------------------------
/**
 *	Remove the Fully Qualify Domain Name for tango less than 5.2 compatibility
 */
//+------------------------------------------------------------------
    std::string StarterUtil::removeFQDN(std::string s) {
        std::string::size_type pos = s.find('.');
        if (pos == std::string::npos)
            return s;
        else
            return s.substr(0, pos);
    }
//+------------------------------------------------------------------
/**
 *	Extract server name from input parameter (servname/instance).
 */
//+------------------------------------------------------------------
    char *StarterUtil::get_server_name(char *argin) {
        std::string fullName(argin);
        std::string serverName;
        std::string::size_type pos = fullName.find('/');
        if (pos == std::string::npos)
            serverName = fullName;
        else
            serverName = fullName.substr(0, pos);

        char *servname = new char[serverName.length() + 1];
        strcpy(servname, serverName.c_str());
        return servname;
    }
//+------------------------------------------------------------------
/**
 *	Extract instance name from input parameter (servname/instance).
 */
//+------------------------------------------------------------------
    char *StarterUtil::get_instance_name(char *argin) {
        std::string fullName(argin);
        std::string instanceName;
        std::string::size_type pos = fullName.find('/');
        if (pos == std::string::npos)
            instanceName = fullName;
        else
            instanceName = fullName.substr(pos + 1);

        char *instancename = new char[instanceName.length() + 1];
        strcpy(instancename, instanceName.c_str());
        return instancename;
    }

//+----------------------------------------------------------------------------
//
// method : 		StarterUtil::check_file()
//
// description : 	Check if executable file exists
//					and return its full name with good path.
//
//-----------------------------------------------------------------------------
    char *StarterUtil::check_exe_file(std::string name) {
        std::string filename(name);
#ifdef    _TG_WINDOWS_
        filename += ".exe";
#endif
        //TANGO_LOG_INFO << "Checking " << filename << std::endl;
        std::ifstream ifs(filename.c_str());
        if (ifs) {
            ifs.close();
            char *exeFullName = new char[strlen(filename.c_str()) + 1];
            strcpy(exeFullName, filename.c_str());
            return exeFullName;
        }
#ifdef    _TG_WINDOWS_

        //	Check for batch file
        filename = name;
        filename += ".bat";
        //TANGO_LOG_INFO << "Checking " << filename << std::endl;
        std::ifstream	ifs2(filename.c_str());
        if (ifs2)
        {
            ifs2.close();
            char *exeFullName = new char[strlen(filename.c_str())+1];
            strcpy(exeFullName, filename.c_str());
            return exeFullName;
        }
#endif

        return nullptr;
    }

//+----------------------------------------------------------------------------
//
// method : 		StarterUtil::check_file()
//
// description : 	Check if executable file exists
//					and return its full name with good path.
//
//-----------------------------------------------------------------------------
    char *StarterUtil::check_exe_file(char *servname, const std::vector<std::string>& v_path) {
        for (auto filename : v_path) {
            filename += slash;
            filename += servname;

            //	Check if exist
            char *p;
            if ((p = check_exe_file(filename)) != nullptr)
                return p;
        }

        //	server has not been found in path(es)
        //----------------------------------------------
        TangoSys_OMemStream out_stream;
        out_stream << servname << " :  not found in \'StartDsPath\' property:" << std::endl;
        for (const auto & path : v_path)
            out_stream << " - " << path << std::endl;
        out_stream << std::ends;
        Tango::Except::throw_exception((const char *) "CANNOT_RUN_FILE",
                                       out_stream.str(),
                                       (const char *) "StarterUtil::check_exe_file()");
    }
//+------------------------------------------------------------------
/**
 *	Format the date and time in the argin value (Ux format) as std::string.
 */
//+------------------------------------------------------------------
    char *StarterUtil::strtime(time_t t) {
        const std::size_t sizeof_str{100};
        static char str[sizeof_str];
        struct tm *st = localtime(&t);

        if (st->tm_year >= 100)
            st->tm_year -= 100;
        snprintf(str, sizeof_str, "%02d/%02d/%02d   %02d:%02d:%02d",
                st->tm_mday, st->tm_mon + 1, st->tm_year,
                st->tm_hour, st->tm_min, st->tm_sec);
        return str;
    }
//+------------------------------------------------------------------
/**
 *	Get the last modification on a file and return it in a std::string.
 *	@param	filename	file's name to get the date.
 */
//+------------------------------------------------------------------
    char *StarterUtil::get_file_date(char *filename) {
        struct stat info{};
        stat(filename, &info);
        return strtime(info.st_mtime);
    }
//+------------------------------------------------------------------
/**
 *	Log info for starter.
 *	@param	message	 mesage to be logged
 */
//+------------------------------------------------------------------
    void StarterUtil::log_starter_info(const std::string& message) {
        std::stringstream strlog;
        strlog << strtime(time(nullptr)) << "\t" << message << std::endl;

        //	Read and close log file.
        std::ifstream ifs((char *) starter_log_file.c_str());
        if (ifs) {
            strlog << ifs.rdbuf();
            ifs.close();
        }
        //	Check for nb lines
        std::string str(strlog.str());
        std::string::size_type pos = 0;
        int nb = 0;
        while (nb < STARTER_LOG_DEPTH && (pos = str.find('\n', pos + 1)) != std::string::npos)
            nb++;

        if (pos != std::string::npos)
            str = str.substr(0, pos);

        //	Write to log file
        std::ofstream ofs((char *) starter_log_file.c_str());
        ofs << str;
        ofs.close();
    }
//+------------------------------------------------------------------
/**
 *	Reset statistics for starter.
 */
//+------------------------------------------------------------------
    void StarterUtil::reset_starter_stat_file(std::vector<ControlledServer> *servers) {
        //	build present situation log
        std::stringstream strlog;
        std::vector<ControlledServer>::iterator it;
        for (it = servers->begin(); it < servers->end(); ++it) {
            if (it->controlled && it->startup_level > 0) {
                time_t failure_time = it->failure_time;
                if (failure_time < 0)
                    failure_time = time(nullptr);

                strlog << it->name << "\t" <<
                       ((it->get_state() == Tango::ON) ? "ON\t" : "FAULT\t") <<
                       it->started_time << "\t" <<
                       failure_time << "\t" <<
                       "false" << std::endl;
            }
        }

        //	Write an empty statistics file
        std::ofstream ofs((char *) starter_stat_file.c_str());
        ofs << strlog.str();
        ofs.close();
    }
//+------------------------------------------------------------------
/**
 *	Log statistica for starter.
 *	@param	message	 mesage to be logged
 */
//+------------------------------------------------------------------
    void StarterUtil::log_starter_statistics(ControlledServer *server) {
        std::stringstream strlog;
        strlog << server->name << "\t" <<
               ((server->get_state() == Tango::ON) ? "ON\t" : "FAULT\t") <<
               server->started_time << "\t" <<
               server->failure_time << "\t" <<
               ((server->auto_start) ? "true" : "false") << std::endl;
        server->auto_start = false;

        //	Read and close log file.
        std::ifstream ifs((char *) starter_stat_file.c_str());
        if (ifs) {
            strlog << ifs.rdbuf();
            ifs.close();
        }
        //	Check for nb lines
        std::string str(strlog.str());
        std::string::size_type pos = 0;
        int nb = 0;
        while (nb < STARTER_STAT_DEPTH && (pos = str.find('\n', pos + 1)) != std::string::npos)
            nb++;

        if (pos != std::string::npos)
            str = str.substr(0, pos);

        //	Write to log file
        std::ofstream ofs((char *) starter_stat_file.c_str());
        ofs << str;
        ofs.close();
    }
//+------------------------------------------------------------------
/**
 *	check if there is not to much log file and rename last one
 *	@param	filename	file's name to get the date and rename.
 */
//+------------------------------------------------------------------
    void StarterUtil::manage_log_file_history(char *filename, int nb_max) {
        std::string log_file(filename);

        //	Try to open log file
        std::ifstream ifs((char *) log_file.c_str());
        if (!ifs)
            return;    //	Does not exist-> do nothing

        //	Get the log file list
        std::vector<std::string> list = get_log_file_list(log_file);
        //for (unsigned int i=0 ; i<list.size() ; i++)
        //	TANGO_LOG_INFO << list[i] << std::endl;

        //	Check if too much files -> delete
        while (list.size() > ((unsigned int) nb_max - 1))    //	-1 because a new one will exist bellow
        {
            //TANGO_LOG_INFO << "Removing " << list[0] << std::endl;
            if (remove(list[0].c_str()) < 0)
                std::cerr << "remove failed : " << strerror(errno) << std::endl;
            list.erase(list.begin());
        }

        //	Build date and time (of file creation) part
        struct stat info{};
        stat(filename, &info);
        struct tm *st = localtime(&info.st_mtime);
        if (st->tm_year >= 100)
            st->tm_year -= 100;
        const std::size_t sizeof_strdate{32};
        char strdate[sizeof_strdate];
        snprintf(strdate, sizeof_strdate, "_[20%02d-%02d-%02d_%02d-%02d-%02d]",
                st->tm_year, st->tm_mon + 1, st->tm_mday,
                st->tm_hour, st->tm_min, st->tm_sec);

        //	search position to insert (before extention)
        std::string str(filename);
        std::string::size_type pos = str.rfind(".log");
        if (pos != std::string::npos)
            str = str.substr(0, pos);

        char *new_filename = new char[strlen(filename) + strlen(strdate) + 1];
        strcpy(new_filename, str.c_str());
        strcat(new_filename, strdate);
        strcat(new_filename, ".log");
        int ret = rename(filename, new_filename);
        if (ret < 0)
            std::cerr << "Renaming " << filename << " to " << new_filename << " failed : " << strerror(errno) << std::endl;
        //else
        //	TANGO_LOG_INFO << "Renaming " << filename << " to " << new_filename << "  done." << std::endl;
        delete[] new_filename;
    }

//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
    bool alphabetical(const std::string& a, const std::string& b) {
        //	returns alphabetic order.
        return (a < b);
    }
//+------------------------------------------------------------------
/**
 *	Rename log file list
 *	@param	filename	log file name
 */
//+------------------------------------------------------------------
    std::vector<std::string> StarterUtil::get_log_file_list(const std::string& logfile) {
        std::vector<std::string> list;
        //	Split path and file name
        std::string::size_type pos = logfile.rfind(slash);
        if (pos != std::string::npos) {
            std::string path = logfile.substr(0, pos);
            pos++;
            //	remove extention
            std::string str = logfile.substr(pos);
            pos = str.rfind('.');
            std::string filter = str.substr(0, pos);
            filter += "_[";

#ifndef _TG_WINDOWS_
            //TANGO_LOG_INFO << "Searching " << filter << "  in " << path << std::endl;
            DIR *dir = opendir((char *) path.c_str());
            if (dir == nullptr) {
                std::string desc;
                //	error
                switch (errno) {
                    case EACCES:
                        desc = "Permission denied.";
                        break;
                    case EMFILE:
                        desc = "Too many file descriptors in use by process.";
                        break;
                    case ENFILE:
                        desc = "Too many file are currently open in the system.";
                        break;
                    case ENOENT:
                        desc = "Directory does not exist or NAME is an empty std::string.";
                        break;
                    case ENOMEM:
                        desc = "Insufficient memory to complete the operation.";
                        break;
                    case ENOTDIR:
                        desc = "NAME is not a directory.";
                        break;
                    default:
                        desc = "Unknown error: ";
                        desc += errno;
                }
                Tango::Except::throw_exception(
                        (const char *) "READ_FILE_LIST_FAILED",
                        desc,
                        (const char *) "StarterUtil::get_log_file_list()");

            }
            struct dirent *ent;
            while ((ent = readdir(dir))) {
                std::string name(ent->d_name);
                pos = name.find(filter);
                if (pos == 0) {
                    std::string full_name(path);
                    full_name += "/";
                    full_name += name;
                    list.push_back(full_name);
                }
            }
            closedir(dir);
#else
            //TANGO_LOG_INFO << "Searching " << filter << "  in " << path << std::endl;
            path += "\\";
            path += filter;
            path += "*";
            WCHAR	*w_path = ProcessData::string2wchar(path);

            WIN32_FIND_DATA fd;
            DWORD dwAttr = FILE_ATTRIBUTE_DIRECTORY;
            HANDLE hFind = FindFirstFile(w_path, &fd);
            if(hFind != INVALID_HANDLE_VALUE) {
                do {
                    std::string	s = ProcessData::wchar2string(fd.cFileName);
                    list.push_back(s);
                }
                while (FindNextFile( hFind, &fd));
                FindClose( hFind);
            }
            else {
                std::string desc = ProcessData::errorCodeToString(GetLastError(), "FindFirstFile()" );
                std::cerr << "Invalid Handle value " << desc << std::endl;
            }
            delete w_path;
#endif
        }
        sort(list.begin(), list.end(), alphabetical);
        return list;
    }

//+------------------------------------------------------------------
/**
 *	method:	StarterUtil::build_log_file_name
 *
 *	description:	Build the error log file name from server name and intancename.
 *
 *      @param  path     path to write file
 *      @param  server   name of the server
 */
//+------------------------------------------------------------------
    std::string StarterUtil::build_log_file_name(char *server) {
        //	Split server and instance names.
        std::string serverName(server);
        unsigned long index = serverName.find('/');
        std::string binary = serverName.substr(0, index);
        std::string instance = serverName.substr(++index);

        //	And create full name with path
        std::string log_file;
        LogPath(log_file, log_home)
        log_file += slash;
        log_file += binary;
        log_file += "_";
        log_file += instance;
        log_file += ".log";
        return log_file;
    }








//====================================================================
//
//	The controlled Servers Object management
//
//====================================================================

//+------------------------------------------------------------------
/**
 *	Get host device servers list from database.
 *
 *	@param dbase	Database device as a DeviceProxy for not implemented API commands.
 */
//+------------------------------------------------------------------
    std::vector<std::string> StarterUtil::get_host_ds_list() {
        //	Read server info from database.
        std::vector<std::string> serverNames;
        for (std::string hostName : hostNames) {
            Tango::DeviceData argin;
            argin << hostName;
            TANGO_LOG_INFO << "DbGetHostServerList for " << hostName << std::endl;
            Tango::DeviceData argout = dbase->command_inout("DbGetHostServerList", argin);
            std::vector<std::string> tmp;
            argout >> tmp;

            //	Check servers really used (Erase this one and database server
            std::vector<std::string>::iterator pos;
            for (pos = tmp.begin(); pos < tmp.end(); ++pos) {
                unsigned long idx = (*pos).find_first_of("/");
                if (idx > 0) {
                    //	Get process name only in lower case before compeare
                    std::string s = (*pos).substr(0, idx);
                    transform(s.begin(), s.end(), s.begin(), ::tolower);
                    if (s == "starter" ||
                        s == "databaseds" ||
                        s == "tangoaccesscontrol" ||
                        s == "logconsumer") {
                        tmp.erase(pos);
                        --pos;    //	because erase decrease size !
                    }
                }
            }
            //	Copy to global std::vector
            for (const auto & j : tmp)
                serverNames.push_back(j);
        }
        TANGO_LOG_INFO << "----------------------------------------" << std::endl;
        TANGO_LOG_INFO << serverNames.size() << " servers found" << std::endl;
        for (const auto & serverName : serverNames)
            TANGO_LOG_INFO << "\t" << serverName << std::endl;
        return serverNames;
    }
//+------------------------------------------------------------------
/**
 *	Read DS info from database to know if it is controlled
 *		and it's starting level.
 *
 *	@param	devname	device to get info.
 *	@param	server	object to be updated from db read.
 */
//+------------------------------------------------------------------
    void StarterUtil::get_server_info(ControlledServer *server) {
        try {
            //	Read server info from database.
            Tango::DeviceData argin;
            argin << server->name;
            Tango::DeviceData argout = dbase->command_inout("DbGetServerInfo", argin);
            std::vector<std::string> result;
            argout >> result;
            server->controlled = atoi(result[2].c_str()) != 0;
            server->startup_level = (short) atoi(result[3].c_str());
        }
        catch (Tango::DevFailed &e) {
            Tango::Except::print_exception(e);
            server->controlled = false;
            server->startup_level = 0;
        }
        //TANGO_LOG_INFO << server->name << " - " << ((server->controlled)? "true": "false");
        //TANGO_LOG_INFO << " ----> Startup level " << server->startup_level <<std::endl;
    }
//+------------------------------------------------------------------
/**
 *	Allocate and fill the servers controlled object
 */
//+------------------------------------------------------------------
    void StarterUtil::build_server_ctrl_object(std::vector<ControlledServer> *servers) {
        std::vector<std::string> result_from_db;
        for (auto & hostName : hostNames) {
            //	Call for servers and their info for each host
            //TANGO_LOG_INFO << "Call for servers on " << hostnames[i] << std::endl;
            Tango::DeviceData argin;
            argin << hostName;
            Tango::DeviceData argout = dbase->command_inout("DbGetHostServersInfo", argin);
            argout >> result_from_db;
        }
        //	Check servers really used (erase this one and database server)
        std::vector<std::string>::iterator pos;
        std::vector<std::string> result;
        for (pos = result_from_db.begin(); pos < result_from_db.end(); pos += 3) {
            unsigned long idx = (*pos).find_first_of("/");
            if (idx > 0) {
                //	Get process name only in lower case before compare
                std::string s = (*pos).substr(0, idx);
                transform(s.begin(), s.end(), s.begin(), ::tolower);
                if (s != "starter" &&
                    s != "databaseds" &&
                    s != "tangoaccesscontrol" &&
                    s != "logconsumer") {
                    result.push_back(*pos);        //	Server name
                    result.push_back(*(pos + 1));    //	Controlled/Not Controlled
                    result.push_back(*(pos + 2));    //	Startup Level
                }
            }
        }

        //	Check if some servers have disappeared
        std::vector<ControlledServer>::iterator it;
        bool redo = true;    //	Iterators management seems to have changed
        //	between vc6 and vc8  (??)
        while (redo) {
            redo = false;
            for (it = servers->begin(); it < servers->end(); ++it) {
                std::string s1(it->name);
                bool found = false;
                for (unsigned long i = 0; !found && i < result.size(); i += 3)
                    found = (s1 == result[i]);

                if (!found) {
                    //	if disappeared then stop thread and remove reference
                    it->thread_data->set_stop_thread();
                    it->thread->join(0);
                    servers->erase(it);
                    redo = true;
                    break;    //	get out of loop (std::vector size has changed).
                }
            }
        }
        //	Check if new servers appeared
        for (pos = result.begin(); pos < result.end();) {
            std::string name(*pos++);
            ControlledServer *p_serv = get_server_by_name(name, *servers);
            if (p_serv == nullptr) {
                //	Create a new server instance
                ControlledServer server;

                server.name = name;
                server.admin_name = "dserver/" + server.name;
                server.controlled = atoi((*pos++).c_str()) != 0;
                server.startup_level = (short) atoi((*pos++).c_str());
                server.set_state(Tango::FAULT);
                server.stopped = false;
                server.auto_start = false;
                server.started_time = time(nullptr);
                server.failure_time = -1;

                //	Add a thread to ping server
                server.thread_data = new PingThreadData(server.name);
                server.thread =
                        new PingThread(server.thread_data, server.name, proc_util);
                server.thread->start();

                servers->push_back(server);
                ms_sleep(50) //	wait for server updated.
            } else {
                //	Update levels
                p_serv->controlled = atoi((*pos++).c_str()) != 0;
                p_serv->startup_level = (short) atoi((*pos++).c_str());
            }
        }
    }

//+------------------------------------------------------------------
/**
 *	search a server in ControlledServer array by it's name .
 *
 *	@param servname	Server searched name.
 */
//+------------------------------------------------------------------
    ControlledServer *StarterUtil::get_server_by_name(std::string &servname, std::vector<ControlledServer> &servers) {
        std::string serverName(servname);
        transform(serverName.begin(), serverName.end(), serverName.begin(), ::tolower);
        for (auto & i : servers) {
            ControlledServer *server = &i;
            std::string ctrlName(server->name);
            transform(ctrlName.begin(), ctrlName.end(), ctrlName.begin(), ::tolower);
            if (ctrlName == serverName)
                return server;
        }
        return nullptr;
    }

//+----------------------------------------------------------------------------
//+----------------------------------------------------------------------------
    void ControlledServer::set_state(Tango::DevState st) {
        if (st == Tango::MOVING && state != Tango::MOVING) {
            //  Has just been switched to MOVING
            moving_time = time(nullptr);
        }
        state = st;
    }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
}    //	namespace
