/**
 * File:	modules/LogView.ycp
 * Package:	YaST2
 * Summary:	Displaying a log with additional functionality
 * Authors:	Jiri Srain <jsrain@suse.cz>
 *
 * $Id: LogView.ycp 18863 2004-08-27 13:14:34Z arvin $
 *
 * All of these functions watch the log file and display
 * added lines as the log grows.
 * <pre>
 * LogView::DisplaySimple ("/var/log/messages");
 * LogView::DisplayFiltered ("/var/log/messages", "\\(tftp\\|TFTP\\)");
 * LogView::Display ($[
 *          "file" : "/var/log/messages",
 *          "grep" : "dhcpd",
 *          "save" : true,
 *          "actions" : [	// menu buttons
 *              [ _("Restart DHCP Server"),
 *                  RestartDhcpDaemon ],
 *              [ _("Save Settings and Restart DHCP Server"),
 *                  DhcpServer::Write ],
 *          ],
 *      ]);
 * </pre>
 */

{

module "LogView";
textdomain "base";

import "CWM";
import "Popup";
import "Label";
import "Report";

// fallback settings variables

/**
 * default value of maximum displayed lines
 */
integer max_lines_default = 100;


// configuration variables

/**
 * global parameters for the log displaying widget
 */
map<string,any> param = $[];

/**
 * list of all the logs that can be displayed
 */
list<map<string,any> > logs = [];

/**
 * index of currently selected log
 */
integer current_index = 0;

/**
 * list of actions that can be processed on the logs
 */
list<list> mb_actions = [];


// status variables

/**
 * current lines of the selected log
 */
list<string> lines = [];


// local functions

/**
 * Get the map describing the particular log file from its index
 * @param index integer index of the log file
 * @return a map describing the log file
 */
define map<string,any> Index2Descr (integer index) {
    return logs[index]:$[];
}

/**
 * Get maximum lines to display for a log
 * @param log_descr a map describing the log
 * @return integer maximum log lines to display
 */
define integer GetMaxLines (map<string,any> log_descr) {
    return log_descr["max_lines"]:param["max_lines"]:max_lines_default;
}

/**
 * Starts the log reading command via background agent
 * @param index integer the index of the log file
 */
define void InitLogReading (integer index) {
    SCR::Execute (.background.kill);
    map<string, any> log_descr = Index2Descr (index);
    integer max_lines = GetMaxLines (log_descr);
    string command = (string) (log_descr["command"]:nil);
    if (command == nil || command == "")
    {
	string file = log_descr["file"]:"";
	if (file == nil || file == "")
	{
	    // error report
	    Report::Error (_("Error occurred while reading the log."));
	    return;
	}
	string grep = log_descr["grep"]:"";
	if (grep != "" && grep != nil)
	    grep = sformat ("| grep --line-buffered '%1'", grep);
	string lc_command
	    = sformat ("cat %1 %2 | wc -l", log_descr["file"]:"", grep);
	map bash_output = (map)SCR::Execute (.target.bash_output, lc_command);
	command = "tail -f -n +0 " + log_descr["file"]:"";
	string addon = "";
	if (bash_output["exit"]:1 == 0)
	{
	    string lc = bash_output["stdout"]:"";
	    lc = filterchars (lc, "1234567890");
	    integer lines_count = tointeger (lc);
	    lines_count = lines_count - 2 * max_lines;
		// don't know why without
		// doubling it discards more lines, out of YaST2
		// it works
	    if (max_lines != 0 && lines_count > 0)
		addon = sformat ("| tail -n +%1", lines_count);
	}
	command = sformat ("%1 %2 %3", command, grep, addon);
    }
    y2milestone ("Calling background agent with command %1", command);
    boolean cmdret = (boolean)SCR::Execute (.background.run_output, command);
    if (! cmdret)
    {
	// error report
	Report::Error (_("Error occurred while reading the log."));
	return;
    }
}

/**
 * Kill processes running on the backgrouns
 * @param key log widget key
 */
define void KillBackgroundProcess (string key) {
    SCR::Execute (.background.kill);
}

/**
 * Remove unneeded items from a list
 * If max_lines is 0, then don't remove anything
 * @param lines a list of strings representing log lines
 * @param max_lines integer lines that should be saved
 * @return a list last max_lines of lines
 * FIXME probably used in multiple locations!!!
 * FIXME variables are global, no need to have them as parameters
 */
define list<string> DeleteOldLines (list<string> lines, integer max_lines) {
    if (0 == max_lines)
	return lines;
    integer sl = size (lines);
    if (sl > max_lines)
    {
	lines = filter (string l, lines, {
	    sl = sl -1;
	    return sl < max_lines;
	});
    }
    return lines;
}

/**
 * Fills the log widget with initial data got from the background agent
 * @param index integer index of the log file
 */
define void FillWidgetWithData (integer index) {
    sleep (100);
    map<string,any> log = Index2Descr (index);
    integer max_lines = GetMaxLines (log);

    integer count = (integer)SCR::Read (.background.newlines);

    if (count > 0)
    {
	lines = (list<string>) SCR::Read (.background.newout);
	lines = DeleteOldLines (lines, max_lines);
	UI::ChangeWidget (`id (`_cwm_log), `Value,
	    mergestring (lines, "\n") + "\n");
    }
}

/**
 * Get the help for the log in case of multiple logs
 * @return string part of the log
 */
global define string LogSelectionHelp () {
    // help for the log widget, part 1, alt. 1
    return _("<p><b><big>Displayed Log</big></b><br>
Use <b>Log</b> to select the log to display. It will be displayed in
the field below.</p>
");
}

/**
 * Get the help for the log in case of a single log
 * @return string part of the log
 */
global define string SingleLogHelp () {
    // help for the log widget, part 1, alt. 2
    return _("<p><b><big>The Log</big></b><br>
This screen displays the log.</p>");

}

/**
 * Get the second part of the help for the log in case of advanced functions
 *  and save support
 * @param label tge label of the menu button
 * @return string part of the log
 */
global define string AdvancedSaveHelp (string label) {
    // help for the log widget, part 2, alt. 1, %1 is a menu button label
    return sformat (_("<p>
To process advanced actions or save the log into a file, click <b>%1</b>
and select the action to process.</p>"), label);

}

/**
 * Get the second part of the help for the log in case of advanced functions
 * @param label tge label of the menu button
 * @return string part of the log
 */
global define string AdvancedHelp (string label) {
    // help for the log widget, part 2, alt. 2, %1 is a menu button label
    return sformat (_("<p>
To process advanced actions, click <b>%1</b>
and select the action to process.</p>"), label);

}

/**
 * Get the second part of the help for the log in case of save support
 * @return string part of the log
 */
global define string SaveHelp () {
    // help for the log widget, part 2, alt. 3
    return _("<p>
To save the log into a file, click <b>Save Log</b> and select the file
to which to save the log.</p>
");
}

/**
 * Get the help of the widget
 * @param logs integer count of displayed logs
 * @param parameters map parameters of the log to display
 * @return string help to the widget
 */
define string CreateHelp (integer logs,
    map parameters)
{
    string help = parameters["help"]:"";
    if (help != "" && help != nil)
	return help;

    string adv_button = parameters["mb_label"]:"";
    if (adv_button == "" || adv_button == nil)
    {
	// menu button
	adv_button = _("Ad&vanced");
    }

    if (regexpmatch (adv_button, "^.*&.*$"))
	adv_button = regexpsub (adv_button, "^(.*)&(.*)$", "\\1\\2");

    boolean save = parameters["save"]:false;
    if (save == nil)
	save = false;

    list<map<string,any> > actions_lst = parameters["actions"]:[];
    if (actions_lst == nil)
	actions_lst = [];
    integer actions = size (actions_lst);

    if (save)
	actions = actions + 1;

    if (logs > 1)
    {
	help = LogSelectionHelp ();
    }
    else if (actions >= 1 || save)
    {
	help = SingleLogHelp ();
    }
    else
    {
	return "";
    }

    if (actions >= 2)
    {
	if (save)
	{
	    help = help + AdvancedSaveHelp (adv_button);

	}
	else
	{
	    help = help + AdvancedHelp (adv_button);
	}

    }
    else if (save)
    {
	help = help + SaveHelp ();
    }

    return help;
}

/**
 * Get the combo box of the available log files
 * @param log_maps a list of maps describing all the logs
 * @return term the combo box widget
 */
define term GetLogSelectionCombo (list<map<string,any> > log_maps) {
    term selection_combo = `Empty ();
    if (size (log_maps) > 0)
    {
	integer index = -1;
	list items = maplist (map m, log_maps, {
	    index = index + 1;
	    return `item (`id (index),
		// combo box entry (only used as fallback in case
		// of error in the YaST code)
		m["log_label"]:m["command"]:m["file"]:_("Log"));
	});
	selection_combo = `ComboBox (`id (`cwm_log_files),
	    `opt (`notify, `hstretch),
	    _("&Log"),
	    items);
    }
    return selection_combo;
}

/**
 * Get the widget with the menu button with actions to be processed on the log
 * @param actions a list of all actions
 * @param save boolean true if the log should be offered to be saved
 * @param mb_label label of the menu button, may be empty for default
 * @return term widget with the menu button
 */
define term GetMenuButtonWidget (list<list> actions, boolean save,
    string mb_label)
{
    list<any> menubutton = [];
    if (save)
	// menubutton entry
	menubutton = add (menubutton, [`_cwm_log_save, _("&Save Log")]);

    if (size (actions) > 0)
    {
	integer index = 0;
	foreach (list a, actions, {
	    menubutton = add (menubutton, [index, a[0]:""]);
	    index = index + 1;
	});
    }

    if (size (menubutton) > 1)
    {
	menubutton = filter (any m, menubutton, ``(
	    is(m,list) && (((list)m)[0]:nil != nil)
	));
	menubutton = maplist (any m, menubutton, {
	    list ml = (list)m;
	    return `item (`id (ml[0]:nil), ml[1]:"");
	});
	if (mb_label == "" || mb_label == nil)
	{
	    mb_label = _("Ad&vanced");
	}
	return `MenuButton (`id (`_cwm_log_menu), mb_label, menubutton);
    }
    else if (size (menubutton) == 1)
    {
	return `PushButton (`id (menubutton[0,0]:(any)""), menubutton[0,1]:"");
    }
    return `Empty ();
}

/**
 * Get the buttons below the box with the log
 * @param popup boolean true if running in popup (and Close is needed)
 * @param glob_param a map of global parameters of the log widget
 * @param log_maps a list of maps describing all the logs
 * @return term the widget with buttons
 */
define term GetButtonsBelowLog (boolean popup, map<string,any> glob_param,
    list<map<string,any> > log_maps)
{
    term left = `Empty ();
    term center = `Empty ();
    term right = `Empty ();

    if (popup)
    {
	center = `PushButton (`id(`close), `opt (`key_F9),
	    Label::CloseButton ());

	if (haskey (glob_param, "help") && glob_param["help"]:"" != "")
	{
	    left = `PushButton (`id (`help), Label::HelpButton ());
	}
    }

    boolean save = glob_param["save"]:false;
    string mb_label = glob_param["mb_label"]:_("Ad&vanced");
    list<list> actions = glob_param["actions"]:[];
    right = GetMenuButtonWidget (actions, save, mb_label);

    return `HBox (
	`HWeight (1, left),
	`HStretch (),
	`HWeight (1, center),
	`HStretch (),
	`HWeight (1, right)
    );
}

