/* Chrysalide - Outil d'analyse de fichiers binaires
 * switch.h - gestion des énumérations Kaitai
 *
 * Copyright (C) 2019 Cyrille Bagard
 *
 *  This file is part of Chrysalide.
 *
 *  Chrysalide is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Chrysalide 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Chrysalide.  If not, see <http://www.gnu.org/licenses/>.
 */


#include "switch.h"


#include <assert.h>
#include <errno.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>


#include <i18n.h>


#include <common/extstr.h>
#include <common/sort.h>
#include <core/logs.h>
#include <plugins/yaml/pair.h>


#include "switch-int.h"
#include "../expression.h"



/* ------------------------ BASCULE DYNAMIQUE SELON CONTEXTE ------------------------ */


/* Construit une valeur d'énumération à partir d'indications. */
static switch_case_t *build_switch_case(const GYamlNode *, bool *);

/* Supprime de la mémoire une bascule selon contexte. */
static void delete_switch_case(switch_case_t *);

/* Détermine si le cas correspond à une valeur de bascule. */
static const char *is_suitable_switch_case_for_bytes(const switch_case_t *, const resolved_value_t *);

/* Détermine si le cas correspond à une valeur de bascule. */
static const char *is_suitable_switch_case_for_integer(const switch_case_t *, kaitai_scope_t *, const resolved_value_t *);



/* ----------------------- SELECTION DYNAMIQUE DE TYPE KAITAI ----------------------- */


/* Initialise la classe des sélections dynamiques de types. */
static void g_kaitai_switch_class_init(GKaitaiSwitchClass *);

/* Initialise une sélection dynamique de type Kaitai. */
static void g_kaitai_switch_init(GKaitaiSwitch *);

/* Supprime toutes les références externes. */
static void g_kaitai_switch_dispose(GKaitaiSwitch *);

/* Procède à la libération totale de la mémoire. */
static void g_kaitai_switch_finalize(GKaitaiSwitch *);



/* --------------------- IMPLEMENTATION DES FONCTIONS DE CLASSE --------------------- */


/* Parcourt un contenu binaire selon des spécifications Kaitai. */
static bool g_kaitai_switch_parse_content(GKaitaiSwitch *, kaitai_scope_t *, GBinContent *, ext_vmpa_t *, GMatchRecord **);



