#include <stdio.h>
#include <stdlib.h>
#include "asn1fix_internal.h"
#include "asn1fix_cws.h"

static int _asn1f_parse_class_object_data(arg_t *, asn1p_expr_t *eclass,
		struct asn1p_ioc_row_s *row, asn1p_wsyntx_t *syntax,
		const uint8_t *buf, const uint8_t *bend,
		int optional_mode, const uint8_t **newpos, int counter);
static int _asn1f_assign_cell_value(arg_t *arg, struct asn1p_ioc_cell_s *cell, const uint8_t *buf, const uint8_t *bend, int counter);
static asn1p_wsyntx_chunk_t *asn1f_next_literal_chunk(asn1p_wsyntx_t *syntax, asn1p_wsyntx_chunk_t *chunk, const uint8_t *buf);

int
asn1f_check_class_object(arg_t *arg) {
	asn1p_expr_t *expr = arg->expr;
	asn1p_expr_t *eclass;
	asn1p_ioc_row_t *row;
	int ret;

	if(expr->meta_type != AMT_VALUESET
	|| expr->expr_type != A1TC_REFERENCE
	|| !expr->value
	|| expr->value->type != ATV_UNPARSED) {
		return 0;
    }

	eclass = asn1f_find_terminal_type(arg, expr);
	if(!eclass
	|| eclass->meta_type != AMT_OBJECTCLASS
	|| eclass->expr_type != A1TC_CLASSDEF) {
		return 0;
	}

	if(!eclass->with_syntax) {
		DEBUG("Can't process classes without %s just yet",
			"WITH SYNTAX");
		return 0;
	}

	row = asn1p_ioc_row_new(eclass);
	assert(row);

	ret = _asn1f_parse_class_object_data(arg, eclass, row,
		eclass->with_syntax,
		expr->value->value.string.buf + 1,
		expr->value->value.string.buf
			+ expr->value->value.string.size - 1,
		0, 0, 0);

	asn1p_ioc_row_delete(row);

	return ret;
}

static int
_asn1f_is_ioc_row_duplicate(asn1p_ioc_table_t *it, asn1p_ioc_row_t *row) {
    if(!it) {
        return 0;
    }

    for(size_t i = 0; i < it->rows; i++) {
        switch(asn1p_ioc_row_match(it->row[i], row)) {
        default:
        case -1:
            return -1;
        case 1:
            continue;
        case 0:
            return 1; /* Duplicate! */
        }
    }
    return 0;
}

struct parse_object_key {
    arg_t *arg;
    asn1p_expr_t *expr;   /* InformationObjectSet */
    asn1p_expr_t *eclass; /* CLASS */
    int sequence;          /* Sequence counter */
};

/*
 * Add to the IoC table if the row is unique.
 */
static int
_asn1f_add_unique_row(arg_t *arg, asn1p_expr_t *expr, asn1p_ioc_row_t *row) {
    assert(expr->ioc_table);

    /* Look for duplicates */
    switch(_asn1f_is_ioc_row_duplicate(expr->ioc_table, row)) {
    case -1:
        DEBUG("Found Information Object Duplicate in %s", expr->Identifier,
              expr->_lineno);
        return -1;
    case 0:
        /* Not a duplicate */
        break;
    case 1:
        /* Proper duplicate detected; ignore */
        asn1p_ioc_row_delete(row);
        return 0;
        }

    asn1p_ioc_table_add(expr->ioc_table, row);

    return 0;
}

/*
 * Given a single blob of unparsed Information Object specification, parse it
 * into a given InformationObjectSet.
 */
static int
_asn1f_parse_object_cb(const uint8_t *buf, size_t size, void *keyp) {
    struct parse_object_key *key = keyp;
    arg_t *arg = key->arg;
    asn1p_expr_t *expr = key->expr;
    asn1p_expr_t *eclass = key->eclass;
	asn1p_ioc_row_t *row;
    int ret;

    key->sequence++;

	row = asn1p_ioc_row_new(eclass);
	assert(row);

    ret = _asn1f_parse_class_object_data(arg, eclass, row, eclass->with_syntax,
                                         buf, buf + size, 0, 0, key->sequence);
    if(ret) {
        LOG(ret, "Cannot parse %s of CLASS %s found at line %d",
            expr->Identifier, eclass->Identifier, expr->_lineno);
        asn1p_ioc_row_delete(row);
		return ret;
	}

    /* Add object to a CLASS. */
    if(_asn1f_add_unique_row(arg, eclass, row) != 0)
        return -1;

    /*
     * Add a copy of the object to the Information Object Set.
     */
	row = asn1p_ioc_row_new(eclass);
	assert(row);
    ret = _asn1f_parse_class_object_data(arg, eclass, row, eclass->with_syntax,
                                         buf, buf + size, 0, 0, key->sequence);
    assert(ret == 0);

    if(_asn1f_add_unique_row(arg, expr, row) != 0)
        return -1;

    return 0;
}

