/****************************************************************************
 * This file is part of the project AqHome.
 * AqHome (c) by 2025 Martin Preuss, all rights reserved.
 *
 * The license for this file can be found in the file COPYING which you
 * should have received along with this file.
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif


#include "./mdevices_page.h"

#include "aqhome-cgi/modules/mdataclient.h"

#include <aqcgi/service/module.h>

#include <aqdiagram/graph/timegraph.h>
#include <aqdiagram/graph/w_graph.h>
#include <aqdiagram/draw/context_cairo.h>

#include <gwenhywfar/debug.h>
#include <gwenhywfar/timestamp.h>
#include <gwenhywfar/text.h>
#include <gwenhywfar/directory.h>

#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>



/* ------------------------------------------------------------------------------------------------
 * defs and enums
 * ------------------------------------------------------------------------------------------------
 */

#define GBAS GWEN_Buffer_AppendString
#define GBAA GWEN_Buffer_AppendArgs


enum {
  MY_LAYOUT_NONE=0,
  MY_LAYOUT_HORIZONTAL,
  MY_LAYOUT_VERTICAL
};



/* ------------------------------------------------------------------------------------------------
 * forward declarations
 * ------------------------------------------------------------------------------------------------
 */

static void _writePage(AQCGI_MODULE *m, AQH_DATACLIENT *dc, GWEN_XMLNODE *nPage, GWEN_BUFFER *dbuf);
static void _writeItem(AQCGI_MODULE *m, AQH_DATACLIENT *dc, const char *sPageId, GWEN_XMLNODE *nItem, GWEN_BUFFER *dbuf);
static void _writeActor(AQH_DATACLIENT *dc, const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf);
static void _writeGraph(const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf);

static void _handlePageActor(AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sActorId, GWEN_XMLNODE *nActor);
static void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc,
                             const char *sPageId, const char *sGraphId,
                             GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf);
static void _genPageGraph(AQH_DATACLIENT *dc, const char *sFilename, GWEN_XMLNODE *nGraph);
static void _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_XMLNODE *nGraph, uint64_t tsBegin, uint64_t tsEnd);
static AQDG_GRAPH_DATAPAIR_LIST *_readCurveData(AQH_DATACLIENT *dc, GWEN_XMLNODE *nCurve, uint64_t tsBegin, uint64_t tsEnd);
static uint64_t _parseTime(const char *s);
static GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName);
static void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, GWEN_BUFFER *dbuf);
static void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, GWEN_BUFFER *dbuf);
static void _writeRgbwToForm(const char *sValueName, uint32_t color, GWEN_BUFFER *dbuf);
static void _writeOnOffToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf);
static void _writeOnOffAutoToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf);
static void _setRgbwData(AQH_DATACLIENT *dc, GWEN_DB_NODE *dbPost, const char *sValueName, const AQH_VALUE *value);
static int _getColorComponent(GWEN_DB_NODE *dbPost, const char *sValueName, const char *sComponent, int defaultValue);
static void _setOnOffData(AQH_DATACLIENT *dc, const AQH_VALUE *value, const char *sValue);
static void _setOnOffAutoData(AQH_DATACLIENT *dc, const AQH_VALUE *value, const char *sValue);

static void _sendPageList(AQCGI_MODULE *m, GWEN_BUFFER *dbuf);
static GWEN_STRINGLIST *_listPageFiles(AQCGI_MODULE *m);
static GWEN_XMLNODE *_readPage(AQCGI_MODULE *m, const char *sPageName);
static GWEN_XMLNODE *_readPageFile(const char *sFilename);
static int _layoutFromString(const char *s);



/* ------------------------------------------------------------------------------------------------
 * code
 * ------------------------------------------------------------------------------------------------
 */

void AQH_ModDevices_RunPageGet(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
  GWEN_DB_NODE *dbQuery;
  const char *sPageId;

  DBG_INFO(NULL, "RunPageGet");
  dbQuery=AQCGI_Request_GetDbQuery(rq);
  sPageId=dbQuery?GWEN_DB_GetCharValue(dbQuery, "page", 0, NULL):NULL;
  if (sPageId && *sPageId) {
    GWEN_XMLNODE *fileNode;

    fileNode=_readPage(m, sPageId);
    if (fileNode) {
      GWEN_XMLNODE *nPage;

      nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", NULL, NULL);
      if (nPage) {
	_writePage(m, dc, nPage, dbuf);
	AQCGI_Request_AddResponseHeaderData(rq, "Refresh: 120");
      }
      else {
	DBG_ERROR(NULL, "No page element in file for \"%s\"", sPageId);
      }
      GWEN_XMLNode_free(fileNode);
    }
    else {
      DBG_INFO(NULL, "here");
    }
  }
  else {
    DBG_ERROR(NULL, "Reading page list");
    _sendPageList(m, dbuf);
  }
}