/**
 * Get the default entry for the combo box with logs
 * @param log_maps a list of maps describing all the logs
 * @return integer the index of the default entry in the combo box
 */
define integer GetDefaultItemForLogsCombo (list<map<string,any> > log_maps) {
    integer default_log = 0;
    if (size (log_maps) > 0)
    {
	integer index = -1;
	foreach (map m, log_maps, {
	    index = index + 1;
	    if (haskey (m, "default") && default_log == 0)
		default_log = index;
	});
    }
    return default_log;
}

/**
 * Switch the displayed log
 * @param index integer index of the log to display
 */
define void LogSwitch (integer index) {
    lines = [];
    current_index = index;

    map<string, any> log_descr = Index2Descr (index);
    // logview caption
    string caption = log_descr["log_label"]:param["log_label"]:_("&Log");
    integer max_lines = GetMaxLines (log_descr);
    UI::ReplaceWidget (`_cwm_log_rp,
	`LogView (`id (`_cwm_log), caption, 15, max_lines));

    InitLogReading (index);
    FillWidgetWithData (index);
}

/**
 * Initialize the displayed log
 * @param key log widget key
 * @param key table widget key
 */
global define void LogInit (string key) {
    param = CWM::GetProcessedWidget ();
    current_index = param["_cwm_default_index"]:0;
    mb_actions = param["_cwm_button_actions"]:[];
    logs = param["_cwm_log_files"]:[];
    if (UI::WidgetExists (`id (`cwm_log_files)))
	UI::ChangeWidget (`id (`cwm_log_files), `value, current_index);
    LogSwitch (current_index);
}

/**
 * Handle the event on the log view widget
 * @param key log widget key
 * @param event map event to handle
 * @return symbol always nil
 */
global define symbol LogHandle (string key, map event) {
    param = CWM::GetProcessedWidget ();
    map<string,any> log = Index2Descr (current_index);
    integer max_lines = GetMaxLines (log);
    integer count = (integer)SCR::Read (.background.newlines);
    if (count > 0)
    {
	list<string> new_lines = (list<string>)
	    SCR::Read (.background.newout);
	foreach (string l, new_lines, {
	    UI::ChangeWidget (`id (`_cwm_log), `LastLine, l + "\n");
	});
	lines = (list<string>) merge (lines, new_lines);
	lines = DeleteOldLines (lines, max_lines);
    }
    any ret = event["ID"]:nil;
    // save the displayed log to file
    if (ret == `_cwm_log_save)
    {
	string filename = UI::AskForSaveFileName(
	    // popup caption
	    "/tmp", "*.log", _("Save Log as..."));
	if (filename != nil)
	{
	    SCR::Write (.target.string, filename,
		mergestring (lines, "\n") + "\n");
	}
    }
    // other operation specified by user
    else if (ret != nil && is (ret, integer))
    {
	integer iret = (integer)ret;
	void() func = (void())(mb_actions[iret, 1]:nil);
	if (func != nil)
	    func ();
	if (mb_actions[iret, 2]:nil == true)
	{
	    SCR::Execute (.background.kill);
	    UI::ChangeWidget (`id (`_cwm_log), `Value, "");
	    InitLogReading (current_index);
	}
    }
    // switch displayed log file
    else if (ret == `cwm_log_files)
    {
	integer index = (integer)
	    UI::QueryWidget (`id (`cwm_log_files), `Value);
	LogSwitch (index);
    }
    return nil;
}

