/* slimski - Simple Login Manager
   Copyright (C) 2004-06 Simone Rota <sip@varlock.com>
   Copyright (C) 2004-06 Johannes Winkelmann <jw@tks6.net>

   This program 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 2 of the License, or
   (at your option) any later version.
*/

#include <fstream>
#include <string>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
// #i nclude <cstdio>  // late addition ~~  needed for logging?   NOT USEFUL
// #i nclude <sstream>   // late addition ~~  needed for logging?
#include "log.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>

#include "cfg.h"
#include "app.h"   //    zeebug
#include <algorithm>  //  removeSpaces

using namespace std;

extern bool testing;
extern bool zeebug;


typedef pair<string,string> option;

Cfg::Cfg()
    : currentSession(-1)
{
    // Configuration options
    options.insert(option("default_path","/usr/local/bin:/usr/bin"));
    options.insert(option("default_xserver","/usr/bin/X11/X"));
    //  howdy    could use (/usr/bin/X11/X) <--^  both are symlinks pointing to Xorg

    options.insert(option("xserver_arguments",""));
    options.insert(option("numlock_enabled","false"));
    options.insert(option("daemonmode_enabled","false"));

    options.insert(option("xauth_path","/usr/bin/xauth"));
  //        ^----- as seen in antiX19          which xauth

    options.insert(option("atlogin_cmd","exec /usr/bin/bash -login ~/.xinitrc %sessiontype"));
    options.insert(option("systemhalt_cmd","/usr/sbin/shutdown -h now"));
    options.insert(option("reboot_cmd","/usr/sbin/shutdown -r now"));
    options.insert(option("exit_enabled","false")); // documentation explains: no passwd, and slimski may have been invoked from a root shell
    options.insert(option("sessionstart_cmd",""));
    options.insert(option("sessionstop_cmd",""));

    options.insert(option("popfirst_cmd",""));

    options.insert(option("F1_msg","Press F1 to change sessiontype"));
    ////       THE COMMAND (TOGGLE SESSIONTYPE) IS HARDCODED
    options.insert(option("F1_font","Verdana:size=12"));
    options.insert(option("F1_color","#FFFFFF"));
    options.insert(option("F1_x","-1"));
    options.insert(option("F1_y","-1"));

    options.insert(option("F2_cmd",""));
    options.insert(option("F2_msg",""));
    options.insert(option("F2_font","Verdana:size=12"));
    options.insert(option("F2_color","#000000"));
    options.insert(option("F2_x","-1"));
    options.insert(option("F2_y","-1"));

    options.insert(option("F3_cmd",""));
    options.insert(option("F3_msg",""));
    options.insert(option("F3_font","Verdana:size=12"));
    options.insert(option("F3_color","#000000"));
    options.insert(option("F3_x","-1"));
    options.insert(option("F3_y","-1"));

    options.insert(option("F4_cmd",""));
    options.insert(option("F4_msg",""));
    options.insert(option("F4_font","Verdana:size=12"));
    options.insert(option("F4_color","#000000"));
    options.insert(option("F4_x","-1"));
    options.insert(option("F4_y","-1"));

    options.insert(option("F5_cmd",""));
    options.insert(option("F5_msg",""));
    options.insert(option("F5_font","Verdana:size=12"));
    options.insert(option("F5_color","#000000"));
    options.insert(option("F5_x","-1"));
    options.insert(option("F5_y","-1"));

    options.insert(option("F6_cmd",""));
    options.insert(option("F6_msg",""));
    options.insert(option("F6_font","Verdana:size=12"));
    options.insert(option("F6_color","#000000"));
    options.insert(option("F6_x","-1"));
    options.insert(option("F6_y","-1"));

    options.insert(option("F7_cmd",""));
    options.insert(option("F7_msg",""));
    options.insert(option("F7_font","Verdana:size=12"));
    options.insert(option("F7_color","#000000"));
    options.insert(option("F7_x","-1"));
    options.insert(option("F7_y","-1"));

    options.insert(option("F8_cmd",""));
    options.insert(option("F8_msg",""));
    options.insert(option("F8_font","Verdana:size=12"));
    options.insert(option("F8_color","#000000"));
    options.insert(option("F8_x","-1"));
    options.insert(option("F8_y","-1"));

    options.insert(option("F9_cmd",""));
    options.insert(option("F9_msg",""));
    options.insert(option("F9_font","Verdana:size=12"));
    options.insert(option("F9_color","#000000"));
    options.insert(option("F9_x","-1"));
    options.insert(option("F9_y","-1"));

    options.insert(option("F10_cmd",""));
    options.insert(option("F10_msg",""));
    options.insert(option("F10_font","Verdana:size=12"));
    options.insert(option("F10_color","#000000"));
    options.insert(option("F10_x","-1"));
    options.insert(option("F10_y","-1"));

    options.insert(option("F11_cmd",""));
    options.insert(option("F11_msg",""));       // suggested use: screenshot
    options.insert(option("F11_font","Verdana:size=12"));
    options.insert(option("F11_color","#000000"));
    options.insert(option("F11_x","-1"));
    options.insert(option("F11_y","-1"));

    options.insert(option("F12_cmd",""));
    options.insert(option("F12_msg",""));
    options.insert(option("F12_font","Verdana:size=12"));
    options.insert(option("F12_color","#000000"));
    options.insert(option("F12_x","-1"));
    options.insert(option("F12_y","-1"));

    options.insert(option("welcome_msg","Welcome to %host"));

    options.insert(option("sessiontype_msg","Sessiontype: "));
    options.insert(option("default_user",""));
    options.insert(option("passwdfocus_enabled","false"));
    options.insert(option("autologin_enabled","false"));
//  options.insert(option("showlogo_enabled","false"));
//  options.insert(option("logo_x","53"));
//  options.insert(option("logo_y","53"));

    options.insert(option("current_theme","default"));

////options.insert(option("lockfile","/var/run/slimski.pidlock"));
////          To avoid "running circles" at start of App constructor,
////          this pathstring is now hardcoded (within slimski App.cpp)
////          (This detail is now noted within slimski.init)

////options.insert(option("logfile","/var/log/slimski.log"));
////          this pathstring is now hardcoded (within slimski App.cpp)

    options.insert(option("authfile","/var/run/slimski.auth"));
    options.insert(option("systemhalt_msg","The system is halting..."));
    options.insert(option("reboot_msg","The system is rebooting..."));
    //  v---- howdy  the as-shipped conf states differently, so...  moot, execpt in case of accidental conf {line,file} deletion
    options.insert(option("sessiontypes","fluxbox,openbox,icewm,startxfce4"));
    //    when user supplies non-blank pathstring (via slimski.conf)
    //    xsessionsdir lookup is performed (and "sessiontypes" conf option is ignored)
    //                       ^--> strike "is" and insert "would be" (currently, this functionality is suppressed)
    options.insert(option("xsessionsdir",""));

    options.insert(option("default_sessiontype","fluxbox"));

    // Theme stuff
    options.insert(option("input_panel_x","50%"));   //  howdy   bub       not suitable for dual-screen displays
    options.insert(option("input_panel_y","40%"));
    options.insert(option("input_name_x","21"));   // WAS 200
    options.insert(option("input_name_y","19"));   // WAS 150
    options.insert(option("input_pass_x","-1")); // default is single inputbox
    options.insert(option("input_pass_y","-1"));
    options.insert(option("input_font","Verdana:size=11"));
    options.insert(option("input_color","#000000"));
    options.insert(option("input_cursor_height","32"));
    options.insert(option("input_hidetextcursor","false"));
    options.insert(option("input_center_text","false"));
    options.insert(option("input_maxlength_name","20"));
    options.insert(option("input_maxlength_passwd","20"));

    options.insert(option("mouse_enabled","true"));   // howdy     flip a coin...

    //  howdy    specifying -1 for both x and y (most msg items) via theme will suppress from display
    //           mentioned in THEMES doc, but is not mentioned for each msg item
    options.insert(option("welcome_font","Verdana:size=14"));
    options.insert(option("welcome_color","#FFFFFF"));
    options.insert(option("welcome_x","-1"));
    options.insert(option("welcome_y","-1"));

                                         // LOGIN TO GRAPHICAL XSESSION USING ROOTUSER ACCOUNT IS FORBIDDEN
    options.insert(option("forbidden_msg","ROOTUSER LOGIN IS FORBIDDEN"));
    ////     THIS MESSAGESTRING IS NOW DISPLAYED VIA showMuMsg() IN A USING A NON-CONFIGURABLE FONT
    //       (ITS FONT SIZE IS ADJUSTED DYNAMICALLY, ACCORDING TO RUNTIME SCREEN WIDTH)
    ////options.insert(option("forbidden_font","Verdana:size=12"));
    ////options.insert(option("forbidden_color","#FF0000"));

    options.insert(option("reboot_enabled","false"));
    options.insert(option("systemhalt_enabled","false"));
    options.insert(option("suspend_cmd",""));
    options.insert(option("disabledviaconf_msg","disabled, per the current configuration"));

    options.insert(option("background_style","solidcolor"));   // this refers to the PANEL
    options.insert(option("background_color","#CCCCCC"));

    options.insert(option("username_font","Verdana:size=12"));
    options.insert(option("username_color","#FFFFFF"));
    options.insert(option("username_x","-1"));
    options.insert(option("username_y","-1"));
    options.insert(option("username_msg","Please enter your username"));

    options.insert(option("password_x","-1"));
    options.insert(option("password_y","-1"));
    options.insert(option("password_msg","Please enter your password"));
    options.insert(option("password_feedback_capslock_msg","Authentication failed (CapsLock is ON)"));
    options.insert(option("password_feedback_msg","Authentication failed"));

    options.insert(option("login_cmd_failed_msg","Failed to execute login command"));

    options.insert(option("msg_color","#FFFF00"));
    options.insert(option("msg_font","Verdana:size=12:bold"));
    options.insert(option("msg_x","10%"));
    options.insert(option("msg_y","50%"));

    options.insert(option("sessiontype_color","#FFFFFF"));
    options.insert(option("sessiontype_font","Verdana:size=16:bold"));
    options.insert(option("sessiontype_x","12%"));
    options.insert(option("sessiontype_y","12%"));

    options.insert(option("cust1_color","#9999EE"));
    options.insert(option("cust1_font","Verdana:size=22"));
    options.insert(option("cust1_msg",""));
    options.insert(option("cust1_x","16%"));
    options.insert(option("cust1_y","18%"));

    options.insert(option("cust2_color","#9999EE"));
    options.insert(option("cust2_font","Verdana:size=20"));
    options.insert(option("cust2_msg",""));
    options.insert(option("cust2_x","16%"));
    options.insert(option("cust2_y","26%"));

    options.insert(option("cust3_color","#9999EE"));
    options.insert(option("cust3_font","Verdana:size=16"));
    options.insert(option("cust3_msg",""));
    options.insert(option("cust3_x","16%"));
    options.insert(option("cust3_y","34%"));   //    howdy  bub      for sake of testing, the implicit value is NOT an INT

    options.insert(option("cust4_color","#9999EE"));
    options.insert(option("cust4_font","Verdana:size=14"));
    options.insert(option("cust4_msg",""));
    options.insert(option("cust4_x","46%"));
    options.insert(option("cust4_y","340"));

    options.insert(option("cust5_color","#9999EE"));
    options.insert(option("cust5_font","Verdana:size=12"));
    options.insert(option("cust5_msg",""));   //    (( "cust5 _ msg  @12   ¥·£·€·$·¢·₡·₢·₣·₤·₥·₦·₧·₩·₪·₫·₭·₮·₯·₹"));
    options.insert(option("cust5_x","58%"));
    options.insert(option("cust5_y","440"));

    error = "";
}