void AQH_ModDevices_RunPageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
  GWEN_DB_NODE *dbQuery;
  const char *sPageId;
  const char *sGraphId;

  DBG_INFO(NULL, "RunPageGraphGet");
  dbQuery=AQCGI_Request_GetDbQuery(rq);
  sPageId=GWEN_DB_GetCharValue(dbQuery, "page", 0, NULL);
  sGraphId=GWEN_DB_GetCharValue(dbQuery, "graph", 0, NULL);
  if (sPageId && *sPageId && sGraphId && *sGraphId) {
    GWEN_XMLNODE *fileNode;

    fileNode=_readPage(m, sPageId);
    if (fileNode) {
      GWEN_XMLNODE *nPage;
      GWEN_XMLNODE *nGraph;

      nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", "id", sPageId);
      nGraph=_getSubItemNode(nPage, sGraphId, "graph");
      if (nPage && nGraph)
	_handlePageGraph(m, rq, dc, sPageId, sGraphId, nGraph, dbuf);
      else {
	DBG_ERROR(NULL, "Graph %s/%s not found", sPageId, sGraphId);
      }
      GWEN_XMLNode_free(fileNode);
    }
    else {
      DBG_INFO(NULL, "here");
    }
  }
}



void AQH_ModDevices_RunPagePost(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQCGI_SESSION *session, AQH_DATACLIENT *dc, GWEN_BUFFER *dbuf)
{
  GWEN_DB_NODE *dbPost;
  const char *sPageId;
  const char *sActorId;

  DBG_INFO(NULL, "RunPagePost");
  dbPost=AQCGI_Request_GetDbPostBody(rq);
  sPageId=GWEN_DB_GetCharValue(dbPost, "page", 0, NULL);
  sActorId=GWEN_DB_GetCharValue(dbPost, "actor", 0, NULL);
  if (sPageId && *sPageId && sActorId && *sActorId) {
    GWEN_XMLNODE *fileNode;

    fileNode=_readPage(m, sPageId);
    if (fileNode) {
      GWEN_XMLNODE *nPage;
      GWEN_XMLNODE *nActor;

      nPage=GWEN_XMLNode_FindFirstTag(fileNode, "page", "id", sPageId);
      nActor=_getSubItemNode(nPage, sActorId, "actor");
      if (nPage && nActor) {
	_handlePageActor(rq, dc, sActorId, nActor);
      }
      else {
	DBG_ERROR(NULL, "Actor %s/%s not found", sPageId, sActorId);
      }
      GWEN_XMLNode_free(fileNode);
    }
    else {
      DBG_INFO(NULL, "here");
    }
  }

  if (sPageId && *sPageId) {
    GWEN_BUFFER *pbuf;

    pbuf=GWEN_Buffer_new(0, 256, 0, 1);
    GBAS(pbuf, "Location: page.html?page=");
    GWEN_Text_EscapeToBuffer(sPageId, pbuf);
    AQCGI_Request_AddResponseHeaderData(rq, GWEN_Buffer_GetStart(pbuf));
    GWEN_Buffer_free(pbuf);
  }
  AQCGI_Request_SetResponseCode(rq, 303);
  AQCGI_Request_SetResponseText(rq, "See other");
}



void _handlePageActor(AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sActorId, GWEN_XMLNODE *nActor)
{
  GWEN_DB_NODE *dbPost;
  const char *sDeviceName;
  const char *sValueName;

  dbPost=AQCGI_Request_GetDbPostBody(rq);
  sDeviceName=GWEN_XMLNode_GetProperty(nActor, "device", NULL);
  sValueName=GWEN_XMLNode_GetProperty(nActor, "value", NULL);

  if (sDeviceName && *sDeviceName && sValueName && *sValueName) {
    AQH_VALUE *value;

    value=AQH_ModDevices_GetValueForDevice(dc, sDeviceName, sValueName);
    if (value) {
      const char *sSystemValueName;

      sSystemValueName=AQH_Value_GetNameForSystem(value);
      if (sSystemValueName) {
	const char *sData;

	sData=GWEN_DB_GetCharValue(dbPost, sActorId, 0, NULL);
	DBG_INFO(NULL, "Setting value %s to %s", sSystemValueName, sData?sData:"empty/no value");
	switch(AQH_Value_GetModality(value)) {
	case AQH_ValueModality_RGBW:      _setRgbwData(dc, dbPost, sActorId, value); break;
	case AQH_ValueModality_OnOff:     _setOnOffData(dc, value, sData);           break;
	case AQH_ValueModality_OnOffAuto: _setOnOffAutoData(dc, value, sData);       break;
	default:
	  break;
	} /* switch */
      }
    }
  }
}



