/*
 * CSS2 parser for qemacs.
 *
 * Copyright (c) 2000-2002 Fabrice Bellard.
 * Copyright (c) 2007-2026 Charlie Gordon.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "util.h"
#include "css.h"

//#define DEBUG

#if 0
#ifdef DEBUG
#define dprintf(fmt, args...) printf(fmt, ##args)
#else
#define dprintf(fmt, args...)
#define NDEBUG
#endif
#define ddprintf(fmt, args...)
#else
#ifdef DEBUG
#define dprintf(fmt, ...)  printf(fmt, ##__VA_ARGS__)
#else
#define dprintf(...)
#define NDEBUG
#endif
#define ddprintf(...)
#endif

#include <assert.h>

static void css_error1(CSSParseState *b, const char *fmt, ...)
    qe__attr_printf(2,3);

static void css_error1(CSSParseState *b, const char *fmt, ...)
{
    char buf[1024];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    css_error(b->error_opaque, b->filename, b->line_num, buf);
}

/***********************************************************/
/* CSS property parser */

/* return a length and its unit. some normalization is done to limit
   the number of units. Return non zero if error. */
static int css_get_length(int *length_ptr, int *unit_ptr, const char *p)
{
    int num, len, unit;
    const char *p1;
    char buf[32];
    double f;

    // FIXME: use strtod directly
    p1 = p;
    if (*p == '+' || *p == '-')
        p++;
    while (qe_isdigit(*p))
        p++;
    if (*p == '.') {
        p++;
        while (qe_isdigit(*p))
            p++;
    }
    len = p - p1;
    if (len == 0)
        return -1;
    /* CG: use pstrncpy */
    if (len > (int)sizeof(buf) - 1)
        len = (int)sizeof(buf) - 1;
    memcpy(buf, p1, len);
    buf[len] = '\0';
    f = strtod(buf, NULL);
    /* CG: should use define or global for consistency with
     * CSS_UNIT_xxx values */
    unit = css_get_enum(p, "px,%,ex,em,mm,in,cm,pt,pc");
    if (unit < 0) {
        /* only 0 is valid without unit */
        if (f != 0.0 || *p != '\0')
            return -1;
        num = 0;
        unit = CSS_UNIT_NONE;
    } else {
        unit++;
        if (f < 0 && unit != CSS_UNIT_PERCENT)
            return -1;
        switch (unit) {
        case CSS_UNIT_PIXEL:
            num = (int)(f);
            break;
        case CSS_UNIT_PERCENT:
            if (f < 0) {
                /* CG: ain't wrong ? */
                if (f >= 100)
                    return -1;
                f = 100 - f;
            }
            num = (int)(f * (0.01 * CSS_LENGTH_FRAC_BASE));
            break;
        case CSS_UNIT_EX:
        case CSS_UNIT_EM:
            num = (int)(f * CSS_LENGTH_FRAC_BASE);
            break;
        case CSS_UNIT_IN:
        phys_unit:
            unit = CSS_UNIT_IN;
            num = (int)(f * CSS_LENGTH_FRAC_BASE);
            break;
        case CSS_UNIT_MM:
            f = f / 25.4;
            goto phys_unit;
        case CSS_UNIT_CM:
            f = f / 2.54;
            goto phys_unit;
        case CSS_UNIT_PT:
            f = f / 72.0;
            goto phys_unit;
        case CSS_UNIT_PC:
            f = f * 12.0 / 72.0;
            goto phys_unit;
        default:
            return -1;
        }
    }
    *length_ptr = num;
    *unit_ptr = unit;
    return 0;
}

/* return the font size in CSS_UNIT_IN corresponding to size 0 <= i <= 6 */
/* XXX: change name */
int get_font_size(int i)
{
    int val;

    val = (14 * CSS_LENGTH_FRAC_BASE) / 72;
    i -= 2;
    /* compute 1.2^(i-2) */
    while (i > 0) {
        val = (val * 12) / 10;
        i--;
    }
    while (i < 0) {
        val = (val * 10) / 12;
        i++;
    }
    return val;
}