Cfg::~Cfg() {
    options.clear();
}


// Creates the Cfg object and parses known options from the given configfile
//     called from app.cpp, to parse conffile... then parse theme file... then parse localized theme variant, if such exists
bool Cfg::readConf(string configfile) {
    string line, fn(configfile);
    map<string,string>::iterator it;
    string op;
    ifstream cfgfile( fn.c_str() );
    if (cfgfile) {
        if (zeebug)
            cout << "    now reading option values specified in " << configfile << endl;

        while (getline( cfgfile, line )) {
            it = options.begin();
            int n = -1;
            while (it != options.end()) {
                op = it->first;
                n = line.find(op);
                if (n == 0) {
                    options[op] = parseOption(line, op);
                    if (zeebug)
                        cout << op << "  :  " << options[op] << endl;    //  this
                }
                it++;
            }
        }
        cfgfile.close();
        if (zeebug)    cout << "________________________________\n" << endl;

        fillSessAvailableList();
        return true;
    } else {
        logStream << "slimski: Cannot read file " << configfile << endl;
        if (zeebug)
             cout << "slimski: Cannot read file " << configfile << endl;

        return false;
    }
}


// Returns the option VALUE, trimmed
string Cfg::parseOption(string line, string daoption ) {
    daoption = daoption.substr(0,500);   //    howdy      an arbitrary limit
    for (const char c : daoption) {
        //      is a control character as classified by the currently installed C locale.
        //      In the default, "C" locale, the control characters are the characters with the codes 0x00-0x1F and 0x7F
        ////         this would be more robust:  https://en.cppreference.com/w/cpp/locale/iscntrl
        if ( iscntrl(c) ) {
            logStream << "slimski: fatal error ~~ value for this variable contains control characters" << endl;
            logStream << "        " << daoption << "\nslimski will now exit." << endl;
            if (testing) {
                cout << "slimski: fatal error ~~ value for this variable contains control characters" << endl;
                cout << "        " << daoption << "\nslimski will now exit." << endl;
            }

            exit(ERR_EXIT);
        }
    }

    return Trim( line.substr(daoption.size(), line.size() - daoption.size()));
}