/**
 * Get the map with the log view widget
 * @param parameters map parameters of the widget to be created, will be
 *  unioned with the generated map
 * <pre>
 *  - "save" -- boolean, if true, then log saving is possible
 *  - "actions" -- list, allows to specify additional actions.
 *                 Each member is a 2- or 3-entry list, first entry is a
 *                 label for the menubutton, the second one is a function
 *                 that will be called when the entry is selected,
 *                 the signature of the function must be void(),
 *			optional 3rd argument, if set to true, forces
 *			restarting of the log displaying command after the
 *			action is performed
 *  - "mb_label" -- string, label of the menubutton, if not specified,
 *                  then "Advanced" is used
 *  - "max_lines" -- integer, maximum of lines to be displayed. If 0,
 *                   then display whole file. Default is 100.
 *  - "help" -- string for a rich text, help to be offered via a popup
 *              when user clicks the "Help" button. If not present,
 *              default help is shown or Help button is hidden.
 * - "widget_height" -- height of the LogView widget, to be adjusted
 *                      so that the widget fits into the dialog well.
 *                      Test it to find the best value, 15 seems to be
 *                      good value (is default if not specified)
 * </pre>
 * @param log_files a list of logs that will be displayed
 * <pre>
 *  - "file" -- string, filename with the log
 *  - "grep" -- string, basic regular expression to be grepped
 *              in the log (for getting relevant  parts of
 *              /var/log/messages. If empty or not present, whole file
 *              is used
 *  - "command" -- allows to specify comand to get the log for cases
 *                 where grep isn't enough. If used, file and grep entries
 *                 are ignored
 *  - "log_label" -- header of the LogView widget, if not set, then the file
 *                   name or the command is used
 *  - "default" -- define and set to true to make this log be active after
 *                 widget is displayed. If not defiend for any log, the
 *                 first log is automatically default. If defined for multiple
 *                 logs, the first one is active
 * </pre>
 * @return map the log widget
 */