/* XXX: expand string dynamically */
/* XXX: end of line ? */
static char *css_parse_string(const char **pp)
{
    char buf[4096], *q;
    const char *p;
    int sep, c;

    p = *pp;
    q = buf;
    sep = *p++;
    for (;;) {
        c = *p++;
        if (c == sep || c == '\0')
            break;
        if (c == '\\') {
            c = *p++;
            if (c == 'A') {
                c = CSS_CONTENT_EOL;
            }
            /* XXX: hex digits */
        }
        if ((q - buf) < (int)sizeof(buf) - 1)
            *q++ = c;
    }
    *q = '\0';
    //    printf("string='%s'\n", buf);
    *pp = p;
    return qe_strdup(buf);
}

/* add a given number of values */
void css_add_prop_values(CSSProperty ***last_prop,
                         int property_index,
                         int nb_values, CSSPropertyValue *val_ptr)
{
    CSSProperty *prop;

    prop = qe_malloc_hack(CSSProperty,
                          (nb_values - 1) * sizeof(CSSPropertyValue));
    if (!prop)
        return;
    prop->property = property_index;
    prop->next = NULL;
    prop->nb_values = nb_values;
    blockcpy(&prop->value, val_ptr, nb_values);

    **last_prop = prop;
    *last_prop = &prop->next;
}

void css_add_prop(CSSProperty ***last_prop,
                  int property_index, CSSPropertyValue *val_ptr)
{
    css_add_prop_values(last_prop, property_index, 1, val_ptr);
}

void css_add_prop_unit(CSSProperty ***last_prop,
                       int property_index, int type, int val)
{
    CSSPropertyValue value;
    value.type = type;
    value.u.val = val;
    css_add_prop(last_prop, property_index, &value);
}

void css_add_prop_int(CSSProperty ***last_prop,
                      int property_index, int val)
{
    css_add_prop_unit(last_prop, property_index, CSS_UNIT_NONE, val);
}

static const char border_style_enum[] =
"none,hidden,dotted,dashed,solid,double,groove,ridge,inset,outset";
static const char list_style_enum[] =
"disc,circle,square,decimal,lower-alpha,upper-alpha,lower-roman,upper-roman,none";

#define MAX_ARGS 32

/* parse the properties and return a list of properties. NOTE: 'b' is
   only used for error reporting */