const string& Cfg::getError() const {
    return error;
}

string& Cfg::getOption(string option) {
    return options[option];
}


void Cfg::printAllOptvals() {
    cout << "\n    v------- list of all slimski runtime vars\n";
    for(auto i = options.cbegin(); i != options.cend(); ++i) {
    //for( map<string, pair<string,string> >::const_iterator i = options.begin() ) {
        cout << i->first << " : " << i->second << "\n";
    }
    cout << "    ^______ list of all slimski runtime vars" << endl;
}


//  return a trimmed string
string Cfg::Trim( const string& s ) {
    if ( s.empty() ) {
        return s;
    }
    int pos = 0;
    string line = s;
    int len = line.length();
    while ( pos < len && isspace( line[pos] ) ) {
        ++pos;
    }
    line.erase( 0, pos );
    pos = line.length()-1;
    while ( pos > -1 && isspace( line[pos] ) ) {
        --pos;
    }
    if ( pos != -1 ) {
        line.erase( pos+1 );
    }
    return line;
}

//  Return the welcome message with replaced vars
//         A NON-GENERIC "INSERT SUBSTRING INTO EXISTING STRING AT PLACEHOLDER POSITION" FN
string Cfg::getWelcomeMessage(){
    string s = getOption("welcome_msg");
    int n = -1;
    n = s.find("%host");
  //if (n >= 0) {
  //    ^---v
    if (n != string::npos) {
        string tmp = s.substr(0, n);
        char host[40];
        gethostname(host,40);
        tmp = tmp + host;
        tmp = tmp + s.substr(n+5, s.size() - n);
        s = tmp;
    }
    n = s.find("%domain");
  //if (n >= 0) {
  //    ^---v
    if (n != string::npos) {
        string tmp = s.substr(0, n);     // welcome_msg accepts two (optional) variables: %host and %domain
        char domain[40];
        getdomainname(domain,40);  // unused
        tmp = tmp + domain;
        tmp = tmp + s.substr(n+7, s.size() - n);
        s = tmp;
    }
    return s;
}