void _handlePageGraph(AQCGI_MODULE *m, AQCGI_REQUEST *rq, AQH_DATACLIENT *dc, const char *sPageId, const char *sGraphId, GWEN_XMLNODE *nGraph, GWEN_BUFFER *dbuf)
{
  GWEN_BUFFER *fbuf;
  int refreshTime;

  refreshTime=GWEN_XMLNode_GetIntProperty(nGraph, "refreshTime", 120);
  fbuf=GWEN_Buffer_new(0, 256, 0, 1);
  _mkPathForGraph(m, sPageId, sGraphId, fbuf);
  if (!AQH_ModService_FileIsCurrent(GWEN_Buffer_GetStart(fbuf), refreshTime)) {
    _genPageGraph(dc, GWEN_Buffer_GetStart(fbuf), nGraph);
  }
  AQH_ModService_RespondWithMimeFile(rq, GWEN_Buffer_GetStart(fbuf), "image/png", dbuf);
  GWEN_Buffer_free(fbuf);
}



void _genPageGraph(AQH_DATACLIENT *dc, const char *sFilename, GWEN_XMLNODE *nGraph)
{
  const char *s;
  const char *sTitle;
  int w;
  int h;
  int precision;
  uint64_t tsBegin;
  uint64_t tsEnd;
  AQDG_GRAPH *g;
  AQDG_DRAW_CONTEXT *drawContext;
  AQDG_OBJECT *graphObject;
  uint32_t tickFlags=0;
  double upperLimit;
  double lowerLimit;

  sTitle=GWEN_XMLNode_GetProperty(nGraph, "title", "untitled");
  w=GWEN_XMLNode_GetIntProperty(nGraph, "width", AQH_MODDEVICES_GRAPH_WIDTH);
  h=GWEN_XMLNode_GetIntProperty(nGraph, "height", AQH_MODDEVICES_GRAPH_HEIGHT);
  precision=GWEN_XMLNode_GetIntProperty(nGraph, "precision", 2);
  tsBegin=_parseTime(GWEN_XMLNode_GetProperty(nGraph, "begin", "-4h"));
  tsEnd=_parseTime(GWEN_XMLNode_GetProperty(nGraph, "end", "0"));

  s=GWEN_XMLNode_GetProperty(nGraph, "lowerLimit", NULL);
  if (s && *s) {
    if (1==sscanf(s, "%lf", &lowerLimit))
      tickFlags|=AQDG_TIMEGRAPH_SETUPTICKS_FLAGS_MINY;
    else {
      DBG_ERROR(NULL, "Ignoring invalid lowerLimit (%s)", s);
    }
  }

  s=GWEN_XMLNode_GetProperty(nGraph, "upperLimit", NULL);
  if (s && *s) {
    if (1==sscanf(s, "%lf", &upperLimit))
      tickFlags|=AQDG_TIMEGRAPH_SETUPTICKS_FLAGS_MAXY;
    else {
      DBG_ERROR(NULL, "Ignoring invalid upperLimit (%s)", s);
    }
  }

  g=AQDG_TimeGraph_new(sTitle, NULL, "Value", NULL, precision);
  _addCurves(dc, g, nGraph, tsBegin, tsEnd);
  AQDG_TimeGraph_SetupTicks(g, tickFlags, lowerLimit, upperLimit);

  DBG_DEBUG(NULL, "Draw graph %s", sFilename);
  drawContext=AQDG_Draw_ContextCairo_Png_new(sFilename, w, h);
  graphObject=AQDG_GraphWidget_new(NULL, AQDG_OBJECT_OPTIONS_STRETCHX | AQDG_OBJECT_OPTIONS_STRETCHY, drawContext);
  AQDG_Object_SetWidth(graphObject, w);
  AQDG_Object_SetHeight(graphObject, h);

  AQDG_GraphWidget_SetupDefaultPens(graphObject);
  AQDG_GraphWidget_SetupDefaultFonts(graphObject);

  AQDG_GraphWidget_FinishWithGraph(graphObject, g);

  AQDG_Object_free(graphObject);
}



void _addCurves(AQH_DATACLIENT *dc, AQDG_GRAPH *g, GWEN_XMLNODE *nGraph, uint64_t tsBegin, uint64_t tsEnd)
{
  GWEN_XMLNODE *nCurve;

  nCurve=GWEN_XMLNode_FindFirstTag(nGraph, "curve", NULL, NULL);
  while(nCurve) {
    const char *sModifier;

    sModifier=GWEN_XMLNode_GetProperty(nCurve, "modifier", NULL);
    if (sModifier && *sModifier) {
      AQDG_GRAPH_DATAPAIR_LIST *dpList;

      dpList=_readCurveData(dc, nCurve, tsBegin, tsEnd);
      if (dpList) {
	const char *sCurveLabel;

	sCurveLabel=GWEN_XMLNode_GetProperty(nCurve, "title", NULL);
	DBG_DEBUG(NULL, "Adding data for %s", sCurveLabel?sCurveLabel:"<no title>");
	AQDG_TimeGraph_ModifyDataAndAddCurve(g, sCurveLabel?sCurveLabel:"<no title>", sModifier, dpList);
      }
    }
    nCurve=GWEN_XMLNode_FindNextTag(nCurve, "curve", NULL, NULL);
  }
}