CSSProperty *css_parse_properties(CSSParseState *b, const char *props_str)
{
    const char *p;
    char property[64];
    char buf[1024], buf2[64];
    int property_index, type, val, nb_args, i, unit;
    int property_index1;
    CSSPropertyValue args[MAX_ARGS];
    CSSProperty **last_prop, *first_prop;
    const CSSPropertyDef *def;

    val = 0;
    first_prop = NULL;
    last_prop = &first_prop;
    p = props_str;
    for (;;) {
        get_str(&p, property, sizeof(property), ":");
        if (*p == '\0')
            break;
        if (*p == ':')
            p++;
        qe_skip_spaces(&p);
        /* find the property */
        def = css_properties;
        for (;;) {
            if (def >= css_properties + NB_PROPERTIES) {
                css_error1(b, "unsupported property '%s'", property);
                /* property not found skip it: find next ';' */
                while (*p && *p != ';')
                    p++;
                goto next;
            }
            if (strequal(def->name, property))
                break;
            def++;
        }
        property_index = def - css_properties;
        type = def->type;

        nb_args = 0;
        for (;;) {
            /* get argument */
            qe_skip_spaces(&p);
            if (*p == ';' || *p == '\0')
                break;
            /* more than 1 argument only if wanted */
            if (nb_args >= 1 &&
                !(type & (CSS_TYPE_FOUR|CSS_TYPE_TWO|CSS_TYPE_ARGS)))
                break;
            if (nb_args >= 2 &&
                !(type & (CSS_TYPE_FOUR|CSS_TYPE_ARGS)))
                break;
            if (nb_args >= 4 &&
                !(type & CSS_TYPE_ARGS))
                break;
            if (nb_args >= MAX_ARGS)
                break;

            if (*p == '\"' || *p == '\'') {
                /* string parsing */
                /* if no string expected, continue parsing */
                if (!(type & CSS_TYPE_STRING))
                    goto next;
                args[nb_args].u.str = css_parse_string(&p);
                unit = CSS_VALUE_STRING;
                goto got_val;
            }

            if (type & CSS_TYPE_ATTR) {
                /* attr(x) support */
                if (strstart(p, "attr(", &p)) {
                    get_str(&p, buf, sizeof(buf), ");");
                    if (buf[0] != '\0') {
                        if (*p != ')')
                            goto next;
                        p++;
                        if (b->ignore_case)
                            qe_strtolower(buf, sizeof(buf), buf);
                        args[nb_args].u.attr_id = css_new_ident(buf);
                        unit = CSS_VALUE_ATTR;
                        goto got_val;
                    }
                }
            }

            if (type & CSS_TYPE_COUNTER) {
                /* counter(x[,type]) support */
                if (strstart(p, "counter(", &p)) {
                    get_str(&p, buf, sizeof(buf), ",);");
                    args[nb_args].u.counter.type = CSS_LIST_STYLE_TYPE_DECIMAL;
                    if (*p == ',') {
                        p++;
                        get_str(&p, buf2, sizeof(buf2), ");");
                        val = css_get_enum(buf2, list_style_enum);
                        if (val >= 0)
                            args[nb_args].u.counter.type = val;
                    }
                    if (*p != ')')
                        goto next;
                    p++;
                    args[nb_args].u.counter.counter_id = css_new_ident(buf);
                    unit = CSS_VALUE_COUNTER;
                    goto got_val;
                }
            }
            get_str(&p, buf, sizeof(buf), ";");

            unit = CSS_UNIT_NONE;
            if (type & CSS_TYPE_AUTO) {
                if (strequal(buf, "auto")) {
                    val = CSS_AUTO;
                    goto got_val;
                }
            }
            if (!(type & CSS_TYPE_NOINHERIT)) {
                if (strequal(buf, "inherit")) {
                    val = CSS_INHERIT;
                    goto got_val;
                }
            }
            if (type & CSS_TYPE_INTEGER) {
                const char *p1;
                val = strtol_c(buf, &p1, 0);
                if (*p1 == '\0') {
                    unit = CSS_VALUE_INTEGER;
                    goto got_val;
                }
            }
            if (type & CSS_TYPE_LENGTH) {
                if (!css_get_length(&val, &unit, buf))
                    goto got_val;
            }
            if (type & CSS_TYPE_BORDER_STYLE) {
                val = css_get_enum(buf, border_style_enum);
                if (val >= 0)
                    goto got_val;
            }
            if (type & CSS_TYPE_LIST_STYLE) {
                val = css_get_enum(buf, list_style_enum);
                if (val >= 0)
                    goto got_val;
            }
            if (type & CSS_TYPE_ENUM) {
                val = css_get_enum(buf, def->name + strlen(def->name) + 1);
                if (val >= 0)
                    goto got_val;
            }
            if (type & CSS_TYPE_ENUM_BITS) {
                val = css_get_enum_bits(buf, def->name + strlen(def->name) + 1);
                if (val >= 0)
                    goto got_val;
            }
            if (type & CSS_TYPE_IDENT) {
                val = css_new_ident(buf);
                unit = CSS_VALUE_IDENT;
                goto got_val;
            }
            if (type & CSS_TYPE_FONT_FAMILY) {
                val = css_get_font_family(buf);
                if (val == 0)
                    val = CSS_INHERIT;
                goto got_val;
            }
            if (type & CSS_TYPE_COLOR) {
                QEColor color;
                /* XXX: color parsing is not always discriminant */
                if (!css_get_color(&color, buf)) {
                    val = color;
                    unit = CSS_VALUE_COLOR;
                    goto got_val;
                }
            }
            css_error1(b, "unrecognized value '%s' for property '%s'",
                       buf, def->name);
            goto next;
        got_val:
            /* specific handling may be necessary. We do them here */
            switch (property_index) {
            case CSS_font_size:
                if (unit == CSS_UNIT_NONE) {
                    if (val == 7) {
                        /* smaller */
                        unit = CSS_UNIT_PERCENT;
                        val = (CSS_LENGTH_FRAC_BASE * 10) / 12;
                    } else if (val == 8) {
                        /* larger */
                        unit = CSS_UNIT_PERCENT;
                        val = (CSS_LENGTH_FRAC_BASE * 12) / 10;
                    } else if (val >= 0) {
                        unit = CSS_UNIT_IN;
                        val = get_font_size(val);
                    } else {
                    goto next;
                    }
                }
                break;
            case CSS_border:
            case CSS_border_left:
            case CSS_border_top:
            case CSS_border_right:
            case CSS_border_bottom:
                if (unit == CSS_VALUE_COLOR) {
                    property_index1 = property_index +
                        CSS_border_color - CSS_border;
                } else if (unit == CSS_UNIT_NONE) {
                    property_index1 = property_index +
                        CSS_border_style - CSS_border;
                } else {
                    property_index1 = property_index +
                        CSS_border_width - CSS_border;
                }
                args[0].type = unit;
                args[0].u.val = val;
                if (property_index == CSS_border) {
                    for (i = 0; i < 4; i++)
                        css_add_prop(&last_prop, property_index1 + 1 + i,
                                     &args[0]);
                } else {
                        css_add_prop(&last_prop, property_index1, &args[0]);
                }
                /* parse next args without storing them */
                continue;
            }

            args[nb_args].type = unit;
            if (unit != CSS_VALUE_STRING &&
                unit != CSS_VALUE_ATTR &&
                unit != CSS_VALUE_COUNTER) {
                args[nb_args].u.val = val;
            }
            nb_args++;
        }
        if (type & CSS_TYPE_SPECIAL)
            goto next;

        if (type & CSS_TYPE_FOUR) {
            CSSPropertyValue v1, v2, v3, v4;
            /* handle specifically the four args case */
            v1 = args[0];
            switch (nb_args) {
            case 1:
                args[1] = args[2] = args[3] = v1;
                break;
            case 2:
                v2 = args[1];
                args[1] = args[3] = v1;
                args[0] = args[2] = v2;
                break;
            case 3:
                v2 = args[1];
                v3 = args[2];
                args[1] = v1;
                args[0] = args[2] = v2;
                args[3] = v3;
                break;
            case 4:
            default:
                v2 = args[1];
                v3 = args[2];
                v4 = args[3];

                args[1] = v1;
                args[2] = v2;
                args[3] = v3;
                args[0] = v4;
                break;
            }
            for (i = 0; i < 4; i++)
                css_add_prop(&last_prop, property_index + 1 + i, &args[i]);
        } else if (type & CSS_TYPE_TWO) {
            if (nb_args == 1)
                args[1] = args[0];
            for (i = 0; i < 2; i++)
                css_add_prop(&last_prop, property_index + 1 + i, &args[i]);
        } else if (type & CSS_TYPE_ARGS) {
            /* unbounded number of args */
            css_add_prop_values(&last_prop, property_index, nb_args, args);
        } else {
            css_add_prop(&last_prop, property_index, &args[0]);
        }
    next:
        qe_skip_spaces(&p);
        if (*p != ';')
            break;
        p++;
    }
    return first_prop;
}

