/*
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#ifndef _PIO_TYPES_H
#define _PIO_TYPES_H

#include <cstdint>
#include <string>
#include <map>
#include <set>
#include <utility>
#include <vector>
#include <memory>

#include "location.h"
#include "pio_enums.h"

struct src_item {
    yy::location location;

    src_item() = default;

    explicit src_item(const yy::location &location) : location(location) {}
};

struct program;
struct pio_assembler;

struct resolvable : public src_item {
    resolvable(const yy::location &l) : src_item(l) {}

    int resolve(const program &program);

    int resolve(pio_assembler *pioasm, const program *program) {
        return resolve(pioasm, program, *this);
    }

    virtual int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) = 0;

    virtual ~resolvable() = default;
};

using rvalue = std::shared_ptr<resolvable>;

enum struct inst_type {
    jmp = 0x0,
    wait = 0x1,
    in = 0x2,
    out = 0x3,
    push_pull = 0x4,
    mov = 0x5,
    irq = 0x6,
    set = 0x7,
};

/* condition codes */
enum struct condition {
    al = 0x0,
    xz = 0x1,
    xnz__ = 0x2,
    yz = 0x3,
    ynz__ = 0x4,
    xney = 0x5,
    pin = 0x6,
    osrez = 0x7,
};

/* in source / out / set target - not all valid */
enum struct in_out_set {
    in_out_set_pins = 0x0,
    in_out_set_x = 0x1,
    in_out_set_y = 0x2,
    in_out_null = 0x3,
    in_out_set_pindirs = 0x4,
    in_status = 0x5,
    out_set_pc = 0x5,
    in_out_isr = 0x6,
    in_osr = 0x7,
    out_exec = 0x7,
};

enum struct irq {
    set = 0x0,
    set_wait = 0x1,
    clear = 0x2,
};

// mov src/dest (not all valid)
enum struct mov {
    pins = 0x0,
    x = 0x1,
    y = 0x2,
    null = 0x3,
    pindirs = 0x3,
    exec = 0x4,
    pc = 0x5,
    status = 0x5,
    isr = 0x6,
    osr = 0x7,
    fifo_y = 0x8,
    fifo_index = 0x9,
};

enum struct mov_status_type {
    unspecified = -1,
    tx_lessthan = 0,
    rx_lessthan = 1,
    irq_set = 2,
};

struct extended_mov {
    mov loc;
    rvalue fifo_index;

    extended_mov() : loc(mov::pindirs), fifo_index(nullptr) {}
    extended_mov(mov _type) : loc(_type), fifo_index(nullptr) {}
    extended_mov(rvalue _fifo_index) : loc(mov::fifo_index), fifo_index(_fifo_index) {}

    bool uses_fifo(void) const { return loc == mov::fifo_index || loc == mov::fifo_y; }
};

enum struct mov_op {
    none = 0x0,
    invert = 0x1,
    bit_reverse = 0x2,
};

struct wait_source {
    enum type {
        gpio = 0x0,
        pin = 0x1,
        irq = 0x2,
        jmppin = 0x3,
    } target;
    rvalue param;
    int irq_type;

    wait_source(type target, rvalue param = 0, int irq_type = 0) : target(target), param(std::move(param)), irq_type(irq_type) {}
};

struct name_ref : public resolvable {
    std::string name;

    name_ref(const yy::location &l, std::string name) : resolvable(l), name(std::move(name)) {}

    int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) override;
};

struct code_block : public resolvable {
    std::string lang;
    std::string contents;

    code_block(const yy::location &l, std::string lang, std::string contents) : resolvable(l), lang(std::move(lang)),
                                                                                contents(std::move(contents)) {}

    int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) override {
        return 0;
    }
};

struct int_value : public resolvable {
    int value;

    int_value(const yy::location &l, int value) : resolvable(l), value(value) {}

    int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) override {
        return value;
    }
};

static inline rvalue resolvable_int(const yy::location &l, int v) {
    return std::shared_ptr<resolvable>(new int_value(l, v));
}

struct binary_operation : public resolvable {
    enum op_type {
        add,
        subtract,
        multiply,
        divide,
        and_, // pesky C++
        or_,
        xor_,
        shl_,
        shr_
    };

    op_type op;
    rvalue left, right;

