/*======================================================================
 FILE: vcardrestriction.c
 CREATOR: Ken Murchison 24 Aug 2022

 SPDX-FileCopyrightText: 2022, Fastmail Pty. Ltd. (https://fastmail.com)
 SPDX-License-Identifier: LGPL-2.1-only OR MPL-2.0
 ======================================================================*/

/**
 * @file vcardrestriction.c
 * @brief Functions to check if a vcardcomponent meets the restrictions
 * imposed by the standard
 */

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

#include "vcardrestriction.h"
#include "icalerror_p.h"

#include <assert.h>

/* Define the structs for the restrictions. these data are filled out
in machine generated code below */

struct vcardrestriction_record;

typedef const char *(*restriction_func) (const struct vcardrestriction_record * rec,
                                         vcardcomponent *comp, vcardproperty *prop);

typedef struct vcardrestriction_record
{
    vcardproperty_version version;
    vcardcomponent_kind component;
    vcardproperty_kind property;
    vcardcomponent_kind subcomponent;
    vcardrestriction_kind restriction;
    restriction_func function;
} vcardrestriction_record;

static const vcardrestriction_record *vcardrestriction_get_restriction(
    const vcardrestriction_record *start,
    vcardproperty_version version, vcardcomponent_kind component,
    vcardproperty_kind property, vcardcomponent_kind subcomp);

static const vcardrestriction_record null_restriction_record =
    { VCARD_VERSION_NONE, VCARD_NO_COMPONENT,
      VCARD_NO_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_UNKNOWN, NULL };

/** Each row gives the result of comparing a restriction against a count.
   The columns in each row represent 0,1,2+. '-1' indicates
   'invalid, 'don't care' or 'needs more analysis' So, for
   VCARD_RESTRICTION_ONE, if there is 1 of a property with that
   restriction, it passes, but if there are 0 or 2+, it fails. */

static const bool compare_map[VCARD_RESTRICTION_UNKNOWN + 1][3] = {
    {1, 1, 1}, /*VCARD_RESTRICTION_NONE */
    {1, 0, 0}, /*VCARD_RESTRICTION_ZERO */
    {0, 1, 0}, /*VCARD_RESTRICTION_ONE */
    {1, 1, 1}, /*VCARD_RESTRICTION_ZEROPLUS */
    {0, 1, 1}, /*VCARD_RESTRICTION_ONEPLUS */
    {1, 1, 0}, /*VCARD_RESTRICTION_ZEROORONE */
    {1, 1, 0}, /*VCARD_RESTRICTION_ONEEXCLUSIVE */
    {1, 1, 0}, /*VCARD_RESTRICTION_ONEMUTUAL */
    {1, 1, 1}  /*VCARD_RESTRICTION_UNKNOWN */
};

static const char restr_string_map[VCARD_RESTRICTION_UNKNOWN + 1][60] = {
    "unknown number", /*VCARD_RESTRICTION_NONE */
    "0", /*VCARD_RESTRICTION_ZERO */
    "1", /*VCARD_RESTRICTION_ONE */
    "zero or more", /*VCARD_RESTRICTION_ZEROPLUS */
    "one or more", /*VCARD_RESTRICTION_ONEPLUS */
    "zero or one", /*VCARD_RESTRICTION_ZEROORONE */
    "zero or one, exclusive with another property", /*VCARD_RESTRICTION_ONEEXCLUSIVE */
    "zero or one, mutual with another property", /*VCARD_RESTRICTION_ONEMUTUAL */
    "unknown number"    /*VCARD_RESTRICTION_UNKNOWN */
};

int vcardrestriction_compare(vcardrestriction_kind restr, int count)
{
    /* restr is an unsigned int and VCARD_RESTRICTION_NONE == 0,
       so no need to check if restr < VCARD_RESTRICTION_NONE */
    if (restr > VCARD_RESTRICTION_UNKNOWN || count < 0) {
        return -1;
    }

    if (count > 2) {
        count = 2;
    }

    return compare_map[restr][count];
}

/* Special case routines */

#define TMP_BUF_SIZE 1024

static const char *vcardrestriction_validate_datetime_value(
    const vcardrestriction_record *rec,
    vcardcomponent *comp, vcardproperty *prop)
{
    vcardtimetype t = vcardproperty_get_bday(prop);
    static char buf[TMP_BUF_SIZE];

    if (vcardtime_is_null_datetime(t)) {
        return 0;
    }

    if (comp && vcardcomponent_get_version(comp) != VCARD_VERSION_40) {
        unsigned missing_time_parts = (unsigned)((t.hour < 0) + (t.minute < 0) + (t.second < 0));

        if (t.year < 0 || t.month < 0 || t.day < 0 ||
            (missing_time_parts && missing_time_parts != 3)) {

            snprintf(buf, TMP_BUF_SIZE,
                     "Failed restrictions for %s property. "
                     "The value must be a full date or date-time",
                     vcardproperty_kind_to_string(rec->property));
            return buf;
        }
    }

    if (!vcardtime_is_valid_time(t)) {
        snprintf(buf, TMP_BUF_SIZE,
                 "Failed restrictions for %s property. "
                 "The value is an invalid date-and-or-time",
                 vcardproperty_kind_to_string(rec->property));
        return buf;
    }

    return 0;
}