#ifdef DEBUG
void css_dump_properties(CSSProperty *prop)
{
    const CSSPropertyDef *def;
    int val, j;
    CSSPropertyValue *value;

    while (prop != NULL) {
        def = css_properties + prop->property;
        val = prop->value.u.val;
        printf("%s: ", def->name);
        if (val == CSS_AUTO) {
            printf("auto");
        } else if (val == CSS_INHERIT) {
            printf("inherit");
        } else {
            value = &prop->value;
            for (j = 0; j < prop-> nb_values;j++) {
                val = value->u.val;
                switch (value->type) {
                case CSS_UNIT_EX:
                    printf("%0.1fex", (double)val / CSS_LENGTH_FRAC_BASE);
                    break;
                case CSS_UNIT_EM:
                    printf("%0.1fem", (double)val / CSS_LENGTH_FRAC_BASE);
                    break;
                case CSS_UNIT_PERCENT:
                    printf("%0.0f%%", (double)val / CSS_LENGTH_FRAC_BASE);
                    break;
                case CSS_VALUE_COLOR:
                    printf("#%06X", val);
                    break;
                case CSS_UNIT_NONE:
                    {
                        const char *p, *p1;
                        int i, len;
                        char buf[100];

                        if (def->type & CSS_TYPE_BORDER_STYLE)
                            p = border_style_enum;
                        else
                            p = def->name + strlen(def->name) + 1;
                        i = 0;
                        for (;;) {
                            p1 = strchr(p, ',');
                            if (i == val) {
                                if (!p1)
                                    len = strlen(p);
                                else
                                    len = p1 - p;
                                memcpy(buf, p, len);
                                buf[len] = '\0';
                                printf("%s", buf);
                                break;
                            }
                            if (!p1) {
                                printf("[%d]", val);
                                break;
                            }
                            i++;
                            p = p1 + 1;
                        }
                    }
                    break;
                case CSS_VALUE_STRING:
                    printf("\"%s\"", value->u.str);
                    break;
                case CSS_VALUE_ATTR:
                    printf("attr(%s)", css_ident_str(value->u.attr_id));
                    break;
                case CSS_VALUE_COUNTER:
                    printf("counter(%s,%d)",
                           css_ident_str(value->u.counter.counter_id),
                           value->u.counter.type);
                    break;
                case CSS_VALUE_INTEGER:
                    printf("%d", val);
                    break;
                case CSS_VALUE_IDENT:
                    printf("%s", css_ident_str(value->u.attr_id));
                    break;
                default:
                    printf("[%d]", val);
                    break;
                }
                if (j != (prop->nb_values - 1))
                    printf(" ");
                value++;
            }
        }
        printf("; ");
        prop = prop->next;
    }
}
#endif