AQDG_GRAPH_DATAPAIR_LIST *_readCurveData(AQH_DATACLIENT *dc, GWEN_XMLNODE *nCurve, uint64_t tsBegin, uint64_t tsEnd)
{
  const char *sDeviceName;
  const char *sValueName;

  sDeviceName=GWEN_XMLNode_GetProperty(nCurve, "device", NULL);
  sValueName=GWEN_XMLNode_GetProperty(nCurve, "value", NULL);

  if (sDeviceName && *sDeviceName && sValueName && *sValueName) {
    AQDG_GRAPH_DATAPAIR_LIST *dpList;
    GWEN_BUFFER *vbuf;

    vbuf=GWEN_Buffer_new(0, 64, 0, 1);
    GBAA(vbuf, "%s/%s", sDeviceName, sValueName);
    dpList=AQH_ModDevices_RequestDataPairList(dc, GWEN_Buffer_GetStart(vbuf), tsBegin, tsEnd, 100000);
    if (dpList) {
      GWEN_Buffer_free(vbuf);
      return dpList;
    }
    GWEN_Buffer_free(vbuf);
  }
  return NULL;
}



// TODO: move to aqhome.{c,h}
uint64_t _parseTime(const char *s)
{
  if (s && *s) {
    if (*s=='-') {
      uint64_t x=0;
      uint64_t now=time(NULL);

      s++;
      while(*s && isdigit(*s)) {
        unsigned int i;

        i=*(s++)-'0';
        x*=10;
        x+=i;
      }
      if (*s) {
        switch(*s) {
        case 0:
        case 'm': x*=60; break;
        case 'h': x*=(60*60); break;
        case 'd': x*=(60*60*24); break;
        case 'w': x*=(60*60*24*7); break;
        case 'M': x*=(60*60*24*30); break;
        case 'y': x*=(60*60*24*365); break;
        default:  break;
        }
      }
      return (now-x);
    }
    if (*s=='@') {
      int y, m, d, H, M, S;

      if (6==sscanf(s+1, "%d/%d/%d-%d:%d:%d", &y, &m, &d, &H, &M, &S)) {
        GWEN_TIMESTAMP *ts;
        uint64_t x=0;

        ts=GWEN_Timestamp_new(y, m, d, H, M, S);
        x=GWEN_Timestamp_toTimeT(ts);
        GWEN_Timestamp_free(ts);
        return x;
      }
      else {
        DBG_ERROR(NULL, "Invalid timespec [%s], expected: @YYYY/MM/DD-HH:MM:SS", s);
        return (uint64_t) (-1);
      }
    }
    else {
      unsigned long int x;

      if (1!=sscanf(s, "%lu", &x)) {
        DBG_ERROR(NULL, "ERROR: Invalid timestamp");
        return (uint64_t) (-1);
      }
      return (uint64_t) x;
    }
  }
  return 0;
}




GWEN_XMLNODE *_getSubItemNode(GWEN_XMLNODE *nPage, const char *sId, const char *sElementName)
{
  GWEN_XMLNODE *nItem;

  nItem=GWEN_XMLNode_FindFirstTag(nPage, "item", NULL, NULL);
  while(nItem) {
    GWEN_XMLNODE *nGraph;

    nGraph=GWEN_XMLNode_FindFirstTag(nItem, sElementName, "id", sId);
    if (nGraph)
      return nGraph;
    nItem=GWEN_XMLNode_FindNextTag(nItem, "item", NULL, NULL);
  }

  return NULL;
}



void _mkPathForGraph(AQCGI_MODULE *m, const char *sPageId, const char *sGraphId, GWEN_BUFFER *buf)
{
  AQCGI_SERVICE *sv;
  const char *s;

  sv=AQH_ModService_GetService(m);
  s=AQCGI_Service_GetCacheFolder(sv);
  GBAA(buf, "%s%s%s-%s", s, GWEN_DIR_SEPARATOR_S, sPageId, sGraphId);
  AQH_ModService_EscapeToBuffer(s, buf);
  GBAS(buf, ".png");
}