static int
_asn1f_foreach_unparsed_union(const asn1p_constraint_t *ct_union,
                              int (*process)(const uint8_t *buf, size_t size,
                                             void *key),
                              void *key) {
    assert(ct_union->type == ACT_CA_UNI);

    for(size_t j = 0; j < ct_union->el_count; j++) {
        const asn1p_constraint_t *ct2 = ct_union->elements[j];
        if(ct2->type == ACT_EL_VALUE && ct2->value->type == ATV_UNPARSED) {
            if(process
               && process(ct2->value->value.string.buf + 1,
                          ct2->value->value.string.size - 2, key)
                      != 0) {
                return -1;
            }
            continue;
        }
        return -1;
    }

    return 0;
}

static int
_asn1f_foreach_unparsed(arg_t *arg, const asn1p_constraint_t *ct,
                        int (*process)(const uint8_t *buf, size_t size,
                                       void *key),
                        void *keyp) {
    struct parse_object_key *key = keyp;
    if(!ct) return -1;

    switch(ct->type) {
    default:
        DEBUG("Constraint is of unknown type %d for CWS", ct->type);
        return -1;
    case ACT_EL_EXT:    /* ... */
        if(key) {
            key->expr->ioc_table->extensible = 1;
        }
        return 0;
    case ACT_CA_UNI:    /* | */
        return _asn1f_foreach_unparsed_union(ct, process, keyp);
    case ACT_CA_CSV:    /* , */
        break;
    case ACT_EL_VALUE:
        if(ct->value->type == ATV_UNPARSED) {
            if(process
               && process(ct->value->value.string.buf + 1,
                          ct->value->value.string.size - 2, key)
                      != 0) {
                return -1;
            }
        }
        return 0;
    }

    for(size_t i = 0; i < ct->el_count; i++) {
        const asn1p_constraint_t *ct1 = ct->elements[i];
        if(_asn1f_foreach_unparsed(arg, ct1, process, keyp) != 0) {
            return -1;
        }
    }

    return 0;
}

static int
_asn1f_constraint_looks_like_object_set(arg_t *arg,
                                        const asn1p_constraint_t *ct) {
    return 0 == _asn1f_foreach_unparsed(arg, ct, NULL, NULL);
}

int
asn1f_parse_class_object(arg_t *arg) {
	asn1p_expr_t *expr = arg->expr;
	asn1p_expr_t *eclass;
    enum {
        FROM_VALUE,
        FROM_CONSTRAINT,
    } source = FROM_VALUE;

	if(expr->meta_type == AMT_VALUE
	&& expr->expr_type == A1TC_REFERENCE
	&& expr->value && expr->value->type == ATV_UNPARSED) {
        source = FROM_VALUE;
    } else if(expr->meta_type != AMT_VALUESET
	|| expr->expr_type != A1TC_REFERENCE) {
        return 0;
    } else if(expr->value && expr->value->type == ATV_UNPARSED) {
        source = FROM_VALUE;
    } else if(!expr->value) {
        if(_asn1f_constraint_looks_like_object_set(arg, expr->constraints)) {
            source = FROM_CONSTRAINT;
        } else {
            return 0;
        }
    } else {
        return 0;
    }

	/*
	 * Find the governing class.
	 */
	eclass = asn1f_find_terminal_type(arg, expr);
	if(!eclass
	|| eclass->meta_type != AMT_OBJECTCLASS
	|| eclass->expr_type != A1TC_CLASSDEF) {
		return 0;
	}

	DEBUG("Value %s of CLASS %s found at line %d",
		expr->Identifier, eclass->Identifier, expr->_lineno);

	if(!eclass->with_syntax) {
		DEBUG("Can't process classes without %s just yet",
			"WITH SYNTAX");
		return 0;
	}

    struct parse_object_key key = {
        .arg = arg,
        .expr = expr,
        .eclass = eclass,
        .sequence = eclass->_type_unique_index
    };

    if(!expr->ioc_table) {
        expr->ioc_table = asn1p_ioc_table_new();
    }

    if(!eclass->ioc_table) {
        eclass->ioc_table = asn1p_ioc_table_new();
    }

    switch(source) {
    case FROM_VALUE:
        if(_asn1f_parse_object_cb(expr->value->value.string.buf + 1,
                           expr->value->value.string.size - 2, &key)
           != 0) {
            return -1;
        }
        break;
    case FROM_CONSTRAINT:
        if(_asn1f_foreach_unparsed(arg, expr->constraints,
                                   _asn1f_parse_object_cb, &key)
           != 0) {
            return -1;
        }
    }

    eclass->_type_unique_index = key.sequence;

	return 0;
}

