/*
  Paper sizes library.

  Copyright (c) 2021-2024 Reuben Thomas <rrt@sc3d.org>.

  This file is part of libpaper.

  This program is free software: you can redistribute it and/or modify it
  under the terms of the MIT license if ENABLE_RELOCATABLE is *undefined*,
  or otherwise under the terms of the GNU Lesser General Public License as
  published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this program.  If not, see
  <https://www.gnu.org/licenses/lgpl-2.1.html>.
*/

#include "config.h"

#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <locale.h>
#if defined _GNU_SOURCE && defined LC_PAPER && defined HAVE_NL_PAPER_CONSTANTS
#define USE_NL_PAPER
#include <langinfo.h>
#endif

#include "relocatable.h"

#include "paper.h"


/* Public variables to help with error messages. */
size_t paper_lineno;
char *paper_specsfile;


static const char *PAPERSIZE_FILENAME = "papersize";
static const char *PAPERSPECS_FILENAME = "paperspecs";
static struct paper *papers = NULL;
static struct paper *default_paper = NULL;
static char *xdg_config_home, *sysconfdir_alloc;
static const char *sysconfdir;
static bool free_xdg_config_home = false;

#define MAX_PAPERNAME_LEN 255


/* Paper size units. */
static struct {
    enum paper_unit unit;
    const char *name;
    double factor;
} units[] = {
    { PAPER_UNIT_PT, "pt", 1. },
    { PAPER_UNIT_MM, "mm", 72. * .1 / 2.54 },
    { PAPER_UNIT_IN, "in", 72. },
};

enum paper_unit paperunitfromname(const char *unit)
{
    for (enum paper_unit i = PAPER_UNIT_PT; i < PAPER_NUM_UNITS; i++)
        if (strcasecmp(units[i].name, unit) == 0)
            return i;

    return PAPER_UNIT_INVALID;
}

const char *paperunitname(enum paper_unit n)
{
    if (n >= PAPER_UNIT_PT && n < PAPER_NUM_UNITS)
        return units[n].name;
    return NULL;
}

double paperunitfactor(enum paper_unit n)
{
    if (n >= PAPER_UNIT_PT && n < PAPER_NUM_UNITS)
        return units[n].factor;
    return 0.0;
}


#ifndef HAVE_STRNDUP
char *strndup(const char *s, size_t n) {
    char *t = malloc(n + 1);
    if (t == NULL)
        return NULL;
    strncpy(t, s, n);
    t[n] = '\0';
    return t;
}
#endif

/* Get the next line from a stream. */
char *getln(FILE *fp)
{
    size_t len = 1024, n = 0;
    char *l = malloc(len + 1), *newl;
    if (l == NULL)
        return NULL;

    int c;
    for (c = getc(fp); c != '\n' && c != EOF; c = getc(fp)) {
        if (n == len) {
            newl = realloc(l, (len *= 2) + 1);
            if (newl == NULL) {
                free(l);
                return NULL;
            }
            l = newl;
        }
        l[n++] = c;
    }

    l[n] = '\0';
    return l;
}

/* Join a file name to a directory to make a path. */
char *file_join(char const *dir, char const *file)
{
    size_t dirlen = strlen(dir);
    char *res = malloc(dirlen + 1 + strlen(file) + 1);
    if (res == NULL)
        return NULL;
    strcpy(res, dir);
    if ((dir[dirlen - 1]) != '/')
        strcat(res, "/");
    return strcat(res, file);
}

/* Return next comma-separated token from a string.
   On the first call, p points to the string, and saveptr is ignored;
   on subsequent calls, p should be NULL, and saveptr unchanged since
   the previous call.
   NULL is returned when no token is available, or on memory allocation
   failure. */
static char *gettok(char *p, char **saveptr) {
    if (p == NULL)
        p = *saveptr;
    /* Skip leading space. */
    for (; isspace(*p); p++)
        ;
    size_t i = 0;
    /* Find following comma or end of line. */
    for (; isprint(p[i]) && p[i] != ','; i++)
        ;
    *saveptr = p + i + (p[i] == ',');
    /* Trim trailing space. */
    while (i > 0 && isspace(p[i - 1]))
        i--;
    if (i == 0)
        return NULL;
    return strndup(p, i);
}


/* Paper list type. */
struct paper {
    const char *name;
    enum paper_unit unit;
    double width, height;
    struct paper *next;
};

/* Accessor functions. */
const char *papername(const struct paper *paper)
{
    return paper->name;
}

double paperwidth(const struct paper *paper)
{
    return paper->width;
}

double paperheight(const struct paper *paper)
{
    return paper->height;
}

enum paper_unit paperunit(const struct paper *paper)
{
    return paper->unit;
}