CSSStyleSheet *css_new_style_sheet(void)
{
    CSSStyleSheet *s;

    s = qe_mallocz(CSSStyleSheet);
    if (!s)
        return NULL;
    s->plast_entry = &s->first_entry;
    return s;
}

static void free_selector(CSSSimpleSelector *ss)
{
    while (ss->attrs) {
        CSSStyleSheetAttributeEntry *attr = ss->attrs;
        ss->attrs = attr->next;
        qe_free(&attr);
    }
}

/* XXX: free idents too */
void css_free_style_sheet(CSSStyleSheet **sp)
{
    if (*sp) {
        CSSStyleSheet *s = *sp;

        while (s->first_entry) {
            CSSStyleSheetEntry *e = s->first_entry;
            s->first_entry = e->next;

            while (e->sel.next) {
                CSSSimpleSelector *ss = e->sel.next;
                e->sel.next = ss->next;
                free_selector(ss);
                qe_free(&ss);
            }
            free_selector(&e->sel);

            while (e->props) {
                CSSProperty *p = e->props;
                e->props = p->next;
                qe_free(&p);
            }
            qe_free(&e);
        }
        qe_free(sp);
    }
}

static int bgetc1(CSSParseState *b)
{
    int ch;

    ch = (unsigned char)*b->ptr;
    if (ch) {
        b->ptr++;
        return ch;
    } else {
        return EOF;
    }
}

static int bgetc(CSSParseState *b)
{
    int ch, last;

    for (;;) {
        ch = bgetc1(b);
        if (ch != '/')
            return ch;

        ch = bgetc1(b);
        if (ch != '*') {
            if (ch != EOF)
                b->ptr--;
            return '/';
        }
        last = 0;
        for (;;) {
            ch = bgetc1(b);
            if (ch == EOF)
                return ch;
            if (ch == '/' && last == '*')
                break;
            last = ch;
        }
    }
}

/* XXX: read_string and read_ident not exactly CSS2 conformant */
static void read_string(CSSParseState *b, int *ch_ptr, char *ident, int ident_size)
{
    int ch, quote;
    char *q;

    quote = *ch_ptr;
    q = ident;
    for (;;) {
        ch = bgetc(b);
        if (ch == quote)
            break;
        if ((q - ident) < ident_size - 1)
            *q++ = ch;
    }
    *q = '\0';
    ch = bgetc(b);
    *ch_ptr = ch;
}

static void read_ident(CSSParseState *b, int *ch_ptr, char *ident, int ident_size)
{
    char *q;
    int c;

    c = *ch_ptr;
    q = ident;
    for (;;) {
        if (!(qe_isalnum_(c) || c == '*' || c == '-'))
            break;
        if ((q - ident) < ident_size - 1)
            *q++ = c;
        c = bgetc(b);
    }
    *q = '\0';
    *ch_ptr = c;
}

static void bskip_spaces(CSSParseState *b, int *ch_ptr)
{
    int c;

    c = *ch_ptr;
    while (qe_isspace(c))
        c = bgetc(b);
    *ch_ptr = c;
}

void add_attribute(CSSStyleSheetAttributeEntry ***last_attr,
                   CSSIdent attr, int op, const char *value)
{
    CSSStyleSheetAttributeEntry *ae;
    int len = strlen(value);

    ae = qe_malloc_hack(CSSStyleSheetAttributeEntry, len);
    ae->attr = attr;
    ae->op = op;
    memcpy(ae->value, value, len + 1);
    ae->next = NULL;
    **last_attr = ae;
    *last_attr = &ae->next;
}