void _writePage(AQCGI_MODULE *m, AQH_DATACLIENT *dc, GWEN_XMLNODE *nPage, GWEN_BUFFER *dbuf)
{
  const char *sPageId;
  GWEN_XMLNODE *nItem;
  int layout;
  const char *s;

  sPageId=GWEN_XMLNode_GetProperty(nPage, "id", NULL);

  /* title */
  s=GWEN_XMLNode_GetProperty(nPage, "title", NULL);
  if (s && *s)
    GBAA(dbuf, "<h1>%s</h1>\n", s);

  layout=_layoutFromString(GWEN_XMLNode_GetProperty(nPage, "layout", "none"));
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "<table class=\"pageTable\">\n");
  if (layout==MY_LAYOUT_HORIZONTAL)
    GBAS(dbuf, "<tr>\n");
  nItem=GWEN_XMLNode_FindFirstTag(nPage, "item", NULL, NULL);
  while(nItem) {
    if (layout==MY_LAYOUT_VERTICAL)
      GBAS(dbuf, "<tr>\n");

    if (layout!=MY_LAYOUT_NONE)
      GBAS(dbuf, "<td>");
    _writeItem(m, dc, sPageId, nItem, dbuf);
    if (layout!=MY_LAYOUT_NONE)
      GBAS(dbuf, "</td>");

    if (layout==MY_LAYOUT_VERTICAL)
      GBAS(dbuf, "</tr>\n");
    nItem=GWEN_XMLNode_FindNextTag(nItem, "item", NULL, NULL);
  } /* while */
  if (layout==MY_LAYOUT_HORIZONTAL)
    GBAS(dbuf, "</tr>\n");
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "</table> <!-- pageTable -->\n");
}



void _writeItem(AQCGI_MODULE *m, AQH_DATACLIENT *dc, const char *sPageId, GWEN_XMLNODE *nItem, GWEN_BUFFER *dbuf)
{
  GWEN_XMLNODE *n;
  uint32_t perms;
  int layout;

  perms=AQH_ModService_GetUserPerms(m);
  layout=_layoutFromString(GWEN_XMLNode_GetProperty(nItem, "layout", "none"));
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "<table class=\"itemTable\">\n");
  if (layout==MY_LAYOUT_HORIZONTAL)
    GBAS(dbuf, "<tr>\n");
  n=GWEN_XMLNode_GetFirstTag(nItem);
  while(n) {
    const char *sName;

    if (layout==MY_LAYOUT_VERTICAL)
      GBAS(dbuf, "<tr>\n");
    sName=GWEN_XMLNode_GetData(n);
    if (sName && *sName) {
      if (strcasecmp(sName, "actor")==0) {
	if (perms && AQH_MODDEVICES_PERMS_VALUEWRITE)
	  _writeActor(dc, sPageId, n, layout, dbuf);
	else {
	  DBG_ERROR(NULL, "No permissions to write values");
	}

      }
      else if (strcasecmp(sName, "graph")==0)
	_writeGraph(sPageId, n, layout, dbuf);
      else {
	DBG_ERROR(NULL, "Ignoring element \"%s\"", sName);
      }
    }
    if (layout==MY_LAYOUT_VERTICAL)
      GBAS(dbuf, "</tr>\n");
    n=GWEN_XMLNode_GetNextTag(n);
  }
  if (layout==MY_LAYOUT_HORIZONTAL)
    GBAS(dbuf, "</tr>\n");
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "</table>  <!-- itemTable -->\n");
}



void _writeActor(AQH_DATACLIENT *dc, const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf)
{
  const char *sActorId;
  const char *sDeviceName;
  const char *sValueName;
  const char *sLabel;

  sActorId=GWEN_XMLNode_GetProperty(n, "id", NULL);
  sLabel=GWEN_XMLNode_GetProperty(n, "label", NULL);
  sDeviceName=GWEN_XMLNode_GetProperty(n, "device", NULL);
  sValueName=GWEN_XMLNode_GetProperty(n, "value", NULL);
  if (sActorId && *sActorId && sDeviceName && *sDeviceName && sValueName && *sValueName) {
    AQH_VALUE *value;

    value=AQH_ModDevices_GetValueForDevice(dc, sDeviceName, sValueName);
    if (value) {
      uint32_t lastData;

      if (layout!=MY_LAYOUT_NONE)
	GBAS(dbuf, "<td>\n");
      lastData=AQH_ModDevices_ValueGetLastDataAsUint32(dc, value, 0);
      GBAS(dbuf,"<form action=\"page.html\" method=\"post\">\n");
      GBAA(dbuf, "<input type=\"hidden\" name=\"page\"   value=\"%s\">\n", sPageId);
      GBAA(dbuf, "<input type=\"hidden\" name=\"actor\"  value=\"%s\">\n", sActorId);

      DBG_INFO(NULL, "Adding actor");
      if (sLabel && *sLabel)
	GBAA(dbuf,"<label for=\"%s\">%s</label>", sActorId, sLabel);
      if (layout!=MY_LAYOUT_NONE) {
	GBAS(dbuf, "</td>\n");
	GBAS(dbuf, "<td>\n");
      }
      switch(AQH_Value_GetModality(value)) {
      case AQH_ValueModality_RGBW:      _writeRgbwToForm(sActorId, lastData, dbuf);      break;
      case AQH_ValueModality_OnOff:     _writeOnOffToForm(sActorId, lastData, dbuf);     break;
      case AQH_ValueModality_OnOffAuto: _writeOnOffAutoToForm(sActorId, lastData, dbuf); break;
      default:                          GBAA(dbuf, "%d", lastData);                      break;
      } /* switch */
      if (layout!=MY_LAYOUT_NONE) {
	GBAS(dbuf, "</td>\n");
	GBAS(dbuf, "<td>\n");
      }
      GBAS(dbuf,"<input type=\"submit\" name=\"action\" value=\"Send\"/>");
      GBAS(dbuf, "</form>\n\n");
      if (layout!=MY_LAYOUT_NONE)
	GBAS(dbuf, "</td>\n");
    }
  }
}



