/*============================================================================
Copyright (c) 2024 Raspberry Pi
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the copyright holder nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
============================================================================*/

#include <locale.h>
#include <gtk/gtk.h>
#include <glib/gstdio.h>
#include <libxml/xpathInternals.h>
#include "raindrop.h"

#define XC(str) ((xmlChar *) str)

/*----------------------------------------------------------------------------*/
/* Global data */
/*----------------------------------------------------------------------------*/

const char *orients[4] = { "normal", "90", "180", "270" };

/*----------------------------------------------------------------------------*/
/* Function prototypes */
/*----------------------------------------------------------------------------*/

void update_labwc_system_config (void);
static void add_mode (int monitor, int w, int h, float f);
void load_labwc_config (void);
static gboolean copy_profile (FILE *fp, FILE *foutp, int nmons);
static int write_config (FILE *fp);
static void merge_configs (const char *infile, const char *outfile);
void save_labwc_config (void);
void init_labwc_config (void);
void reload_labwc_config (void);
void revert_labwc_config (void);
static void read_touchscreen_xml (char *filename);
void load_labwc_touchscreens (void);
static void write_touchscreens (char *filename);
void save_labwc_touchscreens (void);
void reload_labwc_touchscreens (void);
void revert_labwc_touchscreens (void);

/*----------------------------------------------------------------------------*/
/* System update */
/*----------------------------------------------------------------------------*/

void update_labwc_system_config (void)
{
    char *cmd;

    system (SUDO_PREFIX "mkdir -p /etc/xdg/labwc-greeter/");

    cmd = g_strdup_printf (SUDO_PREFIX "cp %s/kanshi/config /etc/xdg/labwc-greeter/config.kanshi",
        g_get_user_config_dir ());
    system (cmd);
    g_free (cmd);

    cmd = g_strdup_printf (SUDO_PREFIX "cp %s/labwc/rcgreeter.xml /etc/xdg/labwc-greeter/rc.xml",
        g_get_user_config_dir ());
    system (cmd);
    g_free (cmd);
}

/*----------------------------------------------------------------------------*/
/* Loading initial config */
/*----------------------------------------------------------------------------*/

static void add_mode (int monitor, int w, int h, float f)
{
    output_mode_t *mod;
    mod = g_new0 (output_mode_t, 1);
    mod->width = w;
    mod->height = h;
    mod->freq = f;
    mod->interlaced = FALSE;
    mons[monitor].modes = g_list_append (mons[monitor].modes, mod);
}

void load_labwc_config (void)
{
    FILE *fp;
    char *line, *cptr;
    size_t len;
    int mon, w, h, i;
    float f;

    char *loc = g_strdup (setlocale (LC_NUMERIC, ""));
    setlocale (LC_NUMERIC, "C");

    mon = -1;

    fp = popen ("wlr-randr", "r");
    if (fp)
    {
        line = NULL;
        len = 0;
        while (getline (&line, &len, fp) != -1)
        {
            if (line[0] != ' ')
            {
                mon++;
                cptr = line;
                while (*cptr != ' ') cptr++;
                *cptr = 0;
                mons[mon].name = g_strdup (line);
                if (strstr (line, "NOOP"))
                {
                    // add virtual modes for VNC display
                    add_mode (mon, 640, 480, 0);
                    add_mode (mon, 720, 480, 0);
                    add_mode (mon, 800, 600, 0);
                    add_mode (mon, 1024, 768, 0);
                    add_mode (mon, 1280, 720, 0);
                    add_mode (mon, 1280, 1024, 0);
                    add_mode (mon, 1600, 1200, 0);
                    add_mode (mon, 1920, 1080, 0);
                    add_mode (mon, 2048, 1080, 0);
                    add_mode (mon, 2560, 1440, 0);
                    add_mode (mon, 3200, 1800, 0);
                    add_mode (mon, 3840, 2160, 0);
                }
            }
            else if (line[2] != ' ')
            {
                if (strstr (line, "Position"))
                {
                    sscanf (line, "  Position: %d,%d", &w, &h);
                    mons[mon].x = w;
                    mons[mon].y = h;
                }
                else if (strstr (line, "Transform"))
                {
                    for (i = 0; i < 4; i++)
                        if (strstr (line, orients[i])) mons[mon].rotation = i * 90;
                }
                else if (strstr (line, "Enabled"))
                {
                    if (strstr (line, "no")) mons[mon].enabled = FALSE;
                    else mons[mon].enabled = TRUE;
                }
                else if (strstr (line, "Scale"))
                {
                    sscanf (line, "  Scale: %f", &f);
                    mons[mon].scale = f;
                }
            }
            else if (line[4] != ' ')
            {
                sscanf (line, "    %dx%d px, %f Hz", &w, &h, &f);
                add_mode (mon, w, h, f);
                if ((mons[mon].enabled && strstr (line, "current"))
                    || (! mons[mon].enabled && strstr (line, "preferred")))
                {
                    mons[mon].width = w;
                    mons[mon].height = h;
                    mons[mon].freq = f;
                }
            }
        }
        free (line);
        pclose (fp);
    }

    setlocale (LC_NUMERIC, loc);
    g_free (loc);
}