static const char *vcardrestriction_validate_timestamp_value(
    const vcardrestriction_record *rec,
    vcardcomponent *comp, vcardproperty *prop)
{
    _unused(comp);

    vcardtimetype t = vcardproperty_get_rev(prop);
    static char buf[TMP_BUF_SIZE];

    if (vcardtime_is_null_datetime(t)) {
        return 0;
    }

    if (!vcardtime_is_timestamp(t) || !vcardtime_is_valid_time(t)) {
        snprintf(buf, TMP_BUF_SIZE,
                 "Failed restrictions for %s property. "
                 "The value is an invalid timestamp",
                 vcardproperty_kind_to_string(rec->property));
        return buf;
    }

    return 0;
}

static int _check_restriction(vcardcomponent *comp,
                              const vcardrestriction_record *record,
                              int count, vcardproperty *prop)
{
    vcardrestriction_kind restr;
    const char *funcr = 0;
    int compare;

    restr = record->restriction;

    if (restr == VCARD_RESTRICTION_ONEEXCLUSIVE ||
        restr == VCARD_RESTRICTION_ONEMUTUAL) {

        /* First treat it as a 0/1 restriction */
        restr = VCARD_RESTRICTION_ZEROORONE;
    }

    compare = vcardrestriction_compare(restr, count);

    assert(compare != -1);

    if (compare == 0) {
        char temp[TMP_BUF_SIZE];
        vcardproperty *errProp;
        vcardparameter *errParam;
        const char *type, *kind;

        if (record->subcomponent != VCARD_NO_COMPONENT) {
            type = "component";
            kind = vcardcomponent_kind_to_string(record->subcomponent);
        } else {
            type = "property";
            kind = vcardproperty_kind_to_string(record->property);
        }

        snprintf(temp, TMP_BUF_SIZE,
                 "Failed restrictions for %s %s. "
                 "Expected %s instances of the %s and got %d",
                 kind, type, restr_string_map[restr], type, count);
        errParam =
            vcardparameter_new_xlicerrortype(VCARD_XLICERRORTYPE_RESTRICTIONCHECK);
        errProp = vcardproperty_vanew_xlicerror(temp, errParam, (void *) 0);
        vcardcomponent_add_property(comp, errProp);
        vcardproperty_free(errProp);
    }

    if (record->function != NULL) {
        funcr = record->function(record, comp, prop);
    }

    if (funcr != 0) {
        vcardproperty *errProp;
        vcardparameter *errParam;

        errParam =
            vcardparameter_new_xlicerrortype(VCARD_XLICERRORTYPE_RESTRICTIONCHECK);
        errProp = vcardproperty_vanew_xlicerror(funcr, errParam, (void *) 0);
        vcardcomponent_add_property(comp, errProp);
        vcardproperty_free(errProp);

        compare = 0;
    }

    return compare;
}