void _writeGraph(const char *sPageId, GWEN_XMLNODE *n, int layout, GWEN_BUFFER *dbuf)
{
  const char *sGraphId;
  int w;
  int h;

  w=GWEN_XMLNode_GetIntProperty(n, "width", AQH_MODDEVICES_GRAPH_WIDTH);
  h=GWEN_XMLNode_GetIntProperty(n, "height", AQH_MODDEVICES_GRAPH_HEIGHT);
  sGraphId=GWEN_XMLNode_GetProperty(n, "id", NULL);
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "<td colspan=\"3\">\n");
  if (sGraphId && *sGraphId)
    _addGraphLink(sPageId, sGraphId, w, h, dbuf);
  if (layout!=MY_LAYOUT_NONE)
    GBAS(dbuf, "</td>\n");
}




void _addGraphLink(const char *sPageId, const char *sGraphId, int w, int h, GWEN_BUFFER *dbuf)
{
  GBAS(dbuf, "<img src=\"pgraph.html?page=");
  AQH_ModService_EscapeToBuffer(sPageId, dbuf);
  GBAS(dbuf, "&graph=");
  AQH_ModService_EscapeToBuffer(sGraphId, dbuf);
  GBAA(dbuf, "\" alt=\"%s\" width=\"%d\" height=\"%d\"", sGraphId, w, h);
  GBAS(dbuf, "/>");
}







void _writeRgbwToForm(const char *sValueName, uint32_t color, GWEN_BUFFER *dbuf)
{
#if 1
  DBG_ERROR(NULL, "Color=%08x (%d, %d, %d, %d)",
	    color,
	    AQH_ModDevices_RgbwGetR(color),
	    AQH_ModDevices_RgbwGetG(color),
	    AQH_ModDevices_RgbwGetB(color),
	    AQH_ModDevices_RgbwGetW(color));
  GBAA(dbuf, "<label for=\"%s_r\">R:</label>", sValueName);
  GBAA(dbuf, "<input type=\"number\" min=\"0\" max=\"255\" name=\"%s_r\" id=name=\"%s_r\" value=\"%d\">",
       sValueName, sValueName, AQH_ModDevices_RgbwGetR(color));

  GBAA(dbuf, "<label for=\"%s_g\">G:</label>", sValueName);
  GBAA(dbuf, "<input type=\"number\" min=\"0\" max=\"255\" name=\"%s_g\" id=name=\"%s_g\" value=\"%d\">",
       sValueName, sValueName, AQH_ModDevices_RgbwGetG(color));

  GBAA(dbuf, "<label for=\"%s_b\">B:</label>", sValueName);
  GBAA(dbuf, "<input type=\"number\" min=\"0\" max=\"255\" name=\"%s_b\" id=name=\"%s_b\" value=\"%d\">",
       sValueName, sValueName, AQH_ModDevices_RgbwGetB(color));

  GBAA(dbuf, "<label for=\"%s_w\">W:</label>", sValueName);
  GBAA(dbuf, "<input type=\"number\" min=\"0\" max=\"255\" name=\"%s_w\" id=name=\"%s_w\" value=\"%d\">",
       sValueName, sValueName, AQH_ModDevices_RgbwGetW(color));

#else
  GBAA(dbuf, "<input type=\"text\" name=\"%s\" id=\"%s\" value=\"#%08x\"/>", sValueName, sValueName, color);
  // else
  GBAA(dbuf, "<input type=\"color\" name=\"%s\" id=\"%s\" value=\"#%08x\"/>#%08x (#%08x)",
       sValueName, sValueName,
       AQH_ModDevices_RgbwToHtmlColor(color),
       AQH_ModDevices_RgbwToHtmlColor(color),
       color);
#endif
}



void _setRgbwData(AQH_DATACLIENT *dc, GWEN_DB_NODE *dbPost, const char *sValueName, const AQH_VALUE *value)
{
  const char *sValueSystemName;
  uint32_t color;
  int rv;

  sValueSystemName=AQH_Value_GetNameForSystem(value);
  DBG_INFO(NULL, "Set value %s", sValueName);
  color=AQH_ModDevices_RgbwFromComponents(_getColorComponent(dbPost, sValueName, "r", 0),
					  _getColorComponent(dbPost, sValueName, "g", 0),
					  _getColorComponent(dbPost, sValueName, "b", 0),
					  _getColorComponent(dbPost, sValueName, "w", 0));
  DBG_INFO(NULL, "Send value [#%08x] to %s", color, sValueSystemName);
  rv=AQH_DataClient_SetData(dc, value, (double) color);
  if (rv<0) {
    DBG_INFO(NULL, "Error sending data: %d", rv);
  }
}