/*----------------------------------------------------------------------------*/
/* Writing config */
/*----------------------------------------------------------------------------*/

static gboolean copy_profile (FILE *fp, FILE *foutp, int nmons)
{
    char *line;
    size_t len;
    gboolean valid = FALSE;
    char *buf, *tmp;
    int m;

    line = NULL;
    len = 0;
    while (getline (&line, &len, fp) != -1)
    {
        if (strstr (line, "profile"))
        {
            valid = TRUE;
            buf = g_strdup (line);
        }
        else if (valid)
        {
            if (strstr (line, "}"))
            {
                tmp = g_strdup_printf ("%s}\n", buf);
                g_free (buf);
                buf = tmp;

                if (nmons) fprintf (foutp, "%s\n", buf);
                g_free (buf);
                return TRUE;
            }
            else if (strstr (line, "output"))
            {
                tmp = g_strdup_printf ("%s%s", buf, line);
                g_free (buf);
                buf = tmp;

                for (m = 0; m < MAX_MONS; m++)
                {
                    if (mons[m].modes == NULL) continue;
                    if (strstr (line, mons[m].name)) nmons--;
                }
            }
        }
    }
    free (line);
    return FALSE;
}

static int write_config (FILE *fp)
{
    int m, nmons = 0;

    char *loc = g_strdup (setlocale (LC_NUMERIC, ""));
    setlocale (LC_NUMERIC, "C");

    fprintf (fp, "profile {\n");
    for (m = 0; m < MAX_MONS; m++)
    {
        if (mons[m].modes == NULL) continue;
        nmons++;
        if (mons[m].enabled == FALSE)
        {
            fprintf (fp, "\t\toutput %s disable\n", mons[m].name);
        }
        else if (mons[m].freq == 0.0)
        {
            fprintf (fp, "\t\toutput %s enable scale %f mode --custom %dx%d position %d,%d transform %s\n",
                mons[m].name, mons[m].scale, mons[m].width, mons[m].height,
                mons[m].x, mons[m].y, orients[mons[m].rotation / 90]);
        }
        else
        {
            fprintf (fp, "\t\toutput %s enable scale %f mode %dx%d@%.3f position %d,%d transform %s\n",
                mons[m].name, mons[m].scale, mons[m].width, mons[m].height, mons[m].freq,
                mons[m].x, mons[m].y, orients[mons[m].rotation / 90]);
        }
    }
    fprintf (fp, "}\n\n");

    setlocale (LC_NUMERIC, loc);
    g_free (loc);

    return nmons;
}