/* add the tag in the hash table */
/* XXX: use ident hash table ? */
CSSStyleSheetEntry *add_style_entry(CSSStyleSheet *s,
                                    CSSSimpleSelector *ss,
                                    int media)
{
    CSSStyleSheetEntry *e, **pp;

    /* add the style sheet entry */
    e = qe_mallocz(CSSStyleSheetEntry);
    if (!e)
        return NULL;
    e->sel = *ss;
    e->media = media;

    /* add in entry list */
    *s->plast_entry = e;
    s->plast_entry = &e->next;
    e->next = NULL;
    /* add in tag hash table */
    pp = &s->tag_hash[css_hash_ident(e->sel.tag, CSS_TAG_HASH_SIZE)];
    while (*pp != NULL)
        pp = &(*pp)->hash_next;
    *pp = e;
    e->hash_next = NULL;
    return e;
}


/* copy a selector and its associated attributes */
static void dup_selector(CSSSimpleSelector *dest, CSSSimpleSelector *src)
{
    CSSStyleSheetAttributeEntry *attr, *first_attr, **plast_attr;

    first_attr = NULL;
    plast_attr = &first_attr;
    for (attr = src->attrs; attr != NULL; attr = attr->next)
        add_attribute(&plast_attr, attr->attr,
                      attr->op, attr->value);
    *dest = *src;
    dest->attrs = first_attr;
}

/* duplicate css properties */
static CSSProperty *dup_properties(CSSProperty *props)
{
    CSSProperty *p, *first_p, **plast_p;
    first_p = NULL;
    plast_p = &first_p;
    for (p = props; p != NULL; p = p->next) {
        css_add_prop_values(&plast_p, p->property, p->nb_values, &p->value);
    }
    return first_p;
}

/* merge style sheet 'a' in 's' */
void css_merge_style_sheet(CSSStyleSheet *s, CSSStyleSheet *a)
{
    CSSStyleSheetEntry *e, *e1;
    CSSSimpleSelector *ss, *ss1, ss2, **pss;

    for (e = a->first_entry; e != NULL; e = e->next) {
        /* add selector */
        dup_selector(&ss2, &e->sel);
        e1 = add_style_entry(s, &ss2, e->media);

        /* add selector operations */
        pss = &e1->sel.next;
        for (ss = e->sel.next; ss != NULL; ss = ss->next) {
            ss1 = qe_mallocz(CSSSimpleSelector);
            dup_selector(ss1, ss);
            *pss = ss1;
            pss = &ss1->next;
        }

        /* add css properties */
        e1->props = dup_properties(e->props);
    }
}

static void beat(CSSParseState *b, int *ch_ptr, const char *str)
{
    int ch;

    ch = *ch_ptr;
    while (*str) {
        if (ch == EOF || ch != *str)
            break;
        ch = bgetc(b);
        str++;
    }
    *ch_ptr = ch;
}