double paperpswidth(const struct paper *paper)
{
    return paper->width * paperunitfactor(paper->unit);
}

double paperpsheight(const struct paper *paper)
{
    return paper->height * paperunitfactor(paper->unit);
}

const struct paper *paperfirst(void)
{
    if (paperinit() != PAPER_OK)
        return NULL;
    return papers;
}

const struct paper *papernext(const struct paper *paper)
{
    return paper->next;
}

/* Constructor. */
static int readspecs(struct paper **papers_list, const char *specsfile, struct paper **last, size_t *lineno) {
    *lineno = 0;
    char *old_locale = setlocale(LC_ALL, NULL);
    if (old_locale != NULL)
        old_locale = strdup(old_locale);
    setlocale(LC_ALL, "C");
    int ret = PAPER_OK;
    FILE *ps = fopen(specsfile, "r");
    if (ps != NULL) {
        struct paper *prev = *papers_list, *p;
        char *l;
        for (*lineno = 1; (l = getln(ps)) != NULL && strlen(l) > 0; prev = p, (*lineno)++) {
            char *saveptr;
            char *name = gettok(l, &saveptr);
            char *wstr = gettok(NULL, &saveptr), *hstr = gettok(NULL, &saveptr);
            char *ustr = gettok(NULL, &saveptr);
            if (name != NULL && wstr != NULL && hstr != NULL && ustr != NULL) {
                errno = 0;
                double w = strtod(wstr, NULL);
                if (w == 0 || errno != 0) {
                    ret = PAPER_BAD_WIDTH;
                    goto err;
                }
                double h = strtod(hstr, NULL);
                if (h == 0 || errno != 0) {
                    ret = PAPER_BAD_HEIGHT;
                    goto err;
                }
                enum paper_unit u;
                if ((u = paperunitfromname(ustr)) == PAPER_UNIT_INVALID) {
                    ret = PAPER_BAD_UNIT;
                    goto err;
                }
                p = calloc(1, sizeof(struct paper));
                if (last != NULL)
                    *last = p;
                if (p == NULL) {
                    ret = PAPER_NOMEM;
                    goto err;
                }
                *p = (struct paper){name, u, w, h, NULL};
                if (prev != NULL) /* Add to end of list. */
                    prev->next = p;
                else /* This is the first item. */
                    prev = *papers_list = p;
            } else
                ret = PAPER_MISSING_FIELD;
        err:
            free(wstr);
            free(hstr);
            free(ustr);
            if (ret != PAPER_OK) {
                free(name);
                break;
            }
            if (feof(ps))
                break;
        }
        fclose(ps);
        free(l);
    }

    if (old_locale != NULL) {
        setlocale(LC_ALL, old_locale);
        free(old_locale);
    }
    return ret;
}

/* Find paper by name. */
_GL_ATTRIBUTE_PURE const struct paper *paperinfo(const char *name)
{
    if (paperinit() != PAPER_OK)
        return NULL;

    for (const struct paper *p = paperfirst(); p != NULL; p = papernext(p))
        if (strcasecmp(name, papername(p)) == 0)
            return p;

    return NULL;
}

/* Find paper by size. */
_GL_ATTRIBUTE_PURE const struct paper *paperwithsize(double pswidth, double psheight)
{
    if (paperinit() != PAPER_OK)
        return NULL;

    for (const struct paper* p = paperfirst(); p != NULL; p = papernext(p))
        if (paperpswidth(p) == pswidth && paperpsheight(p) == psheight)
            return p;

    return NULL;
}


/* Get locale default paper size. */
static const char *localepapername(void) {
#ifdef USE_NL_PAPER
    if (setlocale(LC_PAPER, "") != NULL) {
#define NL_PAPER_GET(x)                                                 \
        ((union { char *string; unsigned word; })nl_langinfo(x)).word
#define MM_TO_PT(v) (unsigned)((v * 72 / 2.54 / 10) + 0.5)
        unsigned w = MM_TO_PT(NL_PAPER_GET(_NL_PAPER_WIDTH));
        unsigned h = MM_TO_PT(NL_PAPER_GET(_NL_PAPER_HEIGHT));
        for (const struct paper *p = paperfirst(); p != NULL; p = papernext(p))
            if ((unsigned)(paperpswidth(p) + 0.5) == w && (unsigned)(paperpsheight(p) + 0.5) == h)
                return papername(p);
    }
#endif

    return NULL;
}