static void merge_configs (const char *infile, const char *outfile)
{
    FILE *finp = fopen (infile, "r");
    FILE *foutp = fopen (outfile, "w");

    // write the profile for this config
    int nmons = write_config (foutp);

    // copy any other profiles
    while (copy_profile (finp, foutp, nmons));

    fclose (finp);
    fclose (foutp);
}

void save_labwc_config (void)
{
    char *infile, *outfile, *inifile, *cmd;

    infile = g_build_filename (g_get_user_config_dir (), "kanshi/config.bak", NULL);
    outfile = g_build_filename (g_get_user_config_dir (), "kanshi/config", NULL);

    // check if a valid config file exists
    cmd = g_strdup_printf ("grep -q profile %s", outfile);
    if (!system (cmd))
    {
        // config file - initialise bak from it
        g_free (cmd);
        cmd = g_strdup_printf ("cp %s %s", outfile, infile);
        system (cmd);
    }
    else
    {
        // null config file - initialise bak from ini file
        g_free (cmd);
        inifile = g_build_filename (g_get_user_config_dir (), "kanshi/config.init", NULL);
        cmd = g_strdup_printf ("cp %s %s", inifile, infile);
        system (cmd);
    }
    g_free (cmd);

    merge_configs (infile, outfile);
    g_free (infile);
    g_free (outfile);
}

void init_labwc_config (void)
{
    FILE *fp;
    char *file, *cmd;

    // check the config directory exists
    file = g_build_filename (g_get_user_config_dir (), "kanshi/", NULL);
    g_mkdir_with_parents (file, S_IRUSR | S_IWUSR | S_IXUSR);
    g_free (file);

    // look for an existing valid config file - if there is one, fall out
    file = g_build_filename (g_get_user_config_dir (), "kanshi/config", NULL);
    cmd = g_strdup_printf ("grep -q profile %s", file);
    g_free (file);
    if (!system (cmd))
    {
        g_free (cmd);
        return;
    }
    g_free (cmd);

    // no valid config file - create an init file
    file = g_build_filename (g_get_user_config_dir (), "kanshi/config.init", NULL);
    fp = fopen (file, "w");
    write_config (fp);
    fclose (fp);
    g_free (file);
}

/*----------------------------------------------------------------------------*/
/* Reload / reversion */
/*----------------------------------------------------------------------------*/

void reload_labwc_config (void)
{
    system ("pkill --signal SIGHUP kanshi");
}

void revert_labwc_config (void)
{
    char *infile, *outfile, *cmd;

    infile = g_build_filename (g_get_user_config_dir (), "kanshi/config.bak", NULL);
    outfile = g_build_filename (g_get_user_config_dir (), "kanshi/config", NULL);
    cmd = g_strdup_printf ("cp %s %s", infile, outfile);
    system (cmd);
    g_free (cmd);
    g_free (infile);
    g_free (outfile);
}

/*----------------------------------------------------------------------------*/
/* Touchscreens */
/*----------------------------------------------------------------------------*/

