/*
 * Copyright (c) 2012-2017, Red Hat.  All Rights Reserved.
 * Copyright (c) 2007-2008, Aconex.  All Rights Reserved.
 * Copyright (c) 2006, Ken McDonell.  All Rights Reserved.
 * 
 * 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.
 * 
 * This program 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.
 */
#include "main.h"
#include "groupcontrol.h"

#define DESPERATE 0

GroupControl::GroupControl(bool restrictArchives) :
    QmcGroup(restrictArchives)
{
    my.samples = 0;
    my.visible = 0;
    my.realDelta = 0;
    my.realPosition = 0;
    my.timeState = StartState;
    my.buttonState = QedTimeButton::Timeless;
    my.pmtimeState = QmcTime::StoppedState;
    memset(&my.delta, 0, sizeof(struct timeval));
    memset(&my.position, 0, sizeof(struct timeval));
}

void
GroupControl::init(int samples, int visible,
			struct timeval *interval, struct timeval *position)
{
    my.samples = samples;
    my.visible = visible;

    if (isArchiveSource()) {
	my.pmtimeState = QmcTime::StoppedState;
	my.buttonState = QedTimeButton::StoppedArchive;
    }
    else {
	my.pmtimeState = QmcTime::ForwardState;
	my.buttonState = QedTimeButton::ForwardLive;
    }
    my.delta = *interval;
    my.position = *position;
    my.realDelta = pmtimevalToReal(interval);
    my.realPosition = pmtimevalToReal(position);

    my.timeData.clear();
    for (int i = 0; i < samples; i++)
	my.timeData.push_back(my.realPosition - (i * my.realDelta));
}

bool
GroupControl::isArchiveSource(void)
{
    // Note: We purposefully are not using QmcGroup::mode() here, as we
    // may not have initialised any contexts yet.  In such a case, live
    // mode is always returned (default, from the QmcGroup constructor).

    return this == archiveGroup;
}

bool
GroupControl::isActive(QmcTime::Packet *packet)
{
    return (((activeGroup == archiveGroup) &&
	     (packet->source == QmcTime::ArchiveSource)) ||
	    ((activeGroup == liveGroup) && 
	     (packet->source == QmcTime::HostSource)));
}

void
GroupControl::addGadget(Gadget *gadget)
{
    my.gadgetsList.append(gadget);
}

void
GroupControl::deleteGadget(Gadget *gadget)
{
    for (int i = 0; i < gadgetCount(); i++)
	if (my.gadgetsList.at(i) == gadget)
	    my.gadgetsList.removeAt(i);
}

int
GroupControl::gadgetCount() const
{
    return my.gadgetsList.size();
}

void
GroupControl::updateBackground(void)
{
    for (int i = 0; i < gadgetCount(); i++)
	my.gadgetsList.at(i)->updateBackground(globalSettings.chartBackground);
}

void
GroupControl::updateTimeAxis(void)
{
    QString tz, otz, unused;

    if (numContexts() > 0 || isArchiveSource() == false) {
	if (numContexts() > 0)
	    defaultTZ(unused, otz);
	else
	    otz = QmcSource::localHost;
	tz = otz;
	pmchart->timeAxis()->setAxisScale(QwtPlot::xBottom,
		my.timeData[my.visible - 1], my.timeData[0],
		pmchart->timeAxis()->scaleValue(my.realDelta, my.visible));
	pmchart->timeAxis()->clearScaleCache();
	pmchart->timeAxis()->replot();
	pmchart->setDateLabel(my.position.tv_sec, tz);
    } else {
	pmchart->timeAxis()->noArchiveSources();
	pmchart->setDateLabel(tr("[No open archives]"));
    }

    if (console->logLevel(PmChart::DebugProtocol)) {
	int i = my.visible - 1;
	console->post(PmChart::DebugProtocol,
		"GroupControl::updateTimeAxis: tz=%s; visible points=%d",
		(const char *)tz.toLatin1(), i);
	console->post(PmChart::DebugProtocol,
		"GroupControl::updateTimeAxis: first time is %.3f (%s)",
		my.timeData[i], timeString(my.timeData[i]));
	console->post(PmChart::DebugProtocol,
		"GroupControl::updateTimeAxis: final time is %.3f (%s)",
		my.timeData[0], timeString(my.timeData[0]));
    }
}

void
GroupControl::updateTimeButton(void)
{
    pmchart->setButtonState(my.buttonState);
}