int _getColorComponent(GWEN_DB_NODE *dbPost, const char *sValueName, const char *sComponent, int defaultValue)
{
  GWEN_BUFFER *buf;
  const char *sData;
  int result;

  buf=GWEN_Buffer_new(0, 64, 0, 1);
  GBAA(buf, "%s_%s", sValueName, sComponent);
  DBG_INFO(NULL, "Read value %s", GWEN_Buffer_GetStart(buf));
  sData=GWEN_DB_GetCharValue(dbPost, GWEN_Buffer_GetStart(buf), 0, NULL);
  GWEN_Buffer_free(buf);
  if (sData) {
    if (1==sscanf(sData, "%u", &result))
      return result;
  }
  return defaultValue;
}



void _writeOnOffToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf)
{
  GBAA(dbuf, "<select name=\"%s\" id=\"%s\">" "<option value=\"unchanged\" >unchanged</option>", sValueName, sValueName);
  GBAA(dbuf, "<option value=\"off\" %s>off</option>", (intVal==0)?"selected":"");
  GBAA(dbuf, "<option value=\"on\" %s>on</option>", (intVal==1)?"selected":"");
  GBAS(dbuf, "</select>");
}



void _setOnOffData(AQH_DATACLIENT *dc, const AQH_VALUE *value, const char *sValue)
{
  if (sValue) {
    const char *sValueSystemName;
    int rv;

    sValueSystemName=AQH_Value_GetNameForSystem(value);
    if (strcasecmp(sValue, "unchanged")==0) {
      DBG_INFO(NULL, "Value %s unchanged", sValueSystemName);
    }
    else if (strcasecmp(sValue, "on")==0) {
      DBG_INFO(NULL, "Send value 1 to %s", sValueSystemName);
      rv=AQH_DataClient_SetData(dc, value, 1.0);
      if (rv<0) {
        DBG_INFO(NULL, "Error sending data: %d", rv);
      }
    }
    else if (strcasecmp(sValue, "off")==0) {
      DBG_INFO(NULL, "Send value 0 to %s", sValueSystemName);
      rv=AQH_DataClient_SetData(dc, value, 0.0);
      if (rv<0) {
        DBG_INFO(NULL, "Error sending data: %d", rv);
      }
    }
    else {
    }
  }
}



void _writeOnOffAutoToForm(const char *sValueName, int intVal, GWEN_BUFFER *dbuf)
{
  GBAA(dbuf, "<select name=\"%s\" id=\"%s\" >" "<option value=\"unchanged\" >unchanged</option>", sValueName, sValueName);
  GBAA(dbuf, "<option value=\"off\" %s>off</option>", (intVal==0)?"selected":"");
  GBAA(dbuf, "<option value=\"on\" %s>on</option>", (intVal==1)?"selected":"");
  GBAA(dbuf, "<option value=\"auto\" %s>auto</option>", (intVal==2)?"selected":"");
  GBAS(dbuf, "</select>");
}



void _setOnOffAutoData(AQH_DATACLIENT *dc, const AQH_VALUE *value, const char *sValue)
{
  if (sValue) {
    const char *sValueSystemName;
    int rv;

    sValueSystemName=AQH_Value_GetNameForSystem(value);
    if (strcasecmp(sValue, "unchanged")==0) {
      DBG_INFO(NULL, "Value %s unchanged", sValueSystemName);
    }
    else if (strcasecmp(sValue, "on")==0) {
      DBG_INFO(NULL, "Send value 1 to %s", sValueSystemName);
      rv=AQH_DataClient_SetData(dc, value, 1.0);
      if (rv<0) {
        DBG_INFO(NULL, "Error sending data: %d", rv);
      }
    }
    else if (strcasecmp(sValue, "off")==0) {
      DBG_INFO(NULL, "Send value 0 to %s", sValueSystemName);
      rv=AQH_DataClient_SetData(dc, value, 0.0);
      if (rv<0) {
        DBG_INFO(NULL, "Error sending data: %d", rv);
      }
    }
    else if (strcasecmp(sValue, "auto")==0) {
      DBG_INFO(NULL, "Send value 2 to %s", sValueSystemName);
      rv=AQH_DataClient_SetData(dc, value, 2.0);
      if (rv<0) {
        DBG_INFO(NULL, "Error sending data: %d", rv);
      }
    }
    else {
      DBG_INFO(NULL, "Invalid value [%s] for %s", sValue, sValueSystemName);
    }
  }
}