static void read_touchscreen_xml (char *filename)
{
    xmlDocPtr xDoc;
    xmlXPathObjectPtr xpathObj;
    xmlXPathContextPtr xpathCtx;
    xmlNode *node;
    xmlAttr *attr;
    char *dev, *mon;
    int i, m;
    touch_mode_t mode;
    GList *model;
    gboolean exists;

    if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) return;

    xmlInitParser ();
    LIBXML_TEST_VERSION
    xDoc = xmlReadFile (filename, NULL, XML_PARSE_NOBLANKS);
    if (xDoc == NULL)
    {
        xmlCleanupParser ();
        return;
    }

    xpathCtx = xmlXPathNewContext (xDoc);
    xmlXPathRegisterNs (xpathCtx, XC ("o"), XC ("http://openbox.org/3.4/rc"));

    xpathObj = xmlXPathEvalExpression (XC ("/o:openbox_config/o:touch"), xpathCtx);
    if (!xmlXPathNodeSetIsEmpty (xpathObj->nodesetval))
    {
        for (i = 0; i < xpathObj->nodesetval->nodeNr; i++)
        {
            node = xpathObj->nodesetval->nodeTab[i];
            dev = NULL;
            mon = NULL;
            mode = MODE_MULTITOUCH;
            for (attr = node->properties; attr; attr = attr->next)
            {
                if (!g_strcmp0 ((char *) attr->name, "deviceName"))
                    dev = g_strdup ((char *) attr->children->content);
                if (!g_strcmp0 ((char *) attr->name, "mapToOutput"))
                    mon = g_strdup ((char *) attr->children->content);
                if (!g_strcmp0 ((char *) attr->name, "mouseEmulation"))
                {
                    if (!g_strcmp0 ((char *) attr->children->content, "yes")) mode = MODE_MOUSEEMU;
                    else mode = MODE_MULTITOUCH;
                }
            }
            if (dev && mon)
            {
                exists = FALSE;
                model = touchscreens;
                while (model)
                {
                    if (!g_strcmp0 ((char *) model->data, dev))
                    {
                        exists = TRUE;
                        break;
                    }
                    model = model->next;
                }
                if (exists)
                {
                    for (m = 0; m < MAX_MONS; m++)
                    {
                        if (mons[m].modes == NULL) continue;
                        if (!g_strcmp0 (mons[m].name, mon))
                        {
                            mons[m].touchscreen = g_strdup (dev);
                            mons[m].tmode = mode;
                        }
                        else if (!g_strcmp0 (dev, mons[m].touchscreen))
                        {
                            g_free (mons[m].touchscreen);
                            mons[m].touchscreen = NULL;
                            mons[m].tmode = MODE_NONE;
                        }
                    }
                }
            }
            g_free (dev);
            g_free (mon);
        }
    }
    xmlXPathFreeObject (xpathObj);

    // cleanup XML
    xmlXPathFreeContext (xpathCtx);
    xmlFreeDoc (xDoc);
    xmlCleanupParser ();
}

void load_labwc_touchscreens (void)
{
    char *infile;

    read_touchscreen_xml ("/etc/xdg/labwc/rc.xml");

    infile = g_build_filename (g_get_user_config_dir (), "labwc/rc.xml", NULL);
    read_touchscreen_xml (infile);
    g_free (infile);
}

static void write_touchscreens (char *filename)
{
    xmlDocPtr xDoc;
    xmlNode *root, *child_node;
    xmlXPathObjectPtr xpathObj;
    xmlXPathContextPtr xpathCtx;
    char *cptr;
    int m, i;

    xmlInitParser ();
    LIBXML_TEST_VERSION
    if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
    {
        xDoc = xmlReadFile (filename, NULL, XML_PARSE_NOBLANKS);
        if (!xDoc) xDoc = xmlNewDoc (XC ("1.0"));
    }
    else xDoc = xmlNewDoc (XC ("1.0"));
    xpathCtx = xmlXPathNewContext (xDoc);
    xmlXPathRegisterNs (xpathCtx, XC ("o"), XC ("http://openbox.org/3.4/rc"));

    // check the root node exists
    xpathObj = xmlXPathEvalExpression (XC ("/o:openbox_config"), xpathCtx);
    if (xmlXPathNodeSetIsEmpty (xpathObj->nodesetval))
    {
        root = xmlNewNode (NULL, XC ("openbox_config"));
        xmlNewNs (root, XC ("http://openbox.org/3.4/rc"), NULL);
        xmlDocSetRootElement (xDoc, root);
    }
    else root = xpathObj->nodesetval->nodeTab[0];
    xmlXPathFreeObject (xpathObj);

    // clear any existing associations for current monitors in the XML
    for (m = 0; m < MAX_MONS; m++)
    {
        if (mons[m].modes == NULL) continue;

        cptr = g_strdup_printf ("/o:openbox_config/o:touch[@mapToOutput='%s']", mons[m].name);
        xpathObj = xmlXPathEvalExpression (XC (cptr), xpathCtx);
        g_free (cptr);

        if (xpathObj->nodesetval)
        {
            for (i = 0; i < xpathObj->nodesetval->nodeNr; i++)
            {
                xmlUnlinkNode (xpathObj->nodesetval->nodeTab[i]);
                xmlFreeNode (xpathObj->nodesetval->nodeTab[i]);
            }
        }
        xmlXPathFreeObject (xpathObj);
    }

    for (m = 0; m < MAX_MONS; m++)
    {
        if (mons[m].modes == NULL) continue;
        if (mons[m].touchscreen == NULL) continue;

        cptr = g_strdup_printf ("/o:openbox_config/o:touch[@deviceName='%s']", mons[m].touchscreen);
        xpathObj = xmlXPathEvalExpression (XC (cptr), xpathCtx);
        g_free (cptr);

        if (xmlXPathNodeSetIsEmpty (xpathObj->nodesetval))
        {
            child_node = xmlNewChild (root, NULL, XC ("touch"), NULL);
            xmlSetProp (child_node, XC ("deviceName"), XC (mons[m].touchscreen));
        }
        else child_node = xpathObj->nodesetval->nodeTab[0];

        xmlSetProp (child_node, XC ("mapToOutput"), XC (mons[m].name));
        xmlSetProp (child_node, XC ("mouseEmulation"), mons[m].tmode == MODE_MOUSEEMU ? XC ("yes") : XC ("no"));
        xmlXPathFreeObject (xpathObj);
    }

    xmlXPathFreeContext (xpathCtx);
    xmlSaveFormatFile (filename, xDoc, 1);
    xmlFreeDoc (xDoc);
    xmlCleanupParser ();
}

