#include "mcg.h"

static struct e_instr em;
static struct basicblock* code_bb;
static struct basicblock* data_bb;

static void queue_insn_label(int opcode, const char* label, arith offset);

static const char* type_to_str(int type)
{
	switch (type)
	{
		case EM_MNEM:       return "EM_MNEM";
		case EM_PSEU:	    return "EM_PSEU";
		case EM_STARTMES:   return "EM_STARTMES";
		case EM_MESARG:     return "EM_MESARG";
		case EM_ENDMES:     return "EM_ENDMES";
		case EM_DEFILB:     return "EM_DEFILB";
		case EM_DEFDLB:     return "EM_DEFDLB";
		case EM_DEFDNAM:    return "EM_DEFDNAM";
		case EM_ERROR:      return "EM_ERROR";
		case EM_FATAL:      return "EM_FATAL";
		case EM_EOF:        return "EM_EOF";
	}

    assert(0 && "invalid EM type");
}

static const char* argtype_to_str(int type)
{
    if (type == 0)        return "...";
    if (type == ilb_ptyp) return "ilb";
    if (type == nof_ptyp) return "nof";
    if (type == sof_ptyp) return "sof";
    if (type == cst_ptyp) return "cst";
    if (type == pro_ptyp) return "pro";
    if (type == str_ptyp) return "str";
    if (type == ico_ptyp) return "ico";
    if (type == uco_ptyp) return "uco";
    if (type == fco_ptyp) return "fco";
    return "???";
}

static void unknown_type(const char* s)
{
    fatal("%s with unknown type '%s'",
        s,
        argtype_to_str(em.em_arg.ema_argtype));
}

static const char* ilabel_to_str(label l)
{
    assert(current_proc != NULL);
    return aprintf(".%s_I%d", current_proc->name, l);
}

static const char* dlabel_to_str(label l)
{
    return aprintf(".D%d", l);
}

static void terminate_block(void)
{
    code_bb->is_terminated = true;
    code_bb = NULL;
}

static struct em* new_insn(int opcode)
{
    struct em* em = calloc(sizeof(struct em), 1);
    em->opcode = opcode;
    return em;
}

static void queue_insn_simple(int opcode)
{
    struct em* em = new_insn(opcode);
    em->paramtype = PARAM_NONE;
    array_append(&code_bb->ems, em);

    switch (opcode)
    {
        case op_bra:
            terminate_block();
            break;
    }
}

static void queue_insn_value(int opcode, arith value)
{
    struct em* em = new_insn(opcode);
    em->paramtype = PARAM_IVALUE;
    em->u.ivalue = value;
    array_append(&code_bb->ems, em);

    switch (opcode)
    {
        case op_csa:
        case op_csb:
        case op_ret:
            terminate_block();
            break;
    }
}

static void queue_insn_label(int opcode, const char* label, arith offset)
{
    struct em* em = new_insn(opcode);
    em->paramtype = PARAM_LVALUE;
    em->u.lvalue.label = label;
    em->u.lvalue.offset = offset;
    array_append(&code_bb->ems, em);

    switch (opcode)
    {
        case op_bra:
            terminate_block();
            break;
    }
}

static void queue_insn_block(int opcode, struct basicblock* left, struct basicblock* right)
{
    struct em* em = new_insn(opcode);
    em->paramtype = PARAM_BVALUE;
    em->u.bvalue.left = left;
    em->u.bvalue.right = right;
    array_append(&code_bb->ems, em);
    
    terminate_block();
}

static void change_basicblock(struct basicblock* newbb)
{
    array_appendu(&current_proc->blocks, newbb);

    if (code_bb && !code_bb->is_terminated)
        queue_insn_block(op_bra, newbb, NULL);

    code_bb = newbb;
}

static void queue_insn_ilabel(int opcode, int label)
{
    const char* name = ilabel_to_str(em.em_ilb);
    struct basicblock* left = bb_get(name);

    switch (opcode)
    {
        case op_bra:
            queue_insn_block(em.em_opcode, left, NULL);
            break;

        case op_zeq:
        case op_zne:
        case op_zlt:
        case op_zle:
        case op_zgt:
        case op_zge:
        case op_beq:
        case op_bne:
        case op_blt:
        case op_ble:
        case op_bgt:
        case op_bge:
        {
            struct basicblock* bb = bb_get(NULL);
            queue_insn_block(em.em_opcode, left, bb);
            change_basicblock(bb);
            break;
        }

        default:
            fatal("parse_em: unhandled conditional '%s'", 
                em_mnem[opcode - sp_fmnem]);
    }
}