    binary_operation(const yy::location &l, op_type op, rvalue left, rvalue right) :
            resolvable(l), op(op), left(std::move(left)), right(std::move(right)) {}

    int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) override;
};

struct unary_operation : public resolvable {
    enum op_type {
        negate,
        reverse
    };

    op_type op;
    rvalue arg;

    unary_operation(const yy::location &l, op_type op, const rvalue &arg) :
            resolvable(l), op(op), arg(arg) {}

    int resolve(pio_assembler *pioasm, const program *program, const resolvable &scope) override;
};

struct symbol : public src_item {
    std::string name;
    rvalue value;
    bool is_public;
    bool is_label;
    int resolve_started;

    symbol(const yy::location &l, std::string name, bool is_extern = false) : src_item(l), name(std::move(name)),
                                                                              is_public(is_extern), is_label(false),
                                                                              resolve_started(false) {}
};

struct raw_encoding {
    enum inst_type type;
    uint arg1;
    uint arg2;
};

struct instruction : public src_item {
    rvalue sideset; // possibly null
    rvalue delay;

    instruction(const yy::location &l) : src_item(l) {}

    // validate while adding instruciton
    virtual void pre_validate(program &program) {}

    virtual uint encode(program &program);

    virtual raw_encoding raw_encode(program &program);
};

struct pio_assembler;

// rvalue with extra encompassing location
struct rvalue_loc {
    rvalue value;
    yy::location location;

    rvalue_loc() = default;

    rvalue_loc(const rvalue &v, const yy::location &l) : value(v), location(l) {}
};

struct in_out {
    yy::location location;
    rvalue pin_count;
    bool right;
    bool autop;
    rvalue threshold;
    int final_pin_count = -1; // not specified
    int final_threshold;
};

struct program : public src_item {
    static const int MAX_INSTRUCTIONS = 32;

    pio_assembler *pioasm;
    std::string name;
    rvalue_loc origin;
    rvalue_loc sideset;
    rvalue_loc set_count;
    in_out in;
    in_out out;
    bool sideset_opt;
    bool sideset_pindirs;

    rvalue wrap_target;
    rvalue wrap;

    int pio_version = 0;
    uint clock_div_int = 1;
    uint clock_div_frac = 0;
    yy::location fifo_loc;
    fifo_config fifo = fifo_config::txrx;
    // 1 bit of bitmap per 16 pins used
    uint8_t used_gpio_ranges = 0;

    std::map<std::string, std::shared_ptr<symbol>> symbols;
    std::vector<std::shared_ptr<symbol>> ordered_symbols;
    std::vector<std::shared_ptr<instruction>> instructions;
    std::map<std::string, std::vector<code_block>> code_blocks;
    std::map<std::string, std::vector<std::pair<std::string,std::string>>> lang_opts;
    struct {
        mov_status_type type = mov_status_type::unspecified;
        rvalue n;
        int param;
        int final_n; // post finalization
    } mov_status;

    // post finalization
    int delay_max;
    int sideset_bits_including_opt; // specified side set bits + 1 if we need presence flag
    int sideset_max;
    int final_set_count = -1;
    int final_out_count = -1;
    int final_in_count = -1;

    program(pio_assembler *pioasm, const yy::location &l, std::string name) :
            src_item(l), pioasm(pioasm), name(std::move(name)), sideset_opt(true), sideset_pindirs(false) {}

    void set_origin(const yy::location &l, const rvalue &_origin) {
        origin = rvalue_loc(_origin, l);
    }

    void set_wrap_target(const yy::location &l);

    void set_wrap(const yy::location &l);

    void set_sideset(const yy::location &l, rvalue _sideset, bool optional, bool pindirs) {
        sideset = rvalue_loc(_sideset, l);
        sideset_opt = optional;
        sideset_pindirs = pindirs;
    }

    void set_out(const yy::location &l, rvalue v, bool right, bool autop, rvalue threshold) {
        out.location = l;
        out.pin_count = std::move(v);
        out.right = right;
        out.autop = autop;
        out.threshold = threshold;
    }

    void set_in(const yy::location &l, rvalue v, bool right, bool autop, rvalue threshold) {
        in.location = l;
        in.pin_count = std::move(v);
        in.right = right;
        in.autop = autop;
        in.threshold = threshold;
    }

    void set_set_count(const yy::location &l, rvalue v) {
        set_count = rvalue_loc(v, l);
    }