void save_labwc_touchscreens (void)
{
    char *infile, *outfile, *cmd;

    char *dir = g_build_filename (g_get_user_config_dir (), "labwc/", NULL);
    g_mkdir_with_parents (dir, S_IRUSR | S_IWUSR | S_IXUSR);
    g_free (dir);

    infile = g_build_filename (g_get_user_config_dir (), "labwc/rc.bak", NULL);
    outfile = g_build_filename (g_get_user_config_dir (), "labwc/rc.xml", NULL);
    cmd = g_strdup_printf ("cp %s %s", outfile, infile);
    system (cmd);
    g_free (cmd);
    write_touchscreens (outfile);
    g_free (infile);
    g_free (outfile);

    outfile = g_build_filename (g_get_user_config_dir (), "labwc/rcgreeter.xml", NULL);
    cmd = g_strdup_printf ("cp /etc/xdg/labwc-greeter/rc.xml %s", outfile);
    system (cmd);
    g_free (cmd);
    write_touchscreens (outfile);
    g_free (outfile);
}

void reload_labwc_touchscreens (void)
{
    system ("labwc --reconfigure");
}

void revert_labwc_touchscreens (void)
{
    char *infile, *outfile, *cmd;

    infile = g_build_filename (g_get_user_config_dir (), "labwc/rc.bak", NULL);
    outfile = g_build_filename (g_get_user_config_dir (), "labwc/rc.xml", NULL);
    cmd = g_strdup_printf ("cp %s %s", infile, outfile);
    system (cmd);
    g_free (cmd);
    g_free (infile);
    g_free (outfile);
}

/*----------------------------------------------------------------------------*/
/* Function table */
/*----------------------------------------------------------------------------*/

wm_functions_t labwc_functions = {
    .init_config = init_labwc_config,
    .load_config = load_labwc_config,
    .load_touchscreens = load_labwc_touchscreens,
    .save_config = save_labwc_config,
    .save_touchscreens = save_labwc_touchscreens,
    .reload_config = reload_labwc_config,
    .reload_touchscreens = reload_labwc_touchscreens,
    .revert_config = revert_labwc_config,
    .revert_touchscreens = revert_labwc_touchscreens,
    .update_system_config = update_labwc_system_config
};

/* End of file */
/*============================================================================*/