/* ---------------------------------------------------------------------------------- */
/*                          BASCULE DYNAMIQUE SELON CONTEXTE                          */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : node    = noeud Yaml à venir lire.                           *
*                defcase = indique si une valeur par défaut est visée. [OUT]  *
*                                                                             *
*  Description : Construit une valeur d'énumération à partir d'indications.   *
*                                                                             *
*  Retour      : Structure de valeur mise en place.                           *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static switch_case_t *build_switch_case(const GYamlNode *node, bool *defcase)
{
    switch_case_t *result;                  /* Enregistrement à retourner  */
    const char *key;                        /* Clef d'une conversion       */
    const char *value;                      /* Valeur Yaml particulière    */

    result = NULL;

    if (!G_IS_YAML_PAIR(node))
        goto exit;

    key = g_yaml_pair_get_key(G_YAML_PAIR(node));
    value = g_yaml_pair_get_value(G_YAML_PAIR(node));

    if (value == NULL)
        goto exit;

    result = malloc(sizeof(switch_case_t));

    result->value = strdup(key);
    result->type = strdup(value);

    *defcase = (strcmp(key, "_") == 0);

 exit:

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : swcase = valeur à traiter.                                   *
*                                                                             *
*  Description : Supprime de la mémoire une bascule selon contexte.           *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
*****************************************************************************/

static void delete_switch_case(switch_case_t *swcase)
{
    free(swcase->value);

    free(swcase->type);

    free(swcase);

}


/******************************************************************************
*                                                                             *
*  Paramètres  : swcase = valeur à analyser.                                  *
*                value  = valeur à comparer.                                  *
*                                                                             *
*  Description : Détermine si le cas correspond à une valeur de bascule.      *
*                                                                             *
*  Retour      : Type à utiliser ou NULL si aucune correspondance établie.    *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
*****************************************************************************/

static const char *is_suitable_switch_case_for_bytes(const switch_case_t *swcase, const resolved_value_t *value)
{
    const char *result;                     /* Désignation à retourner     */
    sized_string_t key;                     /* Changement de format        */
    bool valid;                             /* Validité des opérations     */
    int ret;                                /* Bilan d'une comparaison     */

    result = NULL;

    key.data = swcase->value;
    key.len = strlen(swcase->value);

    valid = (key.len > 2);

    if (valid)
        valid = (swcase->value[0] == '"' || swcase->value[0] == '\'');

    if (valid)
    {
        valid = (key.data[0] == key.data[key.len - 1]);

        key.data++;
        key.len -= 2;

    }

    if (valid)
    {
        if (value->type == GVT_BYTES)
        {
            ret = szmemcmp(&key, &value->bytes);

            if (ret == 0)
                result = swcase->type;

        }

    }

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : swcase = valeur à analyser.                                  *
*                locals = variables locales pour les résolutions de types.    *
*                value  = valeur à comparer.                                  *
*                                                                             *
*  Description : Détermine si le cas correspond à une valeur de bascule.      *
*                                                                             *
*  Retour      : Type à utiliser ou NULL si aucune correspondance établie.    *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
*****************************************************************************/

static const char *is_suitable_switch_case_for_integer(const switch_case_t *swcase, kaitai_scope_t *locals, const resolved_value_t *value)
{
    const char *result;                     /* Désignation à retourner     */
    bool valid;                             /* Validité des opérations     */
    resolved_value_t key;                   /* Changement de format        */
    unsigned long long unsigned_conv;       /* Valeur convertie #1         */
    long long signed_conv;                  /* Valeur convertie #2         */

    result = NULL;

    valid = (swcase->value[0] != '"' && swcase->value[0] != '\'');

    if (valid)
    {
        if (strchr(swcase->value, ':') != NULL)
        {
            valid = resolve_kaitai_expression_as_integer(locals, swcase->value, strlen(swcase->value), &key);

            if (valid)
            {
                if (key.type == GVT_UNSIGNED_INTEGER)
                {
                    if (value->type == GVT_UNSIGNED_INTEGER)
                    {
                        if (key.unsigned_integer == value->unsigned_integer)
                            result = swcase->type;
                    }
                    else
                    {
                        if (key.unsigned_integer == value->signed_integer)
                            result = swcase->type;
                    }
                }
                else
                {
                    if (value->type == GVT_UNSIGNED_INTEGER)
                    {
                        if (key.signed_integer == value->unsigned_integer)
                            result = swcase->type;
                    }
                    else
                    {
                        if (key.signed_integer == value->signed_integer)
                            result = swcase->type;
                    }
                }

            }

        }

        else
        {
            if (value->type == GVT_UNSIGNED_INTEGER)
            {
                unsigned_conv = strtoull(swcase->value, NULL, 0);

                valid = (errno != ERANGE && errno != EINVAL);

                if (valid && unsigned_conv == value->unsigned_integer)
                    result = swcase->type;

            }
            else
            {
                signed_conv = strtoll(swcase->value, NULL, 0);

                valid = (errno != ERANGE && errno != EINVAL);

                if (valid && signed_conv == value->signed_integer)
                    result = swcase->type;

            }

        }

    }

    return result;

}



/* ---------------------------------------------------------------------------------- */
/*                         SELECTION DYNAMIQUE DE TYPE KAITAI                         */
/* ---------------------------------------------------------------------------------- */


/* Indique le type défini pour un choix dynamique de type Kaitai. */
G_DEFINE_TYPE(GKaitaiSwitch, g_kaitai_switch, G_TYPE_KAITAI_PARSER);


/******************************************************************************
*                                                                             *
*  Paramètres  : klass = classe à initialiser.                                *
*                                                                             *
*  Description : Initialise la classe des sélections dynamiques de types.     *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void g_kaitai_switch_class_init(GKaitaiSwitchClass *klass)
{
    GObjectClass *object;                   /* Autre version de la classe  */
    GKaitaiParserClass *parser;             /* Version parente de la classe*/ 

    object = G_OBJECT_CLASS(klass);

    object->dispose = (GObjectFinalizeFunc/* ! */)g_kaitai_switch_dispose;
    object->finalize = (GObjectFinalizeFunc)g_kaitai_switch_finalize;

    parser = G_KAITAI_PARSER_CLASS(klass);

    parser->parse = (parse_kaitai_fc)g_kaitai_switch_parse_content;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : kswitch = instance à initialiser.                            *
*                                                                             *
*  Description : Initialise une sélection dynamique de type Kaitai.           *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void g_kaitai_switch_init(GKaitaiSwitch *kswitch)
{
    kswitch->target = NULL;

    kswitch->cases = NULL;
    kswitch->count = 0;

    kswitch->defcase = NULL;

    kswitch->generic = NULL;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : kswitch = instance d'objet GLib à traiter.                   *
*                                                                             *
*  Description : Supprime toutes les références externes.                     *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void g_kaitai_switch_dispose(GKaitaiSwitch *kswitch)
{
    g_clear_object(&kswitch->generic);

    G_OBJECT_CLASS(g_kaitai_switch_parent_class)->dispose(G_OBJECT(kswitch));

}


/******************************************************************************
*                                                                             *
*  Paramètres  : kswitch = instance d'objet GLib à traiter.                   *
*                                                                             *
*  Description : Procède à la libération totale de la mémoire.                *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void g_kaitai_switch_finalize(GKaitaiSwitch *kswitch)
{
    size_t i;                               /* Boucle de parcours          */

    if (kswitch->target != NULL)
        free(kswitch->target);

    for (i = 0; i < kswitch->count; i++)
        delete_switch_case(kswitch->cases[i]);

    if (kswitch->cases != NULL)
        free(kswitch->cases);

    if (kswitch->defcase != NULL)
        delete_switch_case(kswitch->defcase);

    G_OBJECT_CLASS(g_kaitai_switch_parent_class)->finalize(G_OBJECT(kswitch));

}


/******************************************************************************
*                                                                             *
*  Paramètres  : parent  = noeud Yaml contenant l'attribut à constituer.      *
*                generic = lecteur d'attribut Kaitai à dériver.               *
*                                                                             *
*  Description : Construit une sélection dynamique de type Kaitai.            *
*                                                                             *
*  Retour      : Instance mise en place ou NULL en cas d'échec.               *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

GKaitaiSwitch *g_kaitai_switch_new(GYamlNode *parent, GKaitaiAttribute *generic)
{
    GKaitaiSwitch *result;                   /* Identifiant à retourner     */

    result = g_object_new(G_TYPE_KAITAI_SWITCH, NULL);

    if (!g_kaitai_switch_create(result, parent, generic))
        g_clear_object(&result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : kswitch = sélectionneur de type  à initialiser pleinement.   *
*                parent  = noeud Yaml contenant l'attribut à constituer.      *
*                generic = lecteur d'attribut Kaitai à dériver.               *
*                                                                             *
*  Description : Met en place une sélection dynamique de type Kaitai.         *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

bool g_kaitai_switch_create(GKaitaiSwitch *kswitch, GYamlNode *parent, GKaitaiAttribute *generic)
{
    bool result;                            /* Bilan à retourner           */
    GYamlNode *node;                        /* Noeud de définition         */
    GYamlNode *subnode;                     /* Noeud de précisions         */
    const char *value;                      /* Valeur Yaml particulière    */
    GYamlNode *collec;                      /* Liste de noeuds à traiter   */
    GYamlNode **subnodes;                   /* Eventuels noeuds trouvés    */
    size_t count;                           /* Quantité de ces noeuds      */
    size_t i;                               /* Boucle de parcours          */
    bool defcase;                           /* Définition par défaut ?     */
    switch_case_t *swcase;                  /* Bascule à noter             */

    result = false;

    node = g_yaml_node_find_first_by_path(parent, "/type/");
    if (node == NULL) goto exit;

    /* Source de la bascule */

    subnode = g_yaml_node_find_first_by_path(node, "/switch-on");
    assert(G_IS_YAML_PAIR(subnode));

    value = g_yaml_pair_get_value(G_YAML_PAIR(subnode));
    if (value == NULL)
    {
        g_object_unref(G_OBJECT(subnode));
        goto bad_definition;
    }

    kswitch->target = strdup(value);

    g_object_unref(G_OBJECT(subnode));

    /* Conditions de bascule */

    collec = g_yaml_node_find_first_by_path(node, "/cases/");
    if (collec == NULL) goto bad_definition;
    if (!G_IS_YAML_COLLEC(collec)) goto bad_definition;

    subnodes = g_yaml_collection_get_nodes(G_YAML_COLLEC(collec), &count);

    g_object_unref(G_OBJECT(collec));

    if (count == 0) goto bad_definition;

    for (i = 0; i < count; i++)
    {
        swcase = build_switch_case(subnodes[i], &defcase);
        if (swcase == NULL) break;

        g_object_unref(G_OBJECT(subnodes[i]));

        kswitch->cases = realloc(kswitch->cases, ++kswitch->count * sizeof(switch_case_t *));
        kswitch->cases[kswitch->count - 1] = swcase;

    }

    result = (i == count);

    for (; i < count; i++)
        g_object_unref(G_OBJECT(subnodes[i]));

    if (subnodes != NULL)
        free(subnodes);

    /* Fin des procédures */

    if (result)
    {
        kswitch->generic = generic;
        g_object_ref(G_OBJECT(generic));
    }

 bad_definition:

    g_object_unref(G_OBJECT(node));

 exit:

    return result;

}



/* ---------------------------------------------------------------------------------- */
/*                       IMPLEMENTATION DES FONCTIONS DE CLASSE                       */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : kswitch = structure Kaitai en cours de parcours.             *
*                locals  = variables locales pour les résolutions de types.   *
*                content = données binaires à analyser et traduire.           *
*                epos    = tête de lecture courante. [OUT]                    *
*                record  = noeud d'arborescence d'éléments rencontrés. [OUT]  *
*                                                                             *
*  Description : Parcourt un contenu binaire selon des spécifications Kaitai. *
*                                                                             *
*  Retour      : Bilan de l'opératon : true pour continuer, false sinon.      *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool g_kaitai_switch_parse_content(GKaitaiSwitch *kswitch, kaitai_scope_t *locals, GBinContent *content, ext_vmpa_t *epos, GMatchRecord **record)
{
    bool result;                            /* Bilan à retourner           */
    const char *final_type;                 /* Type à utiliser au final    */
    resolved_value_t value;                 /* Valeur de cible entière     */
    bool status;                            /* Bilan intermédiaire         */
    size_t i;                               /* Boucle de parcours          */
    GKaitaiAttribute *attrib;               /* Lecteur approprié           */

    result = true;

    final_type = NULL;

    /* Tenative n°1 : version "entier" */

    status = resolve_kaitai_expression_as_integer(locals, kswitch->target, strlen(kswitch->target), &value);

    if (status)
        for (i = 0; i < kswitch->count; i++)
        {
            final_type = is_suitable_switch_case_for_integer(kswitch->cases[i], locals, &value);

            if (final_type != NULL)
                goto next_step;

        }

    status = resolve_kaitai_expression_as_bytes(locals, kswitch->target, strlen(kswitch->target), &value);

    /* Tenative n°1 : version "chaîne" */

    if (status)
        for (i = 0; i < kswitch->count; i++)
        {
            final_type = is_suitable_switch_case_for_bytes(kswitch->cases[i], &value);

            if (final_type != NULL)
                goto next_step;

        }

    if (final_type == NULL && kswitch->defcase != NULL)
        final_type = kswitch->defcase->type;

 next_step:

    /* Mise en place d'un attribut et analyse */

    if (final_type != NULL)
    {
        attrib = g_kaitai_attribute_dup_for_user_type(kswitch->generic, final_type);

        result = g_kaitai_parser_parse_content(G_KAITAI_PARSER(attrib), locals, content, epos, record);

        g_object_unref(G_OBJECT(attrib));

    }

    return true;
    return result;

}