static void parse_simple_selector(CSSSimpleSelector *ss, // output only
                                  CSSParseState *b,
                                  int *ch_ptr)
{
    char value[1024];
    char tag[64];
    char tag_id[64];
    char attribute[64];
    int ch, pclass, val;
    CSSStyleSheetAttributeEntry *first_attr, **last_attr;

    ch = *ch_ptr;

    /* read the tag */
    first_attr = NULL;
    last_attr = &first_attr;
    tag[0] = '\0';
    tag_id[0] = '\0';
    pclass = 0;
    read_ident(b, &ch, tag, sizeof(tag));
    if (b->ignore_case)
        qe_strtolower(tag, sizeof(tag), tag);

    /* read '.class', '[xxx]', ':pseudo-class' */
    for (;;) {
        bskip_spaces(b, &ch);
        if (ch == '.') {
            /* read the class and add it as an attribute */
            ch = bgetc(b);
            read_ident(b, &ch, value, sizeof(value));
            add_attribute(&last_attr, CSS_ID_class, CSS_ATTR_OP_EQUAL, value);
        } else if (ch == '#') {
            /* read the id */
            ch = bgetc(b);
            read_ident(b, &ch, tag_id, sizeof(tag_id));
        } else if (ch == '[') {
            /* read the attribute */
            int op;
            ch = bgetc(b);
            read_ident(b, &ch, attribute, sizeof(attribute));
            if (b->ignore_case)
                qe_strtolower(attribute, sizeof(attribute), attribute);

            switch (ch) {
            case '~':
                op = CSS_ATTR_OP_IN_LIST;
                ch = bgetc(b);
                goto get_value;
            case '=':
                op = CSS_ATTR_OP_EQUAL;
                goto get_value;
            case '|':
                op = CSS_ATTR_OP_IN_HLIST;
                ch = bgetc(b);
            get_value:
                ch = bgetc(b);
                if (ch == '\"' || ch == '\'') {
                    read_string(b, &ch, value, sizeof(value));
                } else {
                    read_ident(b, &ch, value, sizeof(value));
                }
                break;
            case ']':
                op = CSS_ATTR_OP_SET;
                value[0] = '\0';
                break;
            default:
                dprintf("op: incorrect char '%c'\n", ch);
                return; /* cannot do more */
            }
            if (ch == ']')
                ch = bgetc(b);
            add_attribute(&last_attr, css_new_ident(attribute), op, value);
        } else if (ch == ':') {
            ch = bgetc(b);
            read_ident(b, &ch, value, sizeof(value));
            val = css_get_enum(value, "first-child,link,visited,active,hover,focus,first-line,first-letter,before,after");
            if (val >= 0)
                pclass |= 1 << val;
        } else {
            break;
        }
    }
    memset(ss, 0, sizeof(CSSSimpleSelector));
    if (tag[0] == '\0') {
        ss->tag = CSS_ID_ALL;
    } else {
        ss->tag = css_new_ident(tag);
    }
    if (tag_id[0] != '\0') {
        /* XXX: not fully correct, but good enough for now */
        add_attribute(&last_attr, CSS_ID_id, CSS_ATTR_OP_EQUAL, value);
        /* we also add the id, just in case we use it in the futur */
        ss->tag_id = css_new_ident(tag_id);
    }
    ss->attrs = first_attr;
    ss->pclasses = pclass;

    *ch_ptr = ch;
}


/* flags: only XML_IGNORE_CASE is handled */
void css_parse_style_sheet(CSSStyleSheet *s, CSSParseState *b)
{
    char value[1024];
    char tag[64];
    char tag_id[64];
    char *q;
    int ch, media, val, last_tree_op, i;
    CSSStyleSheetEntry *e, **first_eprops;
    CSSSimpleSelector ss2, *ss = &ss2, *last_ss, *ss1;
    CSSProperty *props;

    ch = bgetc(b);
    media = CSS_MEDIA_ALL;
    for (;;) {
    redo:
        first_eprops = s->plast_entry;
        bskip_spaces(b, &ch);
        if (ch == EOF)
            break;
        /* eat inserted HTML comments for compatible STYLE tag parsing */
        if (ch == '<') {
            beat(b, &ch, "<!--");
            goto redo;
        } else if (ch == '-') {
            beat(b, &ch, "-->");
            goto redo;
        }

        /* handle '@media { ... }' */
        if (ch == '@') {
            ch = bgetc(b);
            read_ident(b, &ch, tag, sizeof(tag));
            switch (css_get_enum(tag, "media,page")) {
            case 0:
                /* @media */
                media = 0;
                for (;;) {
                    bskip_spaces(b, &ch);
                    read_ident(b, &ch, tag, sizeof(tag));
                    val = css_get_enum(tag, "tty,screen,print,tv,speech,all");
                    if (val < 0 || val == 5)
                        media = CSS_MEDIA_ALL;
                    else
                        media |= (1 << val);
                    bskip_spaces(b, &ch);
                    if (ch == ',') {
                        ch = bgetc(b);
                    } else if (ch == '{' || ch == EOF) {
                        ch = bgetc(b);
                        break;
                    }
                }
                goto redo;
            case 1:
                /* @page */
                bskip_spaces(b, &ch);
                if (ch != '{') {
                    read_ident(b, &ch, tag_id, sizeof(tag_id));
                    bskip_spaces(b, &ch);
                }
                memset(ss, 0, sizeof(CSSSimpleSelector));
                ss->tag = css_new_ident("@page");
                if (tag_id[0] != '\0')
                    ss->tag_id = css_new_ident(tag_id);
                add_style_entry(s, ss, media);
                goto parse_props;
            default:
                css_error1(b, "unrecognized css directive '@%s'", tag);
                break;
            }
        } else if (ch == '}') {
            /* XXX: end of media, should unstack */
            ch = bgetc(b);
            goto redo;
        }

        /* parse a selector list */
        for (;;) {
            /* parse simple selectors with operations */
            last_ss = NULL;
            last_tree_op = CSS_TREE_OP_NONE;
            for (;;) {
                int tree_op;
                bskip_spaces(b, &ch);
                parse_simple_selector(ss, b, &ch);
                bskip_spaces(b, &ch);
                ss->tree_op = last_tree_op;
                ss->next = last_ss;
                if (ch == '+') {
                    tree_op = CSS_TREE_OP_PRECEEDED;
                    ch = bgetc(b);
                    goto add_tree;
                } else
                if (ch == '>') {
                    tree_op = CSS_TREE_OP_CHILD;
                    ch = bgetc(b);
                    goto add_tree;
                } else
                if (qe_isalpha(ch)) {
                    tree_op = CSS_TREE_OP_DESCENDANT;
                add_tree:
                    ss1 = qe_malloc_dup_array(ss, 1);
                    if (ss1) {
                        last_ss = ss1;
                    }
                    last_tree_op = tree_op;
                } else {
                    /* other char: exit */
                    break;
                }
            }
            add_style_entry(s, ss, media);

            /* get next selector, if present */
            if (ch != ',')
                break;
            ch = bgetc(b);
        }
    parse_props:
        /* expect start of properties */
        if (ch != '{')
            break;
        ch = bgetc(b);

        q = value;
        while (ch != '}' && ch != EOF) {
            if ((q - value) < (int)sizeof(value) - 1)
                *q++ = ch;
            ch = bgetc(b);
        }
        *q = '\0';

        if (ch == '}')
            ch = bgetc(b);

        /* the properties are extracted, now add them to each tag */
        /* XXX: should locate entries first, then add, to avoid adding
           duplicate entries */
        /* XXX: should put font properties first to avoid em/ex units
           problems, but it would still not be sufficient. */
        props = css_parse_properties(b, value);
        i = 0;
        for (e = *first_eprops; e != NULL; e = e->next) {
            if (i == 0)
                e->props = props;
            else
                e->props = dup_properties(props);
            i++;
        }
    }
#ifdef DEBUG
    css_dump_style_sheet(s);
#endif
}