#define	SKIPSPACES	for(; buf < bend && isspace(*buf); buf++)

static int
_asn1f_parse_class_object_data(arg_t *arg, asn1p_expr_t *eclass,
		struct asn1p_ioc_row_s *row, asn1p_wsyntx_t *syntax,
		const uint8_t *buf, const uint8_t *bend,
		int optional_mode, const uint8_t **newpos, int counter) {
	struct asn1p_wsyntx_chunk_s *chunk;
	int ret;

	TQ_FOR(chunk, (&syntax->chunks), next) {
		switch(chunk->type) {
		case WC_LITERAL: {
			int token_len = strlen(chunk->content.token);
			SKIPSPACES;
			if(bend - buf < token_len
			|| memcmp(buf, chunk->content.token, token_len)) {
				if(!optional_mode) {
					FATAL("While parsing object class value %s at line %d: Expected: \"%s\", found: \"%s\"",
					arg->expr->Identifier, arg->expr->_lineno, chunk->content.token, buf);
				}
				if(newpos) *newpos = buf;
				return -1;
			}
			buf += token_len;
		    } break;
		case WC_WHITESPACE: break;	/* Ignore whitespace */
		case WC_FIELD: {
			struct asn1p_ioc_cell_s *cell;
			asn1p_wsyntx_chunk_t *next_literal;
			const uint8_t *buf_old = buf;
			const uint8_t *p = 0;

			SKIPSPACES;

			next_literal = asn1f_next_literal_chunk(syntax, chunk, buf);
			if(!next_literal) {
				p += (bend - p);
			} else {
				p = (uint8_t *)strstr((const char *)buf, (const char *)next_literal->content.token);
				if(!p) {
					if (!optional_mode)
						FATAL("Next literal \"%s\" not found !", next_literal->content.token);

					if(newpos) *newpos = buf_old;
					return -1;
				}
			}
			cell = asn1p_ioc_row_cell_fetch(row,
					chunk->content.token);
			if(cell == NULL) {
				FATAL("Field reference %s found in WITH SYNAX {} clause does not match actual field in Object Class %s",
					chunk->content.token,
					eclass->Identifier, eclass->_lineno);
				if(newpos) *newpos = buf;
				return -1;
			}
			DEBUG("Reference %s satisfied by %s (%d)",
				chunk->content.token,
				buf, p - buf);
			ret = _asn1f_assign_cell_value(arg, cell, buf, p, counter);
			if(ret) return ret;
			buf = p;
			if(newpos) *newpos = buf;
		    } break;
		case WC_OPTIONALGROUP: {
			const uint8_t *np = 0;
			SKIPSPACES;
			ret = _asn1f_parse_class_object_data(arg, eclass, row,
				chunk->content.syntax, buf, bend, 1, &np, counter);
			if(newpos) *newpos = np;
			if(ret && np != buf)
				return ret;
			buf = np;
		    } break;
		}
	}


	if(newpos) *newpos = buf;
	return 0;
}


