// $Id$ -*- C++ -*-
// Create a plotter interface

// Copyright (C) 1998 Technische Universitaet Braunschweig, Germany.
// Copyright (C) 2001, 2003 Free Software Foundation, Inc.
// Written by Andreas Zeller <zeller@gnu.org>.
// 
// This file is part of DDD.
// 
// DDD 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.
// 
// DDD 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 DDD -- see the file COPYING.
// If not, see <http://www.gnu.org/licenses/>.
// 
// DDD is the data display debugger.
// For details, see the DDD World-Wide-Web page, 
// `http://www.gnu.org/software/ddd/',
// or send a mail to the DDD developers <ddd@gnu.org>.

char plotter_rcsid[] = 
    "$Id$";

#include "plotter.h"

#include "assert.h"
#include "charsets.h"
#include "cook.h"
#include "ddd.h"
#include "exit.h"
#include "findParent.h"
#include "findWindow.h"
#include "file.h"
#include "filetype.h"
#include "fonts.h"
#include "post.h"
#include "print.h"
#include "regexps.h"
#include "simpleMenu.h"
#include "status.h"
#include "strclass.h"
#include "string-fun.h"
#include "tempfile.h"
#include "verify.h"
#include "version.h"
#include "wm.h"
#include "AppData.h"
#include "Command.h"
#include "Delay.h"
#include "DeleteWCB.h"
#include "HelpCB.h"
#include "MakeMenu.h"
#include "PlotArea.h"
#include "Swallower.h"
#include "DispValue.h"
#include "DataDisp.h"
#include "DestroyCB.h"
#include "TimeOut.h"

#include <stdio.h>
#include <fstream>

#include <Xm/Command.h>
#include <Xm/MainW.h>
#include <Xm/MessageB.h>
#include <Xm/AtomMgr.h>
#include <Xm/FileSB.h>
#include <Xm/DrawingA.h>
#include <Xm/ScrolledW.h>
#include <Xm/SelectioB.h>
#include <Xm/ScrollBar.h>
#include <Xm/Text.h>
#include <Xm/TextF.h>
#include <Xm/ToggleB.h>

static void TraceInputHP (Agent *source, void *, void *call_data);
static void TraceOutputHP(Agent *source, void *, void *call_data);
static void TraceErrorHP (Agent *source, void *, void *call_data);
static void SetStatusHP  (Agent *source, void *, void *call_data);
static void PlotterNotFoundHP(Agent *source, void *, void *call_data);

static void CancelPlotCB(Widget, XtPointer, XtPointer);
static void ExposePlotAreaCB(Widget, XtPointer, XtPointer);
static void ResizePlotAreaCB(Widget, XtPointer, XtPointer);

static void SelectPlotCB(Widget, XtPointer, XtPointer);
static void SelectAndPrintPlotCB(Widget, XtPointer, XtPointer);

static void ReplotCB(Widget, XtPointer, XtPointer);
static void PlotCommandCB(Widget, XtPointer, XtPointer);
static void ExportPlotCB(Widget, XtPointer, XtPointer);

static void ToggleOptionCB(Widget, XtPointer, XtPointer);
static void ToggleLogscaleCB(Widget, XtPointer, XtPointer);
static void SetStyleCB(Widget, XtPointer, XtPointer);
static void SetContourCB(Widget, XtPointer, XtPointer);
static void SetViewCB(Widget, XtPointer, XtPointer);
static void SwallowCB(Widget, XtPointer, XtPointer);

struct PlotWindowInfo {
    DispValue *source;		// The source we depend upon
    string window_name;		// The window name
    PlotAgent *plotter;		// The current Gnuplot instance
    PlotArea *area;		// The area we're drawing in
    Widget shell;		// The shell we're in
    Widget working_dialog;	// The working dialog
    Widget swallower;		// The Gnuplot window
    Widget vsb;			// Vertical scroll bar
    Widget hsb;			// Horizontal scroll bar
    Widget command;		// Command widget
    Widget command_dialog;      // Command dialog
    Widget export_dialog;       // Export dialog
    bool active;		// True if popped up
    XtIntervalId swallow_timer;	// Wait for Window creation

    string settings;		 // Plot settings
    XtIntervalId settings_timer; // Wait for settings
    string settings_file;	 // File to get settings from
    StatusDelay *settings_delay; // Delay while getting settings

    // Constructor - just initialize
    PlotWindowInfo()
	: source(0), window_name(""),
	  plotter(0), area(0), shell(0), working_dialog(0), swallower(0),
	  vsb(0), hsb(0), command(0), command_dialog(0), 
	  export_dialog(0), active(false), swallow_timer(0), 
	  settings(""), settings_timer(0), settings_file(""), settings_delay(0)
    {}

private:
    PlotWindowInfo(const PlotWindowInfo&);
    PlotWindowInfo& operator=(const PlotWindowInfo&);
};


//-------------------------------------------------------------------------
// Menus
//-------------------------------------------------------------------------

static MMDesc file_menu[] = 
{
    { "command", MMPush, { PlotCommandCB, 0 }, 0, 0, 0, 0 },
    MMSep,
    { "replot",  MMPush, { ReplotCB, 0 }, 0, 0, 0, 0 },
    { "print",   MMPush, { SelectAndPrintPlotCB, 0 }, 0, 0, 0, 0 },
    { "export",  MMPush, { ExportPlotCB, 0 }, 0, 0, 0, 0 },
    MMSep,
    { "close",   MMPush, { CancelPlotCB, 0 }, 0, 0, 0, 0 },
    { "exit",    MMPush, { DDDExitCB, XtPointer(EXIT_SUCCESS) }, 0, 0, 0, 0},
    MMEnd
};