global define map CreateWidget (map<string,any> parameters,
    list<map<string,any> > log_files)
{
    // logview caption
    string caption = param["log_label"]:_("&Log");
    integer max_lines = param["max_lines"]:max_lines_default;
    integer height = param["widget_height"]:15;

    integer default_index = GetDefaultItemForLogsCombo (log_files);
    term top_bar = GetLogSelectionCombo (log_files);
    term bottom_bar = GetButtonsBelowLog (false, parameters, log_files);

    return union (
	$[
	    "widget" : `custom,
	    "custom_widget" : `VBox (
		top_bar,
		`ReplacePoint (`id (`_cwm_log_rp),
		    `LogView (`id (`_cwm_log), caption, height, max_lines)
		),
		bottom_bar
	    ),
	    "init" : LogInit,
	    "handle" : LogHandle,
	    "cleanup" : KillBackgroundProcess,
	    "ui_timeout" : 1000,
	    "_cwm_default_index" : default_index,
	    "_cwm_log_files" : log_files,
	    "_cwm_button_actions" : [],
	    "help" : CreateHelp (size (log_files), parameters),
	],
	parameters
    );
}


// old functions for displaying log as a popup


/**
 * Main function for displaying logs
 * @param parameters map description of parameters, with following keys
 * <pre>
 *  - "file" -- string, filename with the log
 *  - "grep" -- string, basic regular expression to be grepped
 *              in the log (for getting relevant  parts of
 *              /var/log/messages. If empty or not present, whole file
 *              is used
 *  - "command" -- allows to specify comand to get the log for cases
 *                 where grep isn't enough. If used, file and grep entries
 *                 are ignored
 *  - "save" -- boolean, if true, then log saving is possible
 *  - "actions" -- list, allows to specify additional actions.
 *                 Each member is a 2- or 3-entry list, first entry is a
 *                 label for the menubutton, the second one is a function
 *                 that will be called when the entry is selected,
 *                 the signature of the function must be void(),
 *			optional 3rd argument, if set to true, forces
 *			restarting of the log displaying command after the
 *			action is performed
 *  - "help" -- string for a rich text, help to be offered via a popup
 *              when user clicks the "Help" button. If not present,
 *              Help button isn't shown
 *  - "mb_label" -- string, label of the menubutton, if not specified,
 *                  then "Advanced" is used
 *  - "max_lines" -- integer, maximum of lines to be displayed. If 0,
 *                   then display whole file. Default is 100.
 *  - "log_label" -- header of the LogView widget, if not set, then "Log"
 *                   is used
 * </pre>
 */