void css_parse_style_sheet_str(CSSStyleSheet *s, void *error_opaque, const char *buffer, int flags)
{
    CSSParseState b1, *b = &b1;
    b->error_opaque = error_opaque;
    b->ptr = buffer;
    b->filename = "builtin";
    b->line_num = 1;
    b->ignore_case = flags & XML_IGNORE_CASE;
    css_parse_style_sheet(s, b);
}

#ifdef DEBUG
static void dump_selector(CSSSimpleSelector *ss)
{
    CSSStyleSheetAttributeEntry *ae;

    switch (ss->tree_op) {
    case CSS_TREE_OP_DESCENDANT:
        dump_selector(ss->next);
        printf(" ");
        break;
    case CSS_TREE_OP_CHILD:
        dump_selector(ss->next);
        printf(" > ");
        break;
    case CSS_TREE_OP_PRECEEDED:
        dump_selector(ss->next);
        printf(" + ");
        break;
    default:
        break;
    }

    printf("%s", css_ident_str(ss->tag));
    if (ss->pclasses & CSS_PCLASS_FIRST_CHILD)
        printf(":first-child");

    if (ss->tag_id)
        printf("#%s", css_ident_str(ss->tag_id));
    ae = ss->attrs;
    while (ae != NULL) {
        printf("[%s", css_ident_str(ae->attr));
        switch (ae->op) {
        case CSS_ATTR_OP_EQUAL:
            printf("=%s", ae->value);
            break;
        case CSS_ATTR_OP_IN_LIST:
            printf("~=%s", ae->value);
                break;
        case CSS_ATTR_OP_IN_HLIST:
            printf("|=%s", ae->value);
            break;
        }
        printf("]");
        ae = ae->next;
    }
}

void css_dump_style_sheet(CSSStyleSheet *s)
{
    CSSStyleSheetEntry *e;

    printf("<STYLE type=\"text/css\">\n");
    e = s->first_entry;
    while (e != NULL) {

        dump_selector(&e->sel);
        printf(" { ");
        css_dump_properties(e->props);
        printf("}\n");
        e = e->next;
    }
    printf("</STYLE>\n");
}
#endif