    void add_label(std::shared_ptr<symbol> label) {
        label->value = resolvable_int(label->location, instructions.size());
        add_symbol(label);
    }

    void add_symbol(std::shared_ptr<symbol> symbol);

    void add_instruction(std::shared_ptr<instruction> inst);

    void add_code_block(const code_block &block);

    void add_lang_opt(std::string lang, std::string name, std::string value);

    void set_pio_version(const yy::location &l, int version);

    void set_clock_div(const yy::location &l, float clock_div);

    void set_fifo_config(const yy::location &l, fifo_config config);

    void set_mov_status(mov_status_type type, rvalue n, int param = 0) {
        mov_status.type = type;
        mov_status.n = n;
        mov_status.param = param;
    }

    void finalize();
protected:
    void init_pio_version();
};

struct instr_jmp : public instruction {
    condition cond;
    rvalue target;

    instr_jmp(const yy::location &l, condition c, rvalue target) : instruction(l), cond(c), target(std::move(target)) { }

    raw_encoding raw_encode(program &program) override;
};

struct instr_wait : public instruction {
    rvalue polarity;
    std::shared_ptr<wait_source> source;

    instr_wait(const yy::location &l, rvalue polarity, std::shared_ptr<wait_source> source) : instruction(l), polarity(
            std::move(polarity)), source(std::move(source)) {}

    raw_encoding raw_encode(program &program) override;
};

struct instr_in : public instruction {
    enum in_out_set src;
    rvalue value;

    instr_in(const yy::location &l, const enum in_out_set &src, rvalue value) : instruction(l), src(src),
                                                                                value(std::move(value)) {}

    raw_encoding raw_encode(program &program) override;
};

struct instr_out : public instruction {
    enum in_out_set dest;
    rvalue value;

    instr_out(const yy::location &l, const enum in_out_set &dest, rvalue value) : instruction(l), dest(dest),
                                                                                  value(std::move(value)) {}

    raw_encoding raw_encode(program &program) override;
};

struct instr_set : public instruction {
    enum in_out_set dest;
    rvalue value;

    instr_set(const yy::location &l, const enum in_out_set &dest, rvalue value) : instruction(l), dest(dest),
                                                                                  value(std::move(value)) {}

    raw_encoding raw_encode(program &program) override;
};


struct instr_push : public instruction {
    bool if_full, blocking;

    instr_push(const yy::location &l, bool if_full, bool blocking) : instruction(l), if_full(if_full),
                                                                     blocking(blocking) {}


    void pre_validate(program& program) override;
    raw_encoding raw_encode(program &program) override {
        uint arg1 = (blocking ? 1u : 0u) | (if_full ? 0x2u : 0);
        return {inst_type::push_pull, arg1, 0};
    }
};

struct instr_pull : public instruction {
    bool if_empty, blocking;

    instr_pull(const yy::location &l, bool if_empty, bool blocking) : instruction(l), if_empty(if_empty),
                                                                      blocking(blocking) {}

    raw_encoding raw_encode(program &program) override {
        uint arg1 = (blocking ? 1u : 0u) | (if_empty ? 0x2u : 0) | 0x4u;
        return {inst_type::push_pull, arg1, 0};
    }
};

struct instr_mov : public instruction {
    extended_mov dest, src;
    mov_op op;

    instr_mov(const yy::location &l, const extended_mov &dest, const extended_mov &src, const mov_op& op = mov_op::none) :
            instruction(l), dest(dest), src(src), op(op) {}

    uint get_push_get_index(const program &program, extended_mov index);
    void pre_validate(program& program) override;
    raw_encoding raw_encode(program &program) override;
};

struct instr_irq : public instruction {
    enum irq modifiers;
    rvalue num;
    int irq_type;

    instr_irq(const yy::location &l, const enum irq &modifiers, rvalue num, int irq_type = 0) :
            instruction(l), modifiers(modifiers), num(std::move(num)), irq_type(irq_type) {}

    raw_encoding raw_encode(program &program) override;
};


struct instr_nop : public instr_mov {
    instr_nop(const yy::location &l) : instr_mov(l, mov::y, mov::y) {}
};

struct instr_word : public instruction {
    rvalue encoding;

    instr_word(const yy::location &l, rvalue encoding) : instruction(l), encoding(std::move(encoding)) {}

    uint encode(program &program) override;
};

#endif
