/* $OpenBSD$ */

/*
* Copyright (c) 2010 Nicholas Marriott <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <sys/types.h>

#include <ctype.h>
#include <string.h>

#include "tmux.h"

static struct layout_cell       *layout_find_bottomright(struct layout_cell *);
static u_short                   layout_checksum(const char *);
static int                       layout_append(struct layout_cell *, char *,
                                    size_t);
static struct layout_cell       *layout_construct(struct layout_cell *,
                                    const char **);
static void                      layout_assign(struct window_pane **,
                                    struct layout_cell *);

/* Find the bottom-right cell. */
static struct layout_cell *
layout_find_bottomright(struct layout_cell *lc)
{
       if (lc->type == LAYOUT_WINDOWPANE)
               return (lc);
       lc = TAILQ_LAST(&lc->cells, layout_cells);
       return (layout_find_bottomright(lc));
}

/* Calculate layout checksum. */
static u_short
layout_checksum(const char *layout)
{
       u_short csum;

       csum = 0;
       for (; *layout != '\0'; layout++) {
               csum = (csum >> 1) + ((csum & 1) << 15);
               csum += *layout;
       }
       return (csum);
}

/* Dump layout as a string. */
char *
layout_dump(struct layout_cell *root)
{
       char    layout[8192], *out;

       *layout = '\0';
       if (layout_append(root, layout, sizeof layout) != 0)
               return (NULL);

       xasprintf(&out, "%04hx,%s", layout_checksum(layout), layout);
       return (out);
}

/* Append information for a single cell. */
static int
layout_append(struct layout_cell *lc, char *buf, size_t len)
{
       struct layout_cell     *lcchild;
       char                    tmp[64];
       size_t                  tmplen;
       const char             *brackets = "][";

       if (len == 0)
               return (-1);

       if (lc->wp != NULL) {
               tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%u,%u,%u",
                   lc->sx, lc->sy, lc->xoff, lc->yoff, lc->wp->id);
       } else {
               tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%u,%u",
                   lc->sx, lc->sy, lc->xoff, lc->yoff);
       }
       if (tmplen > (sizeof tmp) - 1)
               return (-1);
       if (strlcat(buf, tmp, len) >= len)
               return (-1);

       switch (lc->type) {
       case LAYOUT_LEFTRIGHT:
               brackets = "}{";
               /* FALLTHROUGH */
       case LAYOUT_TOPBOTTOM:
               if (strlcat(buf, &brackets[1], len) >= len)
                       return (-1);
               TAILQ_FOREACH(lcchild, &lc->cells, entry) {
                       if (layout_append(lcchild, buf, len) != 0)
                               return (-1);
                       if (strlcat(buf, ",", len) >= len)
                               return (-1);
               }
               buf[strlen(buf) - 1] = brackets[0];
               break;
       case LAYOUT_WINDOWPANE:
               break;
       }

       return (0);
}

/* Check layout sizes fit. */
static int
layout_check(struct layout_cell *lc)
{
       struct layout_cell      *lcchild;
       u_int                    n = 0;

       switch (lc->type) {
       case LAYOUT_WINDOWPANE:
               break;
       case LAYOUT_LEFTRIGHT:
               TAILQ_FOREACH(lcchild, &lc->cells, entry) {
                       if (lcchild->sy != lc->sy)
                               return (0);
                       if (!layout_check(lcchild))
                               return (0);
                       n += lcchild->sx + 1;
               }
               if (n - 1 != lc->sx)
                       return (0);
               break;
       case LAYOUT_TOPBOTTOM:
               TAILQ_FOREACH(lcchild, &lc->cells, entry) {
                       if (lcchild->sx != lc->sx)
                               return (0);
                       if (!layout_check(lcchild))
                               return (0);
                       n += lcchild->sy + 1;
               }
               if (n - 1 != lc->sy)
                       return (0);
               break;
       }
       return (1);
}