static MMDesc view_menu[] = 
{
    { "border",    MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    { "time",      MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    MMSep,
    { "grid",      MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    { "xzeroaxis", MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    { "yzeroaxis", MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    MMEnd
};

static MMDesc contour_menu[] = 
{
    { "base",      MMToggle, { SetContourCB, 0 }, 0, 0, 0, 0 },
    { "surface",   MMToggle, { SetContourCB, 0 }, 0, 0, 0, 0 },
    MMEnd
};

static MMDesc scale_menu[] = 
{
    { "logscale",  MMToggle, { ToggleLogscaleCB, 0 }, 0, 0, 0, 0 },
    MMSep,
    { "xtics",     MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    { "ytics",     MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    { "ztics",     MMToggle, { ToggleOptionCB, 0 }, 0, 0, 0, 0 },
    MMEnd
};

static MMDesc plot_menu[] = 
{
    { "points",         MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "lines",          MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "lines3d",        MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "linespoints",    MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "linespoints3d",  MMToggle | MMUnmanaged, 
                                  { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "impulses",       MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "dots",           MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "steps2d",        MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    { "boxes2d",        MMToggle, { SetStyleCB, 0 }, 0, 0, 0, 0 },
    MMEnd
};

static MMDesc menubar[] = 
{
    { "file",     MMMenu,          MMNoCB, file_menu,        0, 0, 0 },
    { "edit",     MMMenu,          MMNoCB, simple_edit_menu, 0, 0, 0 },
    { "plotView", MMMenu,          MMNoCB, view_menu,        0, 0, 0 },
    { "plot",     MMRadioMenu,     MMNoCB, plot_menu,        0, 0, 0 },
    { "scale",    MMMenu,          MMNoCB, scale_menu,       0, 0, 0 },
    { "contour",  MMMenu,          MMNoCB, contour_menu,     0, 0, 0 },
    { "help",     MMMenu | MMHelp, MMNoCB, simple_help_menu, 0, 0, 0 },
    MMEnd
};


static void configure_plot(PlotWindowInfo *plot);


//-------------------------------------------------------------------------
// Plotter commands
//-------------------------------------------------------------------------

static void send(PlotWindowInfo *plot, const string& cmd)
{
    data_disp->select(plot->source);
    plot->plotter->write(cmd.chars(), cmd.length());
}

static void send_and_replot(PlotWindowInfo *plot, string cmd)
{
    if (cmd.matches(rxwhite))
	return;

    if (!cmd.contains('\n', -1))
	cmd += "\n";
    if (cmd.contains("help", 0))
	cmd += "\n";		// Exit `help'
    else
	cmd += "replot\n";

    if (plot->area != 0)
	plot->area->plot_pending();

    send(plot, cmd);
}


//-------------------------------------------------------------------------
// Set up menu
//-------------------------------------------------------------------------

static void slurp_file(const string& filename, string& target)
{
    std::ifstream is(filename.chars());
    if (is.bad())
    {
	target = "";
	return;
    }

    std::ostringstream s;
    int c;
    while ((c = is.get()) != EOF)
	s << (unsigned char)c;

    target = s;
}

static void GetPlotSettingsCB(XtPointer client_data, XtIntervalId *id)
{
    (void) id;			// Use it
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    assert(plot->settings_timer == *id);
    plot->settings_timer = 0;

    // Check for settings file to be created
    string settings;
    slurp_file(plot->settings_file, settings);

    if (settings.contains("set zero"))
    {
	// Settings are complete
	unlink(plot->settings_file.chars());
	plot->settings = settings;

	configure_plot(plot);

	delete plot->settings_delay;
	plot->settings_delay = 0;
    }
    else
    {
	// Try again in 500 ms
	plot->settings_timer = 
	    XtAppAddTimeOut(XtWidgetToApplicationContext(plot->shell), 500, 
			    GetPlotSettingsCB, XtPointer(plot));
    }
}

static void configure_options(PlotWindowInfo *plot, MMDesc *menu, 
			      const string& settings)
{
    for (int i = 0; menu[i].name != 0; i++)
    {
	if ((menu[i].type & MMTypeMask) != MMToggle)
	    continue;

	string name = menu[i].name;

	const string s1 = "*" + name;
	Widget w = XtNameToWidget(plot->shell, s1.chars());
	XtCallbackProc callback = menu[i].callback.callback;

	bool set = false;
	if (callback == ToggleOptionCB)
	{
	    set = settings.contains("\nset " + name + "\n");
	}
	else if (callback == SetContourCB)
	{
	    if (name == "base")
		set = settings.contains("\nset contour base\n") ||
		    settings.contains("\nset contour both\n");
	    else if (name == "surface")
		set = settings.contains("\nset contour surface\n") ||
		    settings.contains("\nset contour both\n");
	}
	else if (callback == ToggleLogscaleCB)
	{
	    set = settings.contains("\nset logscale ");
	}

	XmToggleButtonSetState(w, set, False);
    }
}

static void configure_plot(PlotWindowInfo *plot)
{
    if (plot->plotter == 0)
	return;

    int ndim = plot->plotter->dimensions();

    // Set up plot menu
    int i;
    for (i = 0; plot_menu[i].name != 0; i++)
    {
	if ((plot_menu[i].type & MMTypeMask) != MMToggle)
	    continue;

	string name = plot_menu[i].name;

	const string s1 = "*" + name;
	Widget w = XtNameToWidget(plot->shell, s1.chars());

	if (name.contains("2d", -1))
	    XtSetSensitive(w, ndim == 2);
	else if (name.contains("3d", -1))
	    XtSetSensitive(w, ndim >= 3);
	else
	    XtSetSensitive(w, ndim >= 2);
    }

    // Log scale is available only iff all values are non-negative
    Widget logscale = XtNameToWidget(plot->shell, "*logscale");
    XtSetSensitive(logscale, plot->plotter->min_v() >= 0.0);

    // Axes can be toggled in 2d mode only
    Widget xzeroaxis = XtNameToWidget(plot->shell, "*xzeroaxis");
    Widget yzeroaxis = XtNameToWidget(plot->shell, "*yzeroaxis");
    XtSetSensitive(xzeroaxis, ndim <= 2);
    XtSetSensitive(yzeroaxis, ndim <= 2);

    // Z Tics are available in 3d mode only
    Widget ztics = XtNameToWidget(plot->shell, "*ztics");
    XtSetSensitive(ztics, ndim >= 3);

    // Contour drawing is available in 3d mode only
    Widget base    = XtNameToWidget(plot->shell, "*base");
    Widget surface = XtNameToWidget(plot->shell, "*surface");
    XtSetSensitive(base,    ndim >= 3);
    XtSetSensitive(surface, ndim >= 3);

    // Set scrollbars
    manage_child(plot->hsb, ndim >= 3);
    manage_child(plot->vsb, ndim >= 3);

    // Check if we can export something
    bool have_source = false;
    bool can_export  = false;
    const StringArray& sources = plot->plotter->data_files();
    for (i = 0; i < sources.size(); i++)
    {
	if (!sources[i].empty())
	{
	    if (have_source)
		can_export  = false; // Multiple source files
	    else
		can_export = have_source = true;
	}
    }

    Widget export_w = XtNameToWidget(plot->shell, "*export");
    set_sensitive(export_w, can_export);

    // The remainder requires settings
    if (plot->settings.empty())
    {
	// No settings yet
	if (plot->settings_timer == 0)
	{
	    plot->settings_delay = 
		new StatusDelay("Retrieving Plot Settings");

	    // Save settings...
	    plot->settings_file = tempfile();
	    string cmd = "save " + quote(plot->settings_file) + "\n";
	    send(plot, cmd);

	    // ...and try again in 250ms
	    plot->settings_timer = 
		XtAppAddTimeOut(XtWidgetToApplicationContext(plot->shell), 250,
				GetPlotSettingsCB, XtPointer(plot));

	    // Set initial scrollbar defaults
	    XtVaSetValues(plot->vsb, XmNvalue, 60, XtPointer(0));
	    XtVaSetValues(plot->hsb, XmNvalue, 30, XtPointer(0));
	}

	return;
    }

    configure_options(plot, view_menu,    plot->settings);
    configure_options(plot, contour_menu, plot->settings);
    configure_options(plot, scale_menu,   plot->settings);

    // Get style
    for (i = 0; plot_menu[i].name != 0; i++)
    {
	if ((plot_menu[i].type & MMTypeMask) != MMToggle)
	    continue;

	string name = plot_menu[i].name;

	const string s1 = "*" + name;
	Widget w = XtNameToWidget(plot->shell, s1.chars());

	bool set = plot->settings.contains("\nset data style " + name + "\n");
	XmToggleButtonSetState(w, set, False);
    }

    // Get position
    int rot_x = 60;
    int rot_z = 30;

    int view_index = plot->settings.index("set view ");
    if (view_index >= 0)
    {
	// `set view <rot_x> {,{<rot_z>}{,{<scale>}{,<scale_z>}}}'
	string view_setting = plot->settings.after("set view ");
	rot_x = atoi(view_setting.chars());
	view_setting = view_setting.after(", ");
	rot_z = atoi(view_setting.chars());
    }

    XtVaSetValues(plot->vsb, XmNvalue, rot_x, XtPointer(0));
    XtVaSetValues(plot->hsb, XmNvalue, rot_z, XtPointer(0));
}



//-------------------------------------------------------------------------
// Decoration stuff
//-------------------------------------------------------------------------

// Start plot
static void popup_plot_shell(PlotWindowInfo *plot)
{
    if (!plot->active && plot->plotter != 0)
    {
	// We have the plot
	plot->plotter->removeHandler(Died, PlotterNotFoundHP, 
				     (void *)plot);

	// Fetch plot settings
	configure_plot(plot);

	// Command and export dialogs are not needed (yet)
	if (plot->command_dialog != 0)
	    XtUnmanageChild(plot->command_dialog);
	if (plot->export_dialog != 0)
	    XtUnmanageChild(plot->export_dialog);

	// Pop down working dialog
	if (plot->working_dialog != 0)
	    XtUnmanageChild(plot->working_dialog);

	// Pop up shell
	XtSetSensitive(plot->shell, True);
	XtPopup(plot->shell, XtGrabNone);
	wait_until_mapped(plot->shell);

	plot->active = true;
    }
}

// Swallow WINDOW
static void swallow(PlotWindowInfo *plot, Window window)
{
    assert(window != None);

    // We have the window
    XtRemoveCallback(plot->swallower, XtNwindowCreatedCallback, 
		     SwallowCB, XtPointer(plot));

    if (plot->swallow_timer != 0)
    {
	XtRemoveTimeOut(plot->swallow_timer);
	plot->swallow_timer = 0;
    }

    XtVaSetValues(plot->swallower, XtNwindow, window, XtPointer(0));

    popup_plot_shell(plot);
}

// Swallow new GNUPLOT window; search from window created on root.
static void SwallowCB(Widget swallower, XtPointer client_data, 
		      XtPointer call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    assert(plot->swallower == swallower);

    SwallowerInfo *info = (SwallowerInfo *)call_data;

    Window root = info->window;
    Window window = None;
    Display *display = XtDisplay(swallower);

    // Try the exact name as given
    if (window == None)
	window = findWindow(display, root, plot->window_name.chars());

    // Try the capitalized name.  Gnuplot does this.
    if (window == None) {
        const string s1 = capitalize(plot->window_name);
	window = findWindow(display, root, s1.chars());
    }

    // Try any `Gnuplot' window just created
    if (window == None)
	window = findWindow(display, root, app_data.plot_window_class);

    if (window != None)
	swallow(plot, window);
}

// Swallow new GNUPLOT window; search from root window (expensive).
static void SwallowTimeOutCB(XtPointer client_data, XtIntervalId *id)
{
    (void) id;

    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    assert(*id == plot->swallow_timer);
    plot->swallow_timer = 0;

    Window root = RootWindowOfScreen(XtScreen(plot->swallower));
    Window window = None;
    Display *display = XtDisplay(plot->swallower);

    // Try the exact name as given
    if (window == None)
	window = findWindow(display, root, plot->window_name.chars());

    // Try the capitalized name.  Gnuplot does this.
    if (window == None) {
        const string s1 = capitalize(plot->window_name);
	window = findWindow(display, root, s1.chars());
    }

    if (window == None)
    {
	// Try again later
	plot->swallow_timer = 
	    XtAppAddTimeOut(XtWidgetToApplicationContext(plot->swallower),
			    app_data.plot_window_delay, 
			    SwallowTimeOutCB, XtPointer(plot));
    }

    if (window != None)
	swallow(plot, window);
}

// Swallow again after window has gone.  This happens while printing.
static void SwallowAgainCB(Widget swallower, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    assert(plot->swallower == swallower);

    XtAddCallback(swallower, XtNwindowCreatedCallback, SwallowCB, 
		  XtPointer(plot));

    if (plot->swallow_timer != 0)
	XtRemoveTimeOut(plot->swallow_timer);

    plot->swallow_timer = 
	XtAppAddTimeOut(XtWidgetToApplicationContext(plot->swallower),
			app_data.plot_window_delay, 
			SwallowTimeOutCB, XtPointer(plot));
}


// Cancel plot
static void popdown_plot_shell(PlotWindowInfo *plot)
{
    static bool entered = false;
    if (entered)
	return;

    entered = true;

    // Manage dialogs
    if (plot->working_dialog != 0)
	XtUnmanageChild(plot->working_dialog);
    if (plot->command_dialog != 0)
	XtUnmanageChild(plot->command_dialog);
    if (plot->export_dialog != 0)
	XtUnmanageChild(plot->export_dialog);

    if (plot->shell != 0)
    {
	XWithdrawWindow(XtDisplay(plot->shell), XtWindow(plot->shell),
			XScreenNumberOfScreen(XtScreen(plot->shell)));
	XtPopdown(plot->shell);

	// XtPopdown may not withdraw an iconified shell.  Hence, make
	// sure the shell really becomes disabled.
	XtSetSensitive(plot->shell, False);
    }

    // Manage settings
    if (plot->settings_timer != 0)
    {
	// Still waiting for settings
	XtRemoveTimeOut(plot->settings_timer);
	plot->settings_timer = 0;

	unlink(plot->settings_file.chars());
    }

    if (plot->settings_delay != 0)
    {
	plot->settings_delay->outcome = "canceled";
	delete plot->settings_delay;
	plot->settings_delay = 0;
    }

    plot->settings = "";

    plot->active = false;

    entered = false;
}

static void CancelPlotCB(Widget, XtPointer client_data, XtPointer)
{
    static bool entered = false;
    if (entered)
	return;

    entered = true;

    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    popdown_plot_shell(plot);

    if (plot->swallower != 0)
    {
	// Don't wait for window to swallow
	XtRemoveAllCallbacks(plot->swallower, XtNwindowCreatedCallback);
	XtRemoveAllCallbacks(plot->swallower, XtNwindowGoneCallback);
    }

    if (plot->swallow_timer != 0)
    {
	XtRemoveTimeOut(plot->swallow_timer);
	plot->swallow_timer = 0;
    }

    if (plot->plotter != 0)
    {
	// Terminate plotter
	plot->plotter->removeHandler(Died, PlotterNotFoundHP, client_data);
	plot->plotter->terminate();
	plot->plotter = 0;
    }

    entered = false;
}

static void DeletePlotterCB(XtPointer client_data, XtIntervalId *)
{
    Agent *plotter = (Agent *)client_data;
    delete plotter;
}

static void DeletePlotterHP(Agent *plotter, void *client_data, void *)
{
    // Plotter has died - delete memory
    XtAppAddTimeOut(XtWidgetToApplicationContext(gdb_w), 0, 
		    DeletePlotterCB, XtPointer(plotter));

    plotter->removeHandler(Died, DeletePlotterHP, client_data);

    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    assert(plot->plotter == 0 || plot->plotter == plotter);
    plot->plotter = 0;
    popdown_plot_shell(plot);
}

static void GetPlotHP(Agent *, void *client_data, void *call_data)
{
    // We got the plot commands
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    // Pass commands to the plot area
    DataLength *dl = (DataLength *)call_data;
    plot->area->plot(dl->data, dl->length);

    // Popup shell
    popup_plot_shell(plot);
}

static void PlotterNotFoundHP(Agent *plotter, void *client_data, void *)
{
#if !NDEBUG
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    assert(plot->plotter == 0 || plot->plotter == plotter);
#endif

    plotter->removeHandler(Died, PlotterNotFoundHP, client_data);

    string base = app_data.plot_command;
    if (base.contains(' '))
	base = base.before(' ');

    Arg args[10];
    Cardinal arg = 0;
    MString msg = rm( capitalize(base) + " could not be started.");
    XtSetArg(args[arg], XmNmessageString, msg.xmstring()); arg++;
    Widget dialog = 
	verify(XmCreateErrorDialog(find_shell(),
				   XMST("no_plotter_dialog"), args, arg));
    XtUnmanageChild(XmMessageBoxGetChild
		    (dialog, XmDIALOG_CANCEL_BUTTON));
    XtAddCallback(dialog, XmNhelpCallback, ImmediateHelpCB, XtPointer(0));

    Delay::register_shell(dialog);
    manage_and_raise(dialog);
}


#define SWALLOWER_NAME "swallower"
#define PLOT_AREA_NAME "area"

static VoidArray plot_infos;

static PlotWindowInfo *new_decoration(const string& name)
{
    PlotWindowInfo *plot = 0;

    // Check whether we can reuse an existing decoration
    for (int i = 0; i < plot_infos.size(); i++)
    {
	PlotWindowInfo *info = (PlotWindowInfo *)plot_infos[i];
	if (info->plotter == 0)
	{
	    // Shell is unused - use this one
	    plot = info;
	    break;
	}
    }

    if (plot == 0)
    {
	plot = new PlotWindowInfo;

	// Create decoration windows
	Arg args[10];
	Cardinal arg = 0;
	XtSetArg(args[arg], XmNallowShellResize, True);       arg++;
	XtSetArg(args[arg], XmNdeleteResponse, XmDO_NOTHING); arg++;

	// Mark shell as `used'
	XtSetArg(args[arg], XmNuserData, XtPointer(True)); arg++;
	plot->shell = verify(XtCreateWidget("plot", topLevelShellWidgetClass,
					    find_shell(), args, arg));

	AddDeleteWindowCallback(plot->shell, CancelPlotCB, XtPointer(plot));

	arg = 0;
	Widget main_window = XmCreateMainWindow(plot->shell, 
						XMST("main_window"), 
						args, arg);
	XtManageChild(main_window);

	MMcreateMenuBar(main_window, "menubar", menubar);
	MMaddCallbacks(file_menu,    XtPointer(plot));
	MMaddCallbacks(simple_edit_menu);
	MMaddCallbacks(view_menu,    XtPointer(plot));
	MMaddCallbacks(plot_menu,    XtPointer(plot));
	MMaddCallbacks(scale_menu,   XtPointer(plot));
	MMaddCallbacks(contour_menu, XtPointer(plot));
	MMaddCallbacks(simple_help_menu);
	MMaddHelpCallback(menubar, ImmediateHelpCB);

	arg = 0;
	XtSetArg(args[arg], XmNscrollingPolicy, XmAPPLICATION_DEFINED); arg++;
	XtSetArg(args[arg], XmNvisualPolicy,    XmVARIABLE);            arg++;
	Widget scroll = 
	    XmCreateScrolledWindow(main_window, XMST("scroll"), args, arg);
	XtManageChild(scroll);

	// Create work window
	Widget work;
	string plot_term_type = downcase(app_data.plot_term_type);
	if (plot_term_type.contains("xlib", 0))
	{
	    // xlib type - create plot area to draw plot commands
	    arg = 0;
	    work = XmCreateDrawingArea(scroll, 
				       XMST(PLOT_AREA_NAME), args, arg);
	    XtManageChild(work);

	    plot->area = 
		new PlotArea(work, make_font(app_data, FixedWidthDDDFont));
	    XtVaSetValues(work, XmNuserData, XtPointer(plot->area), 
			  XtPointer(0));
	}
	else if (plot_term_type.contains("x11", 0))
	{
	    // x11 type - swallow Gnuplot window
	    arg = 0;
	    work = plot->swallower = 
		XtCreateManagedWidget(SWALLOWER_NAME, swallowerWidgetClass, 
				      scroll, args, arg);
	}
	else
	{
	    // Unknown terminal type
	    post_error("Unknown plot terminal type " + 
		       quote(app_data.plot_term_type),
		       "unknown_plot_term_type_error");
	    return 0;
	}

	// Create scroll bars
	const int slider_size = 20;

	arg = 0;
	XtSetArg(args[arg], XmNorientation, XmHORIZONTAL);      arg++;
	XtSetArg(args[arg], XmNminimum,     0);                 arg++;
	XtSetArg(args[arg], XmNmaximum,     360 + slider_size); arg++;
	plot->hsb = XmCreateScrollBar(scroll, XMST("hsb"), args, arg);
	XtManageChild(plot->hsb);

	arg = 0;
	XtSetArg(args[arg], XmNorientation, XmVERTICAL);        arg++;
	XtSetArg(args[arg], XmNminimum,     0);                 arg++;
	XtSetArg(args[arg], XmNmaximum,     180 + slider_size); arg++;
	plot->vsb = XmCreateScrollBar(scroll, XMST("vsb"), args, arg);
	XtManageChild(plot->vsb);

	XtAddCallback(plot->hsb, XmNvalueChangedCallback,
		      SetViewCB, XtPointer(plot));
	XtAddCallback(plot->vsb, XmNvalueChangedCallback,
		      SetViewCB, XtPointer(plot));

#if 0
	XtAddCallback(plot->hsb, XmNdragCallback, SetViewCB, XtPointer(plot));
	XtAddCallback(plot->vsb, XmNdragCallback, SetViewCB, XtPointer(plot));
#endif

	XmScrolledWindowSetAreas(scroll, plot->hsb, plot->vsb, work);

	Delay::register_shell(plot->shell);
	InstallButtonTips(plot->shell);

	plot_infos += plot;
    }

    string title = DDD_NAME ": " + name;
    XtVaSetValues(plot->shell,
		  XmNtitle, title.chars(),
		  XmNiconName, title.chars(),
		  XtPointer(0));

    if (plot->swallower != 0)
    {
	XtRemoveAllCallbacks(plot->swallower, XtNwindowCreatedCallback);
	XtAddCallback(plot->swallower, XtNwindowCreatedCallback,
		      SwallowCB, XtPointer(plot));

	XtRemoveAllCallbacks(plot->swallower, XtNwindowGoneCallback);
	XtAddCallback(plot->swallower, XtNwindowGoneCallback, 
		      SwallowAgainCB, XtPointer(plot));

	if (plot->swallow_timer != 0)
	    XtRemoveTimeOut(plot->swallow_timer);

	plot->swallow_timer = 
	    XtAppAddTimeOut(XtWidgetToApplicationContext(plot->swallower),
			    app_data.plot_window_delay, SwallowTimeOutCB, 
			    XtPointer(plot));
    }

    plot->active = false;

    return plot;
}

// Remove all unused decorations from cache
void clear_plot_window_cache()
{
    for (int i = 0; i < plot_infos.size(); i++)
    {
	PlotWindowInfo *info = (PlotWindowInfo *)plot_infos[i];
	if (info->plotter == 0)
	{
	    // Shell is unused -- destroy it
	    XtDestroyWidget(info->shell);
	    info->shell = 0;
	}
	else
	{
	    // A running shell should be destroyed after invocation.
	    // (FIXME)
	}
    }

    static const VoidArray empty;
    plot_infos = empty;
}


// Create a new plot window
PlotAgent *new_plotter(const string& name, DispValue *source)
{
    static int tics = 1;

    string cmd = app_data.plot_command;
    cmd.gsub("@FONT@", make_font(app_data, FixedWidthDDDFont));

    string window_name = ddd_NAME "plot" + itostring(tics++);
    if (cmd.contains("@NAME@"))
	cmd.gsub("@NAME@", window_name);
    else
	cmd += " -name " + window_name;

    // Create shell
    PlotWindowInfo *plot = new_decoration(name);
    if (plot == 0)
	return 0;

    plot->source      = source;
    plot->window_name = window_name;
    XtVaSetValues(plot->shell, XmNuserData, XtPointer(True), XtPointer(0));

    // Pop up a working dialog
    static Widget dialog = 0;
    if (dialog == 0)
    {
	Arg args[10];
	Cardinal arg = 0;
	dialog = verify(XmCreateWorkingDialog(find_shell(),
					      XMST("launch_plot_dialog"), 
					      args, arg));
	XtUnmanageChild(XmMessageBoxGetChild(dialog,
					     XmDIALOG_OK_BUTTON));
	XtUnmanageChild(XmMessageBoxGetChild(dialog,
					     XmDIALOG_HELP_BUTTON));
    }

    XtRemoveAllCallbacks(dialog, XmNcancelCallback);
    XtAddCallback(dialog, XmNcancelCallback, CancelPlotCB, XtPointer(plot));
    plot->working_dialog = dialog;

    string base = cmd;
    if (base.contains(' '))
	base = cmd.before(' ');
    MString msg = rm("Starting ") + tt(base) + rm("...");
    XtVaSetValues(dialog, XmNmessageString, msg.xmstring(), XtPointer(0));
    manage_and_raise(dialog);
    wait_until_mapped(dialog);

    // Invoke plot process
    PlotAgent *plotter = 
	new PlotAgent(XtWidgetToApplicationContext(plot->shell), cmd);

    XtAddCallback(plot->shell, XtNpopdownCallback,
		  CancelPlotCB, XtPointer(plot));

    if (plot->area != 0)
    {
	XtAddCallback(plot->area->widget(), XmNexposeCallback,
		      ExposePlotAreaCB, XtPointer(plot));
	XtAddCallback(plot->area->widget(), XmNresizeCallback,
		      ResizePlotAreaCB, XtPointer(plot));
    }

    string init = app_data.plot_init_commands;
    init.prepend("set term " + string(app_data.plot_term_type) + "\n");
    if (!init.empty() && !init.contains('\n', -1))
	init += '\n';

    // Add trace handlers
    plotter->addHandler(Input,  TraceInputHP);     // Gnuplot => DDD
    plotter->addHandler(Output, TraceOutputHP);    // DDD => Gnuplot
    plotter->addHandler(Error,  TraceErrorHP);     // Gnuplot Errors => DDD

    // Show Gnuplot Errors in status line
    plotter->addHandler(Error,  SetStatusHP,       (void *)plot);

    // Handle death
    plotter->addHandler(Died, PlotterNotFoundHP, (void *)plot);
    plotter->addHandler(Died, DeletePlotterHP,   (void *)plot);

    if (plot->area != 0)
	plotter->addHandler(Plot, GetPlotHP, (void *)plot);

    plotter->start_with(init);
    plot->plotter = plotter;

    return plotter;
}


//-------------------------------------------------------------------------
// Drawing stuff
//-------------------------------------------------------------------------

static void ExposePlotAreaCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    plot->area->replot(false);
}

static void ResizePlotAreaCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    plot->area->replot(true);
}


//-------------------------------------------------------------------------
// Selection stuff
//-------------------------------------------------------------------------

static void SelectPlotCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    data_disp->select(plot->source);
}

static void SelectAndPrintPlotCB(Widget w, XtPointer client_data, 
				 XtPointer call_data)
{
    SelectPlotCB(w, client_data, call_data);
    PrintPlotCB(w, client_data, call_data);
}



//-------------------------------------------------------------------------
// Plot again
//-------------------------------------------------------------------------

static void ReplotCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    // This transfers the data once again and replots the whole thing
    plot->source->plot();
}

//-------------------------------------------------------------------------
// Command
//-------------------------------------------------------------------------

// Selection from Command widget
static void DoPlotCommandCB(Widget, XtPointer client_data, XtPointer call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    XmCommandCallbackStruct *cbs = (XmCommandCallbackStruct *)call_data;

    MString xcmd(cbs->value, true);
    string cmd = xcmd.str();

    send_and_replot(plot, cmd);
}

// Apply button
static void ApplyPlotCommandCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    Widget text = XmCommandGetChild(plot->command, XmDIALOG_COMMAND_TEXT);
    String cmd_s = 0;

    if (XmIsTextField(text))
	cmd_s = XmTextFieldGetString(text);
    else if (XmIsText(text))
	cmd_s = XmTextGetString(text);
    else {
        assert(0);
	::abort();
    }

    string cmd = cmd_s;
    XtFree(cmd_s);

    send_and_replot(plot, cmd);
}

static void EnableApplyCB(Widget, XtPointer client_data, XtPointer call_data)
{
    Widget apply = (Widget)client_data;
    XmCommandCallbackStruct *cbs = (XmCommandCallbackStruct *)call_data;

    set_sensitive(apply, cbs->length > 0);
}

static void PlotCommandCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    if (plot->command_dialog == 0)
    {
	Arg args[10];
	Cardinal arg = 0;
	Widget dialog = 
	    verify(XmCreatePromptDialog(plot->shell,
					XMST("plot_command_dialog"),
					args, arg));
	Delay::register_shell(dialog);
	plot->command_dialog = dialog;

	Widget apply = XmSelectionBoxGetChild(dialog, XmDIALOG_APPLY_BUTTON);
	XtManageChild(apply);
    
	XtUnmanageChild(XmSelectionBoxGetChild(dialog, 
					       XmDIALOG_OK_BUTTON));
	XtUnmanageChild(XmSelectionBoxGetChild(dialog, 
					       XmDIALOG_SELECTION_LABEL));
	XtUnmanageChild(XmSelectionBoxGetChild(dialog, XmDIALOG_TEXT));

	XtAddCallback(dialog, XmNapplyCallback,
		      ApplyPlotCommandCB, XtPointer(client_data));
	XtAddCallback(dialog, XmNhelpCallback,
		      ImmediateHelpCB, XtPointer(client_data));

	arg = 0;
	Widget command = 
	    verify(XmCreateCommand(dialog, XMST("plot_command"), args, arg));
	plot->command = command;
	XtManageChild(command);

	XtAddCallback(command, XmNcommandEnteredCallback, 
		      DoPlotCommandCB, XtPointer(client_data));
	XtAddCallback(command, XmNcommandChangedCallback, 
		      EnableApplyCB, XtPointer(apply));
	set_sensitive(apply, false);
    }

    manage_and_raise(plot->command_dialog);
}


//-------------------------------------------------------------------------
// Export
//-------------------------------------------------------------------------

static void SetCB(Widget, XtPointer client_data, XtPointer)
{
    bool *value = (bool *)client_data;
    *value = true;
}

static void DoExportCB(Widget w, XtPointer client_data, XtPointer call_data)
{
    SelectPlotCB(w, client_data, call_data);

    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    string target = get_file(w, client_data, call_data);
    if (target.empty())
	return;

    const StringArray& titles  = plot->plotter->data_titles();
    const StringArray& sources = plot->plotter->data_files();

    string source = "";
    string title  = "";
    for (int i = 0; source.empty() && i < sources.size(); i++)
    {
	if (!sources[i].empty())
	{
	    source = sources[i];
	    title  = titles[i];
	}
    }
    
    if (source.empty())
	return;			// This should not happen

    if (access(target.chars(), W_OK) == 0 && is_regular_file(target))
    {
	// File exists - request confirmation
	static Widget confirm_overwrite_dialog = 0;
	if (confirm_overwrite_dialog != 0)
	    DestroyWhenIdle(confirm_overwrite_dialog);

	Arg args[10];
	Cardinal arg = 0;
	XtSetArg(args[arg], XmNdialogStyle, 
		 XmDIALOG_FULL_APPLICATION_MODAL); arg++;
	confirm_overwrite_dialog = 
	    verify(XmCreateQuestionDialog(plot->shell,
					  XMST("confirm_overwrite_dialog"), 
					  args, arg));
	Delay::register_shell(confirm_overwrite_dialog);

	bool yes = false;
	bool no  = false;
	   
	XtAddCallback(confirm_overwrite_dialog,
		      XmNokCallback, SetCB, XtPointer(&yes));
	XtAddCallback(confirm_overwrite_dialog,
		      XmNcancelCallback, SetCB, XtPointer(&no));
	XtAddCallback(confirm_overwrite_dialog, 
		      XmNhelpCallback, ImmediateHelpCB, 0);

	MString question = rm("Overwrite existing file " 
			      + quote(target) + "?");
	XtVaSetValues (confirm_overwrite_dialog, XmNmessageString, 
		       question.xmstring(), XtPointer(0));
	manage_and_raise(confirm_overwrite_dialog);

	XtAppContext app_context = XtWidgetToApplicationContext(plot->shell);
	while (!yes && !no)
	    XtAppProcessEvent(app_context, XtIMAll);

	if (no)
	    return;
    }

    StatusDelay delay("Saving " + title + " data to " + quote(target));

    // Copy SOURCE to TARGET
    std::ifstream is(source.chars());
    std::ofstream os(target.chars());

    if (os.bad())
    {
	FILE *fp = fopen(target.chars(), "w");
	post_error(string("Cannot open ") 
		   + quote(target) + ": " + strerror(errno), 
		   "export_failed_error", plot->shell);
	if (fp)
	    fclose(fp);
	delay.outcome = strerror(errno);
	return;
    }

    int c;
    while ((c = is.get()) != EOF)
	os.put((unsigned char) c);

    XtUnmanageChild(plot->export_dialog);
}

static void ExportPlotCB(Widget w, XtPointer client_data, XtPointer call_data)
{
    SelectPlotCB(w, client_data, call_data);

    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    if (plot->export_dialog == 0)
    {
	Arg args[10];
	Cardinal arg = 0;
	Widget dialog = 
	    verify(XmCreateFileSelectionDialog(plot->shell, 
					       XMST("export_data"), 
					       args, arg));
	plot->export_dialog = dialog;

	Delay::register_shell(dialog);
	XtAddCallback(dialog, XmNokCallback, DoExportCB, client_data);
	XtAddCallback(dialog, XmNcancelCallback, UnmanageThisCB, 
		      XtPointer(dialog));
	XtAddCallback(dialog, XmNhelpCallback, ImmediateHelpCB, XtPointer(0));
    }

    manage_and_raise(plot->export_dialog);
}


//-------------------------------------------------------------------------
// Settings
//-------------------------------------------------------------------------

static void ToggleOptionCB(Widget w, XtPointer client_data, 
			   XtPointer call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    XmToggleButtonCallbackStruct *cbs = 
	(XmToggleButtonCallbackStruct *)call_data;

    string cmd;
    if (cbs->set)
	cmd = string("set ") + XtName(w);
    else
	cmd = string("set no") + XtName(w);

    send_and_replot(plot, cmd);
}

static void ToggleLogscaleCB(Widget, XtPointer client_data, 
			     XtPointer call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    XmToggleButtonCallbackStruct *cbs = 
	(XmToggleButtonCallbackStruct *)call_data;

    string cmd;
    if (cbs->set)
	cmd = "set logscale ";
    else
	cmd = "set nologscale ";

    if (plot->plotter->dimensions() >= 3)
	cmd += "z";
    else
	cmd += "y";

    send_and_replot(plot, cmd);
}

static void SetStyleCB(Widget w, XtPointer client_data, XtPointer call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    XmToggleButtonCallbackStruct *cbs = 
	(XmToggleButtonCallbackStruct *)call_data;

    if (cbs->set)
    {
	string style = XtName(w);
	string cmd;
	if (style.contains("3d", -1))
	{
	    cmd = "set hidden3d\n";
	    style = style.before("3d");
	}
	else
	{
	    cmd = "set nohidden3d\n";
	}
	if (style.contains("2d", -1))
	    style = style.before("2d");
	
	cmd += "set data style " + style;

	send_and_replot(plot, cmd);
    }
}

static void SetContourCB(Widget w, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    Widget base    = XtNameToWidget(XtParent(w), "base");
    Widget surface = XtNameToWidget(XtParent(w), "surface");

    assert (base != 0 && surface != 0);

    bool base_set    = XmToggleButtonGetState(base);
    bool surface_set = XmToggleButtonGetState(surface);

    string cmd;
    if (base_set && surface_set)
	cmd = "set contour both";
    else if (base_set && !surface_set)
	cmd = "set contour base";
    else if (!base_set && surface_set)
	cmd = "set contour surface";
    else
	cmd = "set nocontour";

    send_and_replot(plot, cmd);
}

static void SetViewCB(Widget, XtPointer client_data, XtPointer)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;

    int rot_x = 60;
    int rot_z = 30;

    XtVaGetValues(plot->vsb, XmNvalue, &rot_x, XtPointer(0));
    XtVaGetValues(plot->hsb, XmNvalue, &rot_z, XtPointer(0));

    string cmd = 
	"set view " + itostring(rot_x) + ", " + itostring(rot_z);

    send_and_replot(plot, cmd);
}

//-------------------------------------------------------------------------
// Status line
//-------------------------------------------------------------------------

// Forward Gnuplot error messages to DDD status line
static void SetStatusHP(Agent *, void *client_data, void *call_data)
{
    PlotWindowInfo *plot = (PlotWindowInfo *)client_data;
    DataLength* dl = (DataLength *) call_data;
    string s(dl->data, dl->length);

    (void) plot;		// Use it
#if 0
    if (!plot->active)
    {
	// Probably an invocation problem
	post_gdb_message(s);
	return;
    }
#endif

    if (plot->command != 0)
    {
	string msg = s;
	strip_space(msg);
	MString xmsg = tb(msg);
	XmCommandError(plot->command, xmsg.xmstring());
    }

    while (!s.empty())
    {
	string line;
	if (s.contains('\n'))
	    line = s.before('\n');
	else
	    line = s;
	s = s.after('\n');
	strip_space(line);

	if (!line.empty())
	    set_status(line);
    }
}

//-------------------------------------------------------------------------
// Trace communication
//-------------------------------------------------------------------------

static void trace(const char *prefix, void *call_data)
{
    DataLength* dl = (DataLength *) call_data;
    string s(dl->data, dl->length);

    bool s_ends_with_nl = false;
    if (s.length() > 0 && s[s.length() - 1] == '\n')
    {
	s_ends_with_nl = true;
	s = s.before(int(s.length() - 1));
    }

    s = quote(s);
    string nl = "\\n\"\n";
    nl += replicate(' ', strlen(prefix));
    nl += "\"";
    s.gsub("\\n", nl);

    if (s_ends_with_nl)
	s.at((int)(s.length() - 1), 0) = "\\n";

    dddlog << prefix << s << '\n';
    dddlog.flush();
}

static void TraceInputHP(Agent *, void *, void *call_data)
{
    trace("<< ", call_data);
}

static void TraceOutputHP(Agent *, void *, void *call_data)
{
    trace(">> ", call_data);
}

static void TraceErrorHP(Agent *, void *, void *call_data)
{
    trace("<= ", call_data);
}