QmcTime::State
GroupControl::pmtimeState(void)
{
    return my.pmtimeState;
}

char *
GroupControl::timeState()
{
    static char buf[16];

    switch (my.timeState) {
    case StartState:	strcpy(buf, "Start"); break;
    case ForwardState:	strcpy(buf, "Forward"); break;
    case BackwardState:	strcpy(buf, "Backward"); break;
    case EndLogState:	strcpy(buf, "EndLog"); break;
    case StandbyState:	strcpy(buf, "Standby"); break;
    default:		strcpy(buf, "Dodgey"); break;
    }
    return buf;
}

void
GroupControl::timeSelectionActive(Gadget *source, int timestamp)
{
    QPoint point(timestamp, -1);
    QMouseEvent pressed(QEvent::MouseButtonPress, point,
			Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
    for (int i = 0; i < gadgetCount(); i++) {
	Gadget *gadget = my.gadgetsList.at(i);
	if (source != gadget)
	    gadget->activateTime(&pressed);
    }
}

void
GroupControl::timeSelectionReactive(Gadget *source, int timestamp)
{
    QPoint point(timestamp, -1);
    QMouseEvent moved(QEvent::MouseMove, point,
			Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
    for (int i = 0; i < gadgetCount(); i++) {
	Gadget *gadget = my.gadgetsList.at(i);
	if (source != gadget)
	    gadget->reactivateTime(&moved);
    }
}

void
GroupControl::timeSelectionInactive(Gadget *source)
{
    QPoint point(-1, -1);
    QMouseEvent release(QEvent::MouseButtonRelease, point,
			Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
    for (int i = 0; i < gadgetCount(); i++) {
	Gadget *gadget = my.gadgetsList.at(i);
	if (source != gadget)
	    gadget->deactivateTime(&release);
    }
}

//
// Drive all updates into each gadget (refresh the display)
//
void
GroupControl::refreshGadgets(bool active)
{
#if DESPERATE
    for (int s = 0; s < my.samples; s++)
	console->post(PmChart::DebugProtocol,
		"GroupControl::refreshGadgets: timeData[%2d] is %.2f (%s)",
		s, my.timeData[s], timeString(my.timeData[s]));
    console->post(PmChart::DebugProtocol,
		"GroupControl::refreshGadgets: state=%s", timeState());
#endif

    for (int i = 0; i < gadgetCount(); i++) {
	my.gadgetsList.at(i)->updateValues(my.timeState != BackwardState,
					active, my.samples, my.visible,
					my.timeData[my.visible - 1],
					my.timeData[0],
					my.realDelta);
    }
    if (active) {
	updateTimeButton();
	updateTimeAxis();
    }
}

//
// Setup the initial data needed after opening a view.
// All of the work is in archive mode; in live mode we have
// not yet got any historical data that we can display...
//
void
GroupControl::setupWorldView(void)
{
    if (isArchiveSource() == false)
	return;

    QmcTime::Packet packet;
    packet.source = QmcTime::ArchiveSource;
    packet.state = QmcTime::ForwardState;
    packet.mode = QmcTime::NormalMode;
    memcpy(&packet.delta, pmtime->archiveInterval(), sizeof(packet.delta));
    memcpy(&packet.position, pmtime->archivePosition(), sizeof(packet.position));
    memcpy(&packet.start, pmtime->archiveStart(), sizeof(packet.start));
    memcpy(&packet.end, pmtime->archiveEnd(), sizeof(packet.end));
    adjustWorldView(&packet, true);
}

//
// Received a Set or a VCRMode requiring us to adjust our state
// and possibly rethink everything.  This can result from a time
// control position change, delta change, direction change, etc.
//
void
GroupControl::adjustWorldView(QmcTime::Packet *packet, bool vcrMode)
{
    my.delta = packet->delta;
    my.position = packet->position;
    my.realDelta = pmtimevalToReal(&packet->delta);
    my.realPosition = pmtimevalToReal(&packet->position);

    console->post("GroupControl::adjustWorldView: "
		  "sh=%d vh=%d delta=%.2f position=%.2f (%s) state=%s",
		my.samples, my.visible, my.realDelta, my.realPosition,
		timeString(my.realPosition), timeState());

    QmcTime::State state = packet->state;
    if (isArchiveSource()) {
	if (packet->state == QmcTime::ForwardState)
	    adjustArchiveWorldViewForward(packet, vcrMode);
	else if (packet->state == QmcTime::BackwardState)
	    adjustArchiveWorldViewBackward(packet, vcrMode);
	else
	    adjustArchiveWorldViewStopped(packet, vcrMode);
    }
    else if (state != QmcTime::StoppedState)
	adjustLiveWorldViewForward(packet);
    else
	adjustLiveWorldViewStopped(packet);
}

void
GroupControl::adjustLiveWorldViewStopped(QmcTime::Packet *packet)
{
    if (isActive(packet)) {
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
	updateTimeButton();
    }
}

static bool
fuzzyTimeMatch(double a, double b, double tolerance)
{
    // a matches b if the difference is within 1% of the delta (tolerance)
    return (a == b ||
	    (b > a && a + tolerance > b) ||
	    (b < a && a - tolerance < b));
}

void
GroupControl::adjustLiveWorldViewForward(QmcTime::Packet *packet)
{
    //
    // X-Axis _max_ becomes packet->position.
    // Rest of (preceeding) time window filled in using packet->delta.
    // In live mode, we can only fetch current data.  However, we make
    // an effort to keep old data that happens to align with the delta
    // time points (or near enough) that we are now interested in.  So,
    // "oi" is the old index, whereas "i" is the new timeData[] index.
    // First we try to find a fuzzy match on current old index, if it
    // doesn't exit, we continue moving "oi" until it points at a time
    // larger than the one we're after, and then see how that fares on
    // the next iteration.
    //
    int i, oi, last = my.samples - 1;
    bool preserve = false;
    double tolerance = my.realDelta;
    double position = my.realPosition - (my.realDelta * my.samples);

    for (i = oi = last; i >= 0; i--, position += my.realDelta) {
	while (i > 0 && my.timeData[oi] < position + my.realDelta && oi > 0) {
	    if (fuzzyTimeMatch(my.timeData[oi], position, tolerance) == false) {
#if DESPERATE
		console->post("NO fuzzyTimeMatch %.3f to %.3f (%s)",
			my.timeData[oi], position, timeString(position));
#endif
		if (my.timeData[oi] > position)
		    break;
		oi--;
		continue;
	    }
	    console->post("Saved live data (oi=%d/i=%d) for %s", oi, i,
						timeString(position));
	    for (int j = 0; j < gadgetCount(); j++)
		my.gadgetsList.at(j)->preserveSample(i, oi);
	    my.timeData[i] = my.timeData[oi];
	    preserve = true;
	    oi--;
	    break;
	}

	if (i == 0) {	// refreshGadgets() finishes up last one
	    console->post("Fetching data[%d] at %s", i, timeString(position));
	    my.timeData[i] = position;
	    fetch();
	}
	else if (preserve == false) {
#if DESPERATE
	    console->post("No live data for %s", timeString(position));
#endif
	    my.timeData[i] = position;
	    for (int j = 0; j < gadgetCount(); j++)
		my.gadgetsList.at(j)->punchoutSample(i);
	}
	else
	    preserve = false;
    }
    // One (per-gadget) recalculation & refresh at the end, after all data moved
    for (int j = 0; j < gadgetCount(); j++)
	my.gadgetsList.at(j)->adjustValues();
    my.timeState = (packet->state == QmcTime::StoppedState) ?
			StandbyState : ForwardState;

    bool active = isActive(packet);
    if (active)
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
    refreshGadgets(active);
}

void
GroupControl::adjustArchiveWorldViewForward(QmcTime::Packet *packet, bool setup)
{
    console->post("GroupControl::adjustArchiveWorldViewForward setup=%d", setup);
    my.timeState = ForwardState;

    int setmode = PM_MODE_INTERP;
    int delta = packet->delta.tv_sec;
    if (packet->delta.tv_usec == 0) {
	setmode |= PM_XTB_SET(PM_TIME_SEC);
    } else {
	delta = delta * 1000 + packet->delta.tv_usec / 1000;
	setmode |= PM_XTB_SET(PM_TIME_MSEC);
    }

    //
    // X-Axis _max_ becomes packet->position.
    // Rest of (preceeding) time window filled in using packet->delta.
    //
    int last = my.samples - 1;
    double tolerance = my.realDelta / 20.0;	// 5% of the sample interval
    double position = my.realPosition - (my.realDelta * last);

    double left = position;
    double right = my.realPosition;
    double interval = pmchart->timeAxis()->scaleValue((double)delta, my.visible);

    for (int i = last; i >= 0; i--, position += my.realDelta) {
	if (setup == false &&
	    fuzzyTimeMatch(my.timeData[i], position, tolerance) == true) {
	    continue;
	}

	my.timeData[i] = position;

	struct timeval timeval;
	pmtimevalFromReal(position, &timeval);
	setArchiveMode(setmode, &timeval, delta);
	console->post("Fetching data[%d] at %s", i, timeString(position));
	fetch();
	if (i == 0)		// refreshGadgets() finishes up last one
	    break;
	console->post("GroupControl::adjustArchiveWorldViewForward: "
		      "setting time position[%d]=%.2f[%s] state=%s count=%d",
			i, position, timeString(position),
			timeState(), gadgetCount());
	int cnt = gadgetCount();
	for (int j = 0; j < cnt; j++)
	    my.gadgetsList.at(j)->updateValues(true, j < cnt-1 ? false : true, my.samples, my.visible,
						left, right, interval);
    }

    bool active = isActive(packet);
    if (setup)
	packet->state = QmcTime::StoppedState;
    if (active)
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
    pmtime->setArchivePosition(&packet->position);
    pmtime->setArchiveInterval(&packet->delta);
    refreshGadgets(active);
}

void
GroupControl::adjustArchiveWorldViewBackward(QmcTime::Packet *packet, bool setup)
{
    console->post("GroupControl::adjustArchiveWorldViewBackward");
    my.timeState = BackwardState;

    int setmode = PM_MODE_INTERP;
    int delta = packet->delta.tv_sec;
    if (packet->delta.tv_usec == 0) {
	setmode |= PM_XTB_SET(PM_TIME_SEC);
    } else {
	delta = delta * 1000 + packet->delta.tv_usec / 1000;
	setmode |= PM_XTB_SET(PM_TIME_MSEC);
    }

    //
    // X-Axis _min_ becomes packet->position.
    // Rest of (following) time window filled in using packet->delta.
    //
    int last = my.samples - 1;
    double tolerance = my.realDelta / 20.0;	// 5% of the sample interval
    double position = my.realPosition;

    double left = position - (my.realDelta * last);
    double right = position;
    double interval = pmchart->timeAxis()->scaleValue((double)delta, my.visible);

    for (int i = 0; i <= last; i++, position -= my.realDelta) {
	if (setup == false &&
	    fuzzyTimeMatch(my.timeData[i], position, tolerance) == true) {
	    continue;
	}

	my.timeData[i] = position;

	struct timeval timeval;
	pmtimevalFromReal(position, &timeval);
	setArchiveMode(setmode, &timeval, -delta);
	console->post("Fetching data[%d] at %s", i, timeString(position));
	fetch();
	if (i == last)		// refreshGadgets() finishes up last one
	    break;
	console->post("GroupControl::adjustArchiveWorldViewBackward: "
		      "setting time position[%d]=%.2f[%s] state=%s count=%d",
			i, position, timeString(position),
			timeState(), gadgetCount());
	for (int j = 0; j < gadgetCount(); j++)
	    my.gadgetsList.at(j)->updateValues(false, false, my.samples, my.visible,
						left, right, interval);
    }

    bool active = isActive(packet);
    if (setup)
	packet->state = QmcTime::StoppedState;
    if (active)
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
    pmtime->setArchivePosition(&packet->position);
    pmtime->setArchiveInterval(&packet->delta);
    refreshGadgets(active);
}

void
GroupControl::adjustArchiveWorldViewStopped(QmcTime::Packet *packet, bool needFetch)
{
    console->post("GroupControl::adjustArchiveWorldViewStopped needFetch=%d", needFetch);
    if (needFetch) {	// stopped, but VCR reposition event occurred
	adjustArchiveWorldViewForward(packet, needFetch);
    } else {
	my.timeState = StandbyState;
	packet->state = QmcTime::StoppedState;
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
	updateTimeButton();
    }
}

//
// Catch the situation where we get a larger than expected increase
// in position.  This happens when we restart after a stop in live
// mode (both with and without a change in the delta).
//
static bool
sideStep(double n, double o, double interval)
{
    // tolerance set to 5% of the sample interval:
    return fuzzyTimeMatch(o + interval, n, interval/20.0) == false;
}

//
// Fetch all metric values across all gadgets, and also update the
// unified time axis.
//
void
GroupControl::step(QmcTime::Packet *packet)
{
    double stepPosition = pmtimevalToReal(&packet->position);

    console->post(PmChart::DebugProtocol,
	"GroupControl::step: stepping to time %.2f, delta=%.2f, state=%s",
	stepPosition, my.realDelta, timeState());

    if ((packet->source == QmcTime::ArchiveSource &&
	((packet->state == QmcTime::ForwardState &&
		my.timeState != ForwardState) ||
	 (packet->state == QmcTime::BackwardState &&
		my.timeState != BackwardState))) ||
	 sideStep(stepPosition, my.realPosition, my.realDelta))
	return adjustWorldView(packet, false);

    my.pmtimeState = packet->state;
    my.position = packet->position;
    my.realPosition = stepPosition;

    int last = my.samples - 1;
    if (packet->state == QmcTime::ForwardState) { // left-to-right (all but 1st)
	if (my.samples > 1)
	    my.timeData.pop_back();
	my.timeData.push_front(my.realPosition);
    }
    else if (packet->state == QmcTime::BackwardState) { // right-to-left
	if (my.samples > 1)
	    my.timeData.pop_front();
	my.timeData.push_back(my.realPosition - torange(my.delta, last));
    }

    fetch();

    bool active = isActive(packet);
    if (isActive(packet))
	newButtonState(packet->state, packet->mode, pmchart->isTabRecording());
    refreshGadgets(active);
}

void
GroupControl::VCRMode(QmcTime::Packet *packet, bool dragMode)
{
    if (!dragMode)
	adjustWorldView(packet, true);
    console->post("GroupControl::VCRMode dragMode=%d\n", dragMode);
}

void
GroupControl::setTimezone(QmcTime::Packet *packet, char *tz)
{
    console->post(PmChart::DebugProtocol, "GroupControl::setTimezone %s", tz);

    useTZ(QString(tz));

    if (isActive(packet)) 
	updateTimeAxis();
}

void
GroupControl::setSampleHistory(int v)
{
    console->post("GroupControl::setSampleHistory (%d -> %d)", my.samples, v);
    if (my.samples != v) {
	my.samples = v;

	double right = my.realPosition;
	my.timeData.clear();
	for (v = 0; v < my.samples; v++)
	    my.timeData.push_back(my.realPosition - (v * my.realDelta));
	double left = my.timeData[v-1];
	for (v = 0; v < gadgetCount(); v++)
	    my.gadgetsList.at(v)->resetValues(my.samples, left, right);
    }
}

int
GroupControl::sampleHistory(void)
{
    return my.samples;
}

void
GroupControl::setVisibleHistory(int v)
{
    console->post("GroupControl::setVisibleHistory (%d -> %d)", my.visible, v);

    if (my.visible != v)
	my.visible = v;
}

int
GroupControl::visibleHistory(void)
{
    return my.visible;
}

QVector<double> &
GroupControl::timeAxisData(void)
{
    return my.timeData;
}

QedTimeButton::State
GroupControl::buttonState(void)
{
    return my.buttonState;
}

void
GroupControl::newButtonState(QmcTime::State s, QmcTime::Mode m, bool record)
{
    if (isArchiveSource() == false) {
	if (s == QmcTime::StoppedState)
	    my.buttonState = record ?
		QedTimeButton::StoppedRecord : QedTimeButton::StoppedLive;
	else
	    my.buttonState = record ?
		QedTimeButton::ForwardRecord : QedTimeButton::ForwardLive;
    }
    else if (m == QmcTime::StepMode) {
	if (s == QmcTime::ForwardState)
	    my.buttonState = QedTimeButton::StepForwardArchive;
	else if (s == QmcTime::BackwardState)
	    my.buttonState = QedTimeButton::StepBackwardArchive;
	else
	    my.buttonState = QedTimeButton::StoppedArchive;
    }
    else if (m == QmcTime::FastMode) {
	if (s == QmcTime::ForwardState)
	    my.buttonState = QedTimeButton::FastForwardArchive;
	else if (s == QmcTime::BackwardState)
	    my.buttonState = QedTimeButton::FastBackwardArchive;
	else
	    my.buttonState = QedTimeButton::StoppedArchive;
    }
    else if (s == QmcTime::ForwardState)
	my.buttonState = QedTimeButton::ForwardArchive;
    else if (s == QmcTime::BackwardState)
	my.buttonState = QedTimeButton::BackwardArchive;
    else
	my.buttonState = QedTimeButton::StoppedArchive;
}