/* Parse a layout string and arrange window as layout. */
int
layout_parse(struct window *w, const char *layout, char **cause)
{
       struct layout_cell      *lc, *lcchild;
       struct window_pane      *wp;
       u_int                    npanes, ncells, sx = 0, sy = 0;
       u_short                  csum;

       /* Check validity. */
       if (sscanf(layout, "%hx,", &csum) != 1) {
               *cause = xstrdup("invalid layout");
               return (-1);
       }
       layout += 5;
       if (csum != layout_checksum(layout)) {
               *cause = xstrdup("invalid layout");
               return (-1);
       }

       /* Build the layout. */
       lc = layout_construct(NULL, &layout);
       if (lc == NULL) {
               *cause = xstrdup("invalid layout");
               return (-1);
       }
       if (*layout != '\0') {
               *cause = xstrdup("invalid layout");
               goto fail;
       }

       /* Check this window will fit into the layout. */
       for (;;) {
               npanes = window_count_panes(w);
               ncells = layout_count_cells(lc);
               if (npanes > ncells) {
                       xasprintf(cause, "have %u panes but need %u", npanes,
                           ncells);
                       goto fail;
               }
               if (npanes == ncells)
                       break;

               /* Fewer panes than cells - close the bottom right. */
               lcchild = layout_find_bottomright(lc);
               layout_destroy_cell(w, lcchild, &lc);
       }

       /*
        * It appears older versions of tmux were able to generate layouts with
        * an incorrect top cell size - if it is larger than the top child then
        * correct that (if this is still wrong the check code will catch it).
        */
       switch (lc->type) {
       case LAYOUT_WINDOWPANE:
               break;
       case LAYOUT_LEFTRIGHT:
               TAILQ_FOREACH(lcchild, &lc->cells, entry) {
                       sy = lcchild->sy + 1;
                       sx += lcchild->sx + 1;
               }
               break;
       case LAYOUT_TOPBOTTOM:
               TAILQ_FOREACH(lcchild, &lc->cells, entry) {
                       sx = lcchild->sx + 1;
                       sy += lcchild->sy + 1;
               }
               break;
       }
       if (lc->type != LAYOUT_WINDOWPANE && (lc->sx != sx || lc->sy != sy)) {
               log_debug("fix layout %u,%u to %u,%u", lc->sx, lc->sy, sx,sy);
               layout_print_cell(lc, __func__, 0);
               lc->sx = sx - 1; lc->sy = sy - 1;
       }

       /* Check the new layout. */
       if (!layout_check(lc)) {
               *cause = xstrdup("size mismatch after applying layout");
               goto fail;
       }

       /* Resize to the layout size. */
       window_resize(w, lc->sx, lc->sy, -1, -1);

       /* Destroy the old layout and swap to the new. */
       layout_free_cell(w->layout_root);
       w->layout_root = lc;

       /* Assign the panes into the cells. */
       wp = TAILQ_FIRST(&w->panes);
       layout_assign(&wp, lc);

       /* Update pane offsets and sizes. */
       layout_fix_offsets(w);
       layout_fix_panes(w, NULL);
       recalculate_sizes();

       layout_print_cell(lc, __func__, 0);

       notify_window("window-layout-changed", w);

       return (0);

fail:
       layout_free_cell(lc);
       return (-1);
}

/* Assign panes into cells. */
static void
layout_assign(struct window_pane **wp, struct layout_cell *lc)
{
       struct layout_cell      *lcchild;

       switch (lc->type) {
       case LAYOUT_WINDOWPANE:
               layout_make_leaf(lc, *wp);
               *wp = TAILQ_NEXT(*wp, entry);
               return;
       case LAYOUT_LEFTRIGHT:
       case LAYOUT_TOPBOTTOM:
               TAILQ_FOREACH(lcchild, &lc->cells, entry)
                       layout_assign(wp, lcchild);
               return;
       }
}

/* Construct a cell from all or part of a layout tree. */
static struct layout_cell *
layout_construct(struct layout_cell *lcparent, const char **layout)
{
       struct layout_cell     *lc, *lcchild;
       u_int                   sx, sy, xoff, yoff;
       const char             *saved;

       if (!isdigit((u_char) **layout))
               return (NULL);
       if (sscanf(*layout, "%ux%u,%u,%u", &sx, &sy, &xoff, &yoff) != 4)
               return (NULL);

       while (isdigit((u_char) **layout))
               (*layout)++;
       if (**layout != 'x')
               return (NULL);
       (*layout)++;
       while (isdigit((u_char) **layout))
               (*layout)++;
       if (**layout != ',')
               return (NULL);
       (*layout)++;
       while (isdigit((u_char) **layout))
               (*layout)++;
       if (**layout != ',')
               return (NULL);
       (*layout)++;
       while (isdigit((u_char) **layout))
               (*layout)++;
       if (**layout == ',') {
               saved = *layout;
               (*layout)++;
               while (isdigit((u_char) **layout))
                       (*layout)++;
               if (**layout == 'x')
                       *layout = saved;
       }

       lc = layout_create_cell(lcparent);
       lc->sx = sx;
       lc->sy = sy;
       lc->xoff = xoff;
       lc->yoff = yoff;

       switch (**layout) {
       case ',':
       case '}':
       case ']':
       case '\0':
               return (lc);
       case '{':
               lc->type = LAYOUT_LEFTRIGHT;
               break;
       case '[':
               lc->type = LAYOUT_TOPBOTTOM;
               break;
       default:
               goto fail;
       }

       do {
               (*layout)++;
               lcchild = layout_construct(lc, layout);
               if (lcchild == NULL)
                       goto fail;
               TAILQ_INSERT_TAIL(&lc->cells, lcchild, entry);
       } while (**layout == ',');

       switch (lc->type) {
       case LAYOUT_LEFTRIGHT:
               if (**layout != '}')
                       goto fail;
               break;
       case LAYOUT_TOPBOTTOM:
               if (**layout != ']')
                       goto fail;
               break;
       default:
               goto fail;
       }
       (*layout)++;

       return (lc);

fail:
       layout_free_cell(lc);
       return (NULL);
}