global define void Display (map<string, any> parameters) {
	param = parameters;

	// menubutton
	string mb_label = param["mb_label"]:_("Ad&vanced");
	integer max_lines = param["max_lines"]:max_lines_default;
	string log_label = param["log_label"]:_("&Log");

	logs = [param];

	InitLogReading (0);

	term button_line = GetButtonsBelowLog (true, param, [param]);

        UI::OpenDialog (`HBox (`HSpacing (1), `VBox (
            `VSpacing (1),
            `HSpacing (70),
            // log view header
            `LogView (`id (`_cwm_log), log_label, 19, max_lines),
            `VSpacing (1),
	    button_line,
            `VSpacing (1)), `HSpacing (1)));

	if (param["help"]:"" != "")
	{
	    UI::ReplaceWidget (`id (`rep_left),
		`PushButton (`id (`help), Label::HelpButton ()));
	}
	mb_actions = param["actions"]:[];

	FillWidgetWithData (0);

        any ret = nil;
        while (ret != `close && ret != `cancel)
	{
	    map event = (map)UI::WaitForEvent (1000);
	    ret = event["ID"]:nil;
	    if (ret == `help)
	    {
		UI::OpenDialog (`VBox (
		    `RichText (`id (`help_text), param["help"]:""),
		    `HBox (
			`HStretch (),
			`PushButton (`id (`close), Label::CloseButton ()),
			`HStretch ()
		    )
		));
		while (ret != `close && ret != `cancel)
		    ret = UI::UserInput ();
		ret = nil;
		UI::CloseDialog ();
	    }
	    else
	    {
		LogHandle ("", event);
	    }
	}
	SCR::Execute (.background.kill);
        UI::CloseDialog ();
        return;
}

/**
 * Display specified file, list 100 lines
 * @param file string filename of file with the log
 */
global define void DisplaySimple (string file) {
    Display ($[ "file" : file ]);
}

/**
 * Display log with filtering with 100 lines
 * @param file string filename of file with the log
 * @param grep string basic regular expression to be grepped in file
 */
global define void DisplayFiltered (string file, string grep) {
    Display ($[ "file" : file, "grep" : grep ]);
}


}