static bool vcardrestriction_check_component(vcardcomponent *comp)
{
    vcardcomponent_kind comp_kind, inner_kind;
    vcardproperty_version version;
    vcardproperty_kind prop_kind;
    const vcardrestriction_record *start_record;
    vcardproperty *version_prop = NULL;
    vcardcomponent *inner_comp;
    int count;
    int compare;
    bool valid = true;

    comp_kind = vcardcomponent_isa(comp);

    switch (comp_kind) {
    case VCARD_VCARD_COMPONENT:
        /* Get the VERSION property from the component */
        version_prop =
            vcardcomponent_get_first_property(comp, VCARD_VERSION_PROPERTY);
        break;

    default:
        break;
    }

    if (version_prop == 0) {
        version = VCARD_VERSION_NONE;
    } else {
        version = vcardproperty_get_version(version_prop);
    }

    /* Check all of the properties in this component */

    start_record = vcardrestriction_get_restriction(NULL, VCARD_VERSION_NONE,
                                                    comp_kind,
                                                    VCARD_ANY_PROPERTY,
                                                    VCARD_NO_COMPONENT);
    if (start_record != &null_restriction_record) {

        for (prop_kind = VCARD_ANY_PROPERTY + 1;
             prop_kind != VCARD_NO_PROPERTY; prop_kind++) {

            const vcardrestriction_record *record =
                vcardrestriction_get_restriction(start_record, version, comp_kind,
                                                 prop_kind, VCARD_NO_COMPONENT);

            vcardproperty *prop =
                vcardcomponent_get_first_property(comp, prop_kind);

            count = vcardcomponent_count_properties(comp, prop_kind, 1);

            compare = _check_restriction(comp, record, count, prop);

            valid = valid && compare;
        }
    }

    /* Now check the inner components */

    start_record = vcardrestriction_get_restriction(NULL, VCARD_VERSION_NONE,
                                                    comp_kind,
                                                    VCARD_NO_PROPERTY,
                                                    VCARD_ANY_COMPONENT);

    if (start_record != &null_restriction_record) {

        for (inner_kind = VCARD_NO_COMPONENT + 3;
             inner_kind != VCARD_NUM_COMPONENT_TYPES; inner_kind++) {

            const vcardrestriction_record *record =
                vcardrestriction_get_restriction(start_record, version, comp_kind,
                                                VCARD_NO_PROPERTY, inner_kind);

            count = vcardcomponent_count_components(comp, inner_kind);

            compare = _check_restriction(comp, record, count, NULL);

            valid = valid && compare;
        }
    }

    for (inner_comp = vcardcomponent_get_first_component(comp, VCARD_ANY_COMPONENT);
         inner_comp != 0;
         inner_comp = vcardcomponent_get_next_component(comp, VCARD_ANY_COMPONENT)) {

        compare = vcardrestriction_check_component(inner_comp);

        valid = valid && compare;
    }

    return valid;
}

bool vcardrestriction_check(vcardcomponent *outer_comp)
{
    vcardcomponent_kind comp_kind;
    bool valid;

    icalerror_check_arg_rz((outer_comp != 0), "outer comp");

    comp_kind = vcardcomponent_isa(outer_comp);

    if (comp_kind != VCARD_VCARD_COMPONENT &&
        comp_kind != VCARD_XROOT_COMPONENT) {
        icalerror_set_errno(ICAL_BADARG_ERROR);
        return 0;
    }

    valid = vcardrestriction_check_component(outer_comp);

    return valid;
}

static const vcardrestriction_record vcardrestriction_records[] = {
    {VCARD_VERSION_21, VCARD_VCARD_COMPONENT, VCARD_FN_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROPLUS, NULL},
    {VCARD_VERSION_21, VCARD_VCARD_COMPONENT, VCARD_N_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ONE, NULL},
    {VCARD_VERSION_30, VCARD_VCARD_COMPONENT, VCARD_FN_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ONEPLUS, NULL},
    {VCARD_VERSION_30, VCARD_VCARD_COMPONENT, VCARD_N_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ONE, NULL},
    {VCARD_VERSION_40, VCARD_VCARD_COMPONENT, VCARD_FN_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ONEPLUS, NULL},
    {VCARD_VERSION_40, VCARD_VCARD_COMPONENT, VCARD_N_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_ANNIVERSARY_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, vcardrestriction_validate_datetime_value},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_BDAY_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, vcardrestriction_validate_datetime_value},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_BIRTHPLACE_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_CREATED_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, vcardrestriction_validate_timestamp_value},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_DEATHDATE_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, vcardrestriction_validate_datetime_value},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_DEATHPLACE_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_GENDER_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_KIND_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_LANGUAGE_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_PRODID_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_REV_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, vcardrestriction_validate_timestamp_value},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_UID_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ZEROORONE, NULL},
    {VCARD_VERSION_NONE, VCARD_VCARD_COMPONENT, VCARD_VERSION_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_ONE, NULL},
    {VCARD_VERSION_NONE, VCARD_NO_COMPONENT, VCARD_NO_PROPERTY, VCARD_NO_COMPONENT, VCARD_RESTRICTION_NONE, NULL}
};

static const vcardrestriction_record *vcardrestriction_get_restriction(
    const vcardrestriction_record *start,
    vcardproperty_version version, vcardcomponent_kind component,
    vcardproperty_kind property, vcardcomponent_kind subcomp)
{
    const vcardrestriction_record *rec;

    if (!start) {
        start = &vcardrestriction_records[0];
    }

    for (rec = start; rec && rec->restriction != VCARD_RESTRICTION_NONE; rec++) {

        if ((version == VCARD_VERSION_NONE ||
             rec->version == VCARD_VERSION_NONE || version == rec->version) &&
            (component == VCARD_ANY_COMPONENT ||
             (component == rec->component &&
              (property == VCARD_ANY_PROPERTY || property == rec->property) &&
              (subcomp == VCARD_ANY_COMPONENT || subcomp == rec->subcomponent)))) {
            return rec;
        }
    }

    return &null_restriction_record;
}