static void queue_ilabel(arith label)
{
    change_basicblock(bb_get(ilabel_to_str(label)));
}

/* This is really hacky; to handle basic block flow
 * descriptor blocks, we need to be able to identify
 * them, read them and parse them. So we create
 * can exit to. So we create fake bb objects for each
 * block, purely to track this, and copy ints and labels
 * into them.
 */

static void data_block_int(arith value)
{
	if (data_bb)
	{
		struct em* em = new_insn(op_loc);
		em->paramtype = PARAM_IVALUE;
		em->u.ivalue = value;
		array_append(&data_bb->ems, em);
	}
}

static void data_block_label(const char* label)
{
	if (data_bb)
	{
		struct em* em = new_insn(op_bra);
		em->paramtype = PARAM_BVALUE;
		em->u.bvalue.left = bb_get(label);
		array_append(&data_bb->ems, em);
	}
}

static arith safe_atol(const char* s)
{
	arith result;

	errno = 0;
	result = strtoul(s, NULL, 0);
	if (errno == ERANGE)
		result = strtol(s, NULL, 0);
	if (errno == ERANGE)
		fatal("constant '%s' not parseable", s);
	return result;
}

static void parse_pseu(void)
{
	switch (em.em_opcode)
	{
		case ps_exp: /* external proc */
		case ps_exa: /* external array */
		case ps_inp: /* internal proc */
		case ps_ina: /* internal array */
		{
			bool export = (em.em_opcode == ps_exp) || (em.em_opcode == ps_exa);
			bool proc = (em.em_opcode == ps_exp) || (em.em_opcode == ps_inp);

			switch (em.em_arg.ema_argtype)
			{
				case pro_ptyp:
					symbol_declare(strdup(em.em_pnam), export, proc);
					break;

				case sof_ptyp:
                    assert(em.em_off == 0);
					symbol_declare(strdup(em.em_dnam), export, proc);
					break;

                case nof_ptyp:
                    assert(em.em_off == 0);
                    symbol_declare(dlabel_to_str(em.em_dlb), export, proc);
                    break;

				default:
                    unknown_type("exp, exa, inp, ina");
			}
			break;
		}

		case ps_con: /* .data */
		case ps_rom: /* .rom */
        {
            bool ro = (em.em_opcode == ps_rom);

			switch (em.em_arg.ema_argtype)
			{
				case ico_ptyp:
				case uco_ptyp:
                {
                    arith val = safe_atol(em.em_string);
                    data_int(val, em.em_size, ro);
                    data_block_int(val);
                    break;
                }

                case fco_ptyp:
                {
                    data_float(em.em_string, em.em_size, ro);
                    break;
                }

				case str_ptyp:
                    data_block((const uint8_t*) em.em_string, strlen(em.em_string), em.em_size, ro);
					break;

                case cst_ptyp:
                {
                    arith value = em.em_cst;
                    data_int(value, EM_wordsize, ro);
                    data_block_int(value);
                    break;
				}
                    
                case nof_ptyp:
                    data_offset(dlabel_to_str(em.em_dlb), em.em_off, ro);
                    break;

                case sof_ptyp:
                case pro_ptyp:
                    data_offset(strdup(em.em_dnam), em.em_off, ro);
                    break;

                case ilb_ptyp:
                {
                    const char* label = ilabel_to_str(em.em_ilb);
                    data_offset(label, 0, ro);
                    data_block_label(label);
                    break;
                }

				default:
                    unknown_type("con, rom");
			}
			break;
        }

        case ps_bss:
        {
            switch (em.em_arg.ema_argtype)
            {
                case cst_ptyp:
                    data_bss(EM_bsssize, em.em_cst);
                    break;
                    
				case ico_ptyp:
				case uco_ptyp:
                {
                    arith val = safe_atol(em.em_string);
                    data_int(val, em.em_size, false);
                    break;
                }

                case sof_ptyp:
                    data_offset(strdup(em.em_dnam), em.em_off, false);
                    break;

                default:
                    unknown_type("bss");
            }
            break;
        }

		case ps_pro: /* procedure start */
        {
            struct symbol* symbol;

            current_proc = calloc(sizeof(struct procedure), 1);
            current_proc->name = strdup(em.em_pnam);
            current_proc->entry = bb_get(current_proc->name);
            current_proc->locals_size = em.em_nlocals;
            code_bb = current_proc->entry;
            code_bb->is_root = true;
            array_append(&current_proc->blocks, code_bb);

            symbol = symbol_get(current_proc->name);
            symbol->section = SECTION_TEXT;
            symbol->proc = current_proc;
            symbol->is_proc = true;
            break;
        }

		case ps_end: /* procedure end */
            tb_procedure();

            current_proc = NULL;
            code_bb = NULL;
			break;

		default:
            fatal("unknown pseudo with opcode %d\n", em.em_opcode);
	}
}