//   howdy     compareto   stoi()
int Cfg::string2int(const char* string, bool* ok) {
    char* err = 0;
    int l = (int)strtol(string, &err, 10);
    if (ok) {
        *ok = (*err == 0);
    }
    return (*err == 0) ? l : 0;
}


        //   howdy bub     RECHECK whatif the value represents a PERCENT value
        // used ONLY for: input_name_x, input_name_y, input_pass_x, input_pass_y
int Cfg::getIntOption(std::string option) {
    return string2int(options[option].c_str());
}
/**             THIS IS A LATE CHANGE, PERHAPS UNNEEDED. CONSIDER REVERTING THIS LATER
int Cfg::getIntOption(std::string option, int defaultVal) {
    int l = string2int(options[option].c_str());
    return (l > 0) ? l : defaultVal;
}
*/


/**
             // howdy        not yet used       nixme
bool Cfg::optionIsTrue(string option) const {
    //  these comparisons should be, but are not, case-insensitive
    return options.at(option)=="true" || options.at(option)=="yes" || options.at(option)=="on";
}
*/


// Get absolute position
int Cfg::absolutepos(const string& position, int max, int width) {
    //int n = -1;
    int n = position.find("%");     // howdy          whatif  1%4 typo
    if (n>0) { // X Position expressed in percentage
        int result = (max*string2int(position.substr(0, n).c_str())/100) - (width / 2);
        return result < 0 ? 0 : result ;
    } else { // Absolute X position
        return string2int(position.c_str());
    }
}