void _sendPageList(AQCGI_MODULE *m, GWEN_BUFFER *dbuf)
{
  GWEN_STRINGLIST *sl;

  GBAS(dbuf, "<h1>Page List</h1>\n");
  sl=_listPageFiles(m);
  if (sl) {
    GWEN_STRINGLISTENTRY *se;

    GBAS(dbuf,
	 "<table class=\"datatable\">\n"
	 "<thead><tr><th>Page</th><th>Title</th></tr></thead>\n"
	 "<tbody>\n");
    se=GWEN_StringList_FirstEntry(sl);
    while(se) {
      const char *filename;

      filename=GWEN_StringListEntry_Data(se);
      if (filename && *filename) {
	GWEN_XMLNODE *node;

	DBG_ERROR(NULL, "Reading file \"%s\"", filename);
	node=_readPageFile(filename);
	if (node) {
	  GWEN_XMLNODE *nPage;

	  nPage=GWEN_XMLNode_FindFirstTag(node, "page", NULL, NULL);
	  if (nPage) {
	    const char *sId;
	    const char *sTitle;

	    sId=GWEN_XMLNode_GetProperty(nPage, "id", NULL);
	    sTitle=GWEN_XMLNode_GetProperty(nPage, "title", sId);
	    if (sId && *sId)
	      GBAA(dbuf, "<tr><td><a href=\"page.html?page=%s\">%s</a></td><td>%s</td></tr>\n", sId, sId, sTitle);
	  }
	  GWEN_XMLNode_free(node);
	}
      }
      se=GWEN_StringListEntry_Next(se);
    }
    GBAS(dbuf, "</tbody></table>");
    GWEN_StringList_free(sl);
  }
  else {
    GBAS(dbuf, "No pages.");
  }
}



GWEN_STRINGLIST *_listPageFiles(AQCGI_MODULE *m)
{
  GWEN_BUFFER *fbuf;
  AQCGI_SERVICE *sv;
  GWEN_STRINGLIST *sl;
  int rv;

  sv=AQH_ModService_GetService(m);
  fbuf=GWEN_Buffer_new(0, 256, 0, 1);
  GBAA(fbuf, "%s%spages", AQCGI_Service_GetRuntimeFolder(sv), GWEN_DIR_SEPARATOR_S);
  sl=GWEN_StringList_new();
  rv=GWEN_Directory_GetMatchingFilesRecursively(GWEN_Buffer_GetStart(fbuf), sl, "*.xml");
  if (rv<0) {
    DBG_INFO(NULL, "Error reading pages (%d)", rv);
    GWEN_StringList_free(sl);
    GWEN_Buffer_free(fbuf);
    return NULL;
  }
  if (GWEN_StringList_Count(sl)<1) {
    GWEN_StringList_free(sl);
    GWEN_Buffer_free(fbuf);
    return NULL;
  }
  GWEN_Buffer_free(fbuf);

  return sl;
}



GWEN_XMLNODE *_readPage(AQCGI_MODULE *m, const char *sPageName)
{
  GWEN_BUFFER *fbuf;
  AQCGI_SERVICE *sv;
  GWEN_XMLNODE *fileNode;

  sv=AQH_ModService_GetService(m);
  fbuf=GWEN_Buffer_new(0, 256, 0, 1);
  GBAA(fbuf, "%s%spages%s", AQCGI_Service_GetRuntimeFolder(sv), GWEN_DIR_SEPARATOR_S, GWEN_DIR_SEPARATOR_S);
  AQH_ModService_EscapeToBuffer(sPageName, fbuf);
  GBAS(fbuf, ".xml");

  fileNode=_readPageFile(GWEN_Buffer_GetStart(fbuf));
  if (fileNode==NULL) {
    DBG_INFO(NULL, "here");
    GWEN_Buffer_free(fbuf);
    return NULL;
  }

  GWEN_Buffer_free(fbuf);
  return fileNode;
}



GWEN_XMLNODE *_readPageFile(const char *sFilename)
{
  GWEN_XMLNODE *fileNode;
  int rv;

  fileNode=GWEN_XMLNode_new(GWEN_XMLNodeTypeTag, sFilename);
  rv=GWEN_XML_ReadFile(fileNode, sFilename, GWEN_XML_FLAGS_DEFAULT);
  if (rv<0) {
    DBG_ERROR(NULL, "Error reading \"%s\": %s (%d)", sFilename?sFilename:"<no name>", strerror(errno), errno);
    GWEN_XMLNode_free(fileNode);
    return NULL;
  }

  return fileNode;
}



int _layoutFromString(const char *s)
{
  if (s && *s) {
    if (strcasecmp(s, "none")==0)
      return MY_LAYOUT_NONE;
    else if (strcasecmp(s, "horizontal")==0)
      return MY_LAYOUT_HORIZONTAL;
    else if (strcasecmp(s, "vertical")==0)
      return MY_LAYOUT_VERTICAL;
  }
  return MY_LAYOUT_NONE;
}