/* Parse a paper size from a paperconf file. */
static const char *papernamefile(const char *file)
{
    static char papername[MAX_PAPERNAME_LEN + 1];
    FILE *fp = fopen(file, "r");
    if (fp == NULL)
        return NULL;
    char *l = getln(fp);
    if (l != NULL && strlen(l) > 0) {
        char *saveptr, *s;
        s = gettok(l, &saveptr);
        if (s)
            strncpy(papername, s, MAX_PAPERNAME_LEN);
        papername[MAX_PAPERNAME_LEN] = '\0';
        free(s);
        free(l);
    }
    fclose(fp);
    return papername;
}

/* Return the default configured paper name, as per paper(1). */
const char *defaultpapername(void) {
    if (paperinit() != PAPER_OK)
        return NULL;

    const char *paperstr = getenv("PAPERSIZE");
    if (paperstr == NULL && xdg_config_home != NULL) {
        char *papersize = file_join(xdg_config_home, PAPERSIZE_FILENAME);
        if (papersize != NULL) {
            paperstr = papernamefile(papersize);
            free(papersize);
        }
    }
    if (paperstr == NULL)
        paperstr = localepapername();
    if (paperstr == NULL) {
        char *papersize = file_join(sysconfdir, PAPERSIZE_FILENAME);
        if (papersize != NULL) {
            paperstr = papernamefile(papersize);
            free(papersize);
        }
    }
    if (paperstr == NULL && default_paper != NULL)
        paperstr = papername(default_paper);

    return paperstr;
}

/* Alias for defaultpapername; its return value must be freed! */
char *systempapername(void) {
    const char *papername = defaultpapername();
    if (papername == NULL)
        return NULL;
    return strdup(papername);
}

/* Get the default paper size. */
const struct paper *defaultpaper(void)
{
    const char *papername = defaultpapername();
    if (papername == NULL)
        return NULL;
    return paperinfo(papername);
}


/* Initialize the library. */
/* Although the documentation requires callers to call paperinit(), this was
   not historically necessary, and many don't, so we take care of it. */
static int initialized = false;
int paperinit(void)
{
    if (initialized)
        return PAPER_OK;
    initialized = true;

    int ret = PAPER_OK;

    /* Read system paperspecs. */
    struct paper *system_papers = NULL;
    sysconfdir = relocate2("/etc", &sysconfdir_alloc);
    if (sysconfdir != NULL) {
        char *system_paperspecs = file_join(sysconfdir, PAPERSPECS_FILENAME);
        if (system_paperspecs == NULL)
            return PAPER_NOMEM;
        ret = readspecs(&system_papers, system_paperspecs, NULL, &paper_lineno);
        free(paper_specsfile);
        paper_specsfile = system_paperspecs;
    }

    /* Set default paper to first system paper, if any. */
    if (system_papers != NULL)
        default_paper = system_papers;

    /* Read user paperspecs. */
    xdg_config_home = getenv("XDG_CONFIG_HOME");
    if (xdg_config_home == NULL) {
        char *home = getenv("HOME");
        if (home != NULL) {
            free_xdg_config_home = true;
            xdg_config_home = file_join(home, ".config");
            if (xdg_config_home == NULL)
                return PAPER_NOMEM;
        }
    }
    char *user_paperspecs = NULL;
    if (xdg_config_home != NULL) {
        user_paperspecs = file_join(xdg_config_home, PAPERSPECS_FILENAME);
        if (user_paperspecs == NULL)
            return PAPER_NOMEM;
    }
    struct paper *last_paper = NULL;
    if (user_paperspecs != NULL) {
        size_t user_lineno;
        int user_ret = readspecs(&papers, user_paperspecs, &last_paper, &user_lineno);
        if (ret == PAPER_OK) {
            ret = user_ret;
            free(user_paperspecs);
        } else if (paper_lineno == 0) {
            free(paper_specsfile);
            paper_specsfile = user_paperspecs;
            paper_lineno = user_lineno;
        } else
            free(user_paperspecs);
    }

    if (papers != NULL) {
        /* If no default paper yet, set it to first user paper. */
        if (default_paper == NULL)
            default_paper = papers;

        /* Concatenate system papers to user papers. */
        if (last_paper != NULL)
            last_paper->next = system_papers;
        else
            last_paper = system_papers;
    }

    if (papers == NULL) /* System papers are all we have. */
        papers = system_papers;

    return ret;
}

/* Shut down the library. */
int paperdone(void)
{
    if (initialized) {
        free(sysconfdir_alloc);
        sysconfdir = sysconfdir_alloc = NULL;
        if (free_xdg_config_home)
            free(xdg_config_home);
        xdg_config_home = NULL;
        initialized = false;
    }
    return PAPER_OK;
}

/* Set the prefix directory for relocation. */
void papersetprefixdir(const char *new_prefix)
{
#if ENABLE_RELOCATABLE
    set_relocation_prefix(REAL_INSTALLPREFIX, new_prefix);
#else
    (void)new_prefix;
#endif
}