// split a comma-separated string into a vector of strings
//        called only once, from fillSessAvailableList()...  as "split(sessiontypes, strSessionList, ',', false)"
void Cfg::split(vector<string>& v, const string& str, char c, bool useEmpty) {
    v.clear();
    string::const_iterator s = str.begin();
    string tmp;
    while (true) {
        string::const_iterator begin = s;
        while (*s != c && s != str.end()) { ++s; }
    tmp = string(begin, s);
    if (useEmpty || tmp.size() > 0)
            v.push_back(tmp);
            if (s == str.end()) {
                break;
            }
        if (++s == str.end()) {
        if (useEmpty)
                v.push_back("");
            break;
        }
    }
}


void Cfg::fillSessAvailableList(){
    string strSessionList = getOption("sessiontypes");
    string strXsessionsDir  = getOption("xsessionsdir");

    sessiontypes.clear();    //  howdy    unnecessary here?

// -------------------
    if( !strXsessionsDir.empty() ) {
        DIR *pDir = opendir(strXsessionsDir.c_str());
        if (pDir != NULL) {
            struct dirent *pDirent = NULL;

            while ((pDirent = readdir(pDir)) != NULL) {
                string strFile(strXsessionsDir);
                strFile += "/";
                strFile += pDirent->d_name;
                struct stat oFileStat;

                if (stat(strFile.c_str(), &oFileStat) == 0) {
                    if (S_ISREG(oFileStat.st_mode)  &&  access(strFile.c_str(), R_OK) == 0) {
                        ifstream desktop_file( strFile.c_str() );
                        if (desktop_file) {
                            string line, str_exec = "";
                            while (getline(desktop_file, line)) {
                                if (line.substr(0, 5) == "Exec=") {
                                    str_exec = line.substr(5);
                                    if (!str_exec.empty()) {
                                        sessiontypes.push_back(str_exec);
                                        // if (zeebug)     cout << str_exec << endl;
                                    }
                                }
                            }
                            desktop_file.close();
                        }
                    }
                }
            }
            closedir(pDir);
        }

        if (sessiontypes.empty()) {
            logStream << "\n  no usable .desktop files found in the specified XSESSIONSDIR\n  slimski will now exit.\n" << endl;
            if (zeebug)
                cout << "\n  no usable .desktop files found in the specified XSESSIONSDIR\n  slimski will now exit.\n" << endl;

            exit(ERR_EXIT);
        }
// -------------------

    } else if ( !strSessionList.empty() ) {
        // we do provide a conf variable: default_sessiontype
        strSessionList = removeSpaces(strSessionList);   // pre-parse
        split(sessiontypes, strSessionList, ',', false);
    } else {
        if (testing) {
            cout << "RECHECK YOUR CONF ~~ no sessiontypes were specified, or" << endl;
            cout << "   no usable desktop files exist in the spcified xsessionsdir\n" << endl;
            cout << "cannot proceed; slimski must now exit." << endl;
        }

        logStream << "RECHECK YOUR CONF ~~ no sessiontypes were specified, or" << endl;
        logStream << "   no usable desktop files exist in the spcified xsessionsdir\n" << endl;
        logStream << "cannot proceed; slimski must now exit." << endl;
        exit(ERR_EXIT);
    }
}


string Cfg::removeSpaces(string str) {
    str.erase(remove(str.begin(), str.end(), ' '), str.end());
    return str;
}

bool Cfg::isValidSesstype(string hoo) {                      //   v--- If no elements match, find() returns the last.
    if ( std::find(sessiontypes.begin(), sessiontypes.end(), hoo) != sessiontypes.end() ) {
        return true;      //                                <--^   counterintuitive, eh
    } else {
        return false;    //   howdy bub   TEST    blank string yields false
    }
}

int Cfg::getIntSesslistSize() {
    size_t elm = sessiontypes.size();
    int goodint = static_cast<int>(elm);
    return goodint;
}

string Cfg::dacurrentSessiontype() {
    if (sessiontypes.size() > 0) {
        return sessiontypes[0];
    } else {
        return "";  // howdy    we hope to never arrive here
    }
}


string Cfg::nextSession(string current) {
    if (sessiontypes.size() < 1)
        return current;

    currentSession = (currentSession + 1) % sessiontypes.size();
    return sessiontypes[currentSession];
}