static arith mes_get_cst(void)
{
    EM_getinstr(&em);
    if (em.em_type != EM_MESARG)
        fatal("malformed MES");
    return em.em_cst;
}

static void parse_mes(void)
{
    assert(em.em_arg.ema_argtype == cst_ptyp);
    switch (em.em_cst)
    {
        case 0: /* error */
            fatal("MES 0 received (explicit halt)");

        case 3: /* register variable */
        {
            /* ego will sometimes generate 'mes 3' pseudos with no actual
             * parameters. Detect and ignore these. */
            
            EM_getinstr(&em);
            if (em.em_type == EM_MESARG)
            {
                arith offset = em.em_cst;
                int size = mes_get_cst();
                int type = mes_get_cst();
                int priority = mes_get_cst();
                tb_regvar(current_proc, offset, size, type, priority);
            }
            break;
        }
    }

    while ((em.em_type == EM_STARTMES) || (em.em_type == EM_MESARG))
        EM_getinstr(&em);

    if (em.em_type != EM_ENDMES)
        fatal("malformed MES");
}

static void create_data_label(const char* label)
{
    data_label(label);
    if (current_proc)
    {
        /* Create the fake bb used to track values inside this data block
         * (as it's a chance it's a jump table which we'll need to process
         * later).
         */
        data_bb = bb_get(label);
        data_bb->is_fake = true;
    }
}

void parse_em(void)
{
    EM_getinstr(&em);
	tb_filestart();

	while (em.em_type != EM_EOF)
	{
        switch (em.em_type)
        {
            case EM_PSEU:
				parse_pseu();
                break;

            case EM_DEFILB:
                queue_ilabel(em.em_ilb);
                break;

            case EM_DEFDLB:
                create_data_label(dlabel_to_str(em.em_dlb));
                break;

            case EM_DEFDNAM:
                create_data_label(strdup(em.em_dnam));
                break;

            case EM_STARTMES:
                parse_mes();
                break;

            case EM_MNEM:
                if (code_bb)
                {
                    int flags = em_flag[em.em_opcode - sp_fmnem];

                    if (flags & EM_PAR)
                    {
                        switch (em.em_argtype)
                        {
                            case 0:
                                /* This is an instruction which would normally
                                 * take a size, but the size is provided on the
                                 * stack. We hates them. */
                                queue_insn_simple(em.em_opcode);
                                break;
                                
                            case ilb_ptyp:
                                queue_insn_ilabel(em.em_opcode, em.em_ilb);
                                break;

                            case nof_ptyp:
                                queue_insn_label(em.em_opcode,
                                    dlabel_to_str(em.em_dlb), em.em_off);
                                break;

                            case sof_ptyp:
                                queue_insn_label(em.em_opcode,
                                    strdup(em.em_dnam), em.em_off);
                                break;

                            case pro_ptyp:
                                queue_insn_label(em.em_opcode,
                                    strdup(em.em_pnam), 0);
                                break;

                            case cst_ptyp:
                                if ((flags & EM_PAR) == PAR_B)
                                    queue_insn_ilabel(em.em_opcode, em.em_ilb);
                                else
                                    queue_insn_value(em.em_opcode, em.em_cst);
                                break;

                            default:
                                unknown_type("instruction");
                        }
                    }
                    else
                        queue_insn_simple(em.em_opcode);
                }
                break;

            default:
                fatal("unrecognised instruction type '%d'", em.em_type);
        }

		EM_getinstr(&em);
	}

	tb_fileend();
}

/* vim: set sw=4 ts=4 expandtab : */