static int
_asn1f_assign_cell_value(arg_t *arg, struct asn1p_ioc_cell_s *cell,
                         const uint8_t *buf, const uint8_t *bend, int counter) {
    asn1p_expr_t *expr = (asn1p_expr_t *)NULL;
	char *mivr; /* Most Immediate Value Representation */
	int new_ref = 1;
	asn1p_t *asn;
	asn1p_expr_t *type_expr = (asn1p_expr_t *)NULL;
	int i, ret = 0, psize;
	char *pp;

	if((bend - buf) <= 0) {
		FATAL("Assignment warning: empty string is being assigned into %s for %s at line %d",
			cell->field->Identifier,
			arg->expr->Identifier, arg->expr->_lineno);
		return -1;
	}

	mivr = malloc(bend - buf + 1);
	assert(mivr);
	memcpy(mivr, buf, bend - buf);
	mivr[bend - buf] = '\0';
	/* remove trailing space */
	for (i = bend - buf - 1; (i > 0) && isspace(mivr[i]); i--)
		mivr[i] = '\0';

	/* This value 100 should be larger than following formatting string */
	psize = bend - buf + 100;
	pp = calloc(1, psize);
	if(pp == NULL) {
		free(mivr);
		return -1;
	}

	if(cell->field->expr_type == A1TC_CLASSFIELD_TFS) {
		ret = snprintf(pp, psize,
			"M DEFINITIONS ::=\nBEGIN\n"
			"V ::= %s\n"
			"END\n",
			mivr
		);
	} else if(cell->field->expr_type == A1TC_CLASSFIELD_FTVFS) {
		type_expr = TQ_FIRST(&(cell->field->members));
		ret = snprintf(pp, psize,
				"M DEFINITIONS ::=\nBEGIN\n"
				"v %s ::= %s\n"
				"END\n",
				type_expr->reference ? 
					type_expr->reference->components[0].name : 
					_asn1p_expr_type2string(type_expr->expr_type),
				mivr
			);
	} else {
		WARNING("asn1c only be able to parse TypeFieldSpec and FixedTypeValueFieldSpec. Failed when parsing %s at line %d\n", mivr, arg->expr->_lineno);
		free(mivr);
		free(pp);
		return -1;
	}
	DEBUG("ASN.1:\n\n%s\n", pp);

	assert(ret < psize);
	psize = ret;

	asn = asn1p_parse_buffer(pp, psize,
		arg->expr->module->source_file_name, arg->expr->_lineno, A1P_NOFLAGS);
	free(pp);
	if(asn == NULL) {
		FATAL("Cannot parse Setting token %s "
			"at line %d",
			mivr,
			arg->expr->_lineno
		);
		free(mivr);
		return -1;
	} else {
        asn1p_module_t *mod = TQ_FIRST(&(asn->modules));
        assert(mod);

        /* This member removal is safe with respect to members hash since the
         * entire asn module will be deleted down below.
         */
        expr = TQ_REMOVE(&(mod->members), next);
		assert(expr);

        expr->parent_expr = NULL;
        asn1p_expr_set_source(expr, arg->expr->module, arg->expr->_lineno);
        expr->_type_unique_index = counter;
        DEBUG("Parsed identifier %s, mivr [%s], reference [%s] value [%s]",
              expr->Identifier, mivr, asn1p_ref_string(expr->reference),
              asn1f_printable_value(expr->value));
        free(expr->Identifier);
        if(expr->value) {
			expr->Identifier = strdup(asn1f_printable_value(expr->value));
        } else if (expr->reference) {
			expr->Identifier = strdup(expr->reference->components[expr->reference->comp_count - 1].name);
		} else {
			expr->Identifier = mivr;
		}
		asn1p_delete(asn);
	}

	if(expr->reference &&
		!asn1f_lookup_symbol(arg, expr->rhs_pspecs, expr->reference)) {

		asn1p_ref_free(expr->reference);
		new_ref = 0;
		expr->reference = type_expr->reference;
		if (asn1f_value_resolve(arg, expr, 0)) {
			expr->reference = 0;
			asn1p_expr_free(expr);
			FATAL("Cannot find %s referenced by %s at line %d",
				mivr, arg->expr->Identifier,
				arg->expr->_lineno);
			free(mivr);
			return -1;
		}
	}

	DEBUG("Field %s assignment of %s got %s",
		cell->field->Identifier, mivr, expr->Identifier);

	cell->value = expr;
	cell->new_ref = new_ref;

	if(expr->Identifier != mivr) {
		free(mivr);
    }

	return 0;
}

static asn1p_wsyntx_chunk_t *
asn1f_next_literal_chunk(asn1p_wsyntx_t *syntax, asn1p_wsyntx_chunk_t *chunk, const uint8_t *buf)
{
	asn1p_wsyntx_chunk_t *next_chunk;

	next_chunk = TQ_NEXT(chunk, next);
	do {
		if(next_chunk == (struct asn1p_wsyntx_chunk_s *)0) {
			if(!syntax->parent)
				break;
			next_chunk = TQ_NEXT(syntax->parent, next);
		} else if(next_chunk->type == WC_LITERAL) {
			if(strstr((const char *)buf, (char *)next_chunk->content.token))
				break;
			if(!syntax->parent)
				break;
			next_chunk = TQ_NEXT(syntax->parent, next);
		} else if(next_chunk->type == WC_WHITESPACE) {
			next_chunk = TQ_NEXT(next_chunk, next);
		} else if(next_chunk->type == WC_OPTIONALGROUP) {
			syntax = next_chunk->content.syntax;
			next_chunk = TQ_FIRST(((&next_chunk->content.syntax->chunks)));
		} else 
			break;
	} while (next_chunk);

	return next_chunk;
}
