/* Copyright 2020 Dimitris Papavasiliou <dpapavas@protonmail.ch>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <spi_master.h>

#include "quantum.h"
#include "split_util.h"
#include "transport.h"
#include "timer.h"

#include "lagrange.h"

struct led_context {
    led_t led_state;
    layer_state_t layer_state;
};

uint8_t transceive(uint8_t b) {
    for (SPDR = b ; !(SPSR & _BV(SPIF)) ; );
    return SPDR;
}

/* The SPI bus, doesn't have any form of protocol built in, so when
 * the other side isn't present, any old noise on the line will appear
 * as matrix data.  To avoid interpreting data as keystrokes, we do a
 * simple n-way (8-way here) handshake before each scan, where each
 * side sends a prearranged sequence of bytes. */

bool shake_hands(bool master) {
    const uint8_t m = master ? 0xf8 : 0;
    const uint8_t a = 0xa8 ^ m, b = 0x50 ^ m;
    bool synchronized = true;

    uint8_t i;

    i = SPSR;
    i = SPDR;

    do {
        /* Cycling the SS pin on each attempt is necessary, as it
         * resets the AVR's SPI core and guarantees proper
         * alignment. */

        if (master) {
            writePinLow(SPI_SS_PIN);
        }

        for (i = 0 ; i < 8 ; i += 1) {
            if (transceive(a + i) != b + i) {
                synchronized = false;
                break;
            }
        }

        if (master) {
            writePinHigh(SPI_SS_PIN);
        }
    } while (i < 8);

    return synchronized;
}

bool transport_master(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) {
    const struct led_context context = {
        host_keyboard_led_state(),
        layer_state
    };

    uint8_t i;

    /* We shake hands both before and after transmitting the matrix.
     * Doing it before transmitting is necessary to ensure
     * synchronization: Due to the master-slave nature of the SPI bus,
     * the master calls the shots.  If we just go ahead and start
     * clocking bits, the slave side might be otherwise engaged at
     * that moment, so we'll initially read zeros, or garbage.  Then
     * when the slave gets around to transmitting its matrix, we'll
     * misinterpret the keys it sends, leading to spurious
     * keypresses. */

    /* The handshake forces the master to wait for the slave to be
     * ready to start transmitting. */

    do {
        shake_hands(true);

        /* Receive the matrix from the other side, while transmitting
         * LED and layer states. */

        spi_start(SPI_SS_PIN, 0, 0, 4);

        for (i = 0 ; i < sizeof(matrix_row_t[MATRIX_ROWS / 2]) ; i += 1) {
            spi_status_t x;

            x = spi_write(i < sizeof(struct led_context) ?
                          ((uint8_t *)&context)[i] : 0);

            if (x == SPI_STATUS_TIMEOUT) {
                return false;
            }

            ((uint8_t *)slave_matrix)[i] = (uint8_t)x;
        }

        spi_stop();

        /* In case of errors during the transmission, e.g. if the
         * cable was disconnected and since there is no inherent
         * error-checking protocol, we would simply interpret noise as
         * data. */

        /* To avoid this, both sides shake hands after transmitting.
         * If synchronization was lost during transmission, the (first)
         * handshake will fail.  In that case we go around and
         * re-transmit. */

    } while (!shake_hands(true));

    return true;
}

void transport_slave(matrix_row_t master_matrix[], matrix_row_t slave_matrix[]) {
    static struct led_context context;
    struct led_context new_context;

    uint8_t i;

    /* Do the reverse of master above.  Note that timing is critical,
     * so interrupts must be turned off. */

    cli();
    shake_hands(false);

    do {
        for (i = 0 ; i < sizeof(matrix_row_t[MATRIX_ROWS / 2]) ; i += 1) {
            uint8_t b;

            b = transceive(((uint8_t *)slave_matrix)[i]);

            if (i < sizeof(struct led_context)) {
                ((uint8_t *)&new_context)[i] = b;
            }
        }
    } while (!shake_hands(false));

    sei();

    /* Update the layer and LED state if necessary. */

    if (!isLeftHand) {
        if (context.led_state.raw != new_context.led_state.raw) {
            context.led_state.raw = new_context.led_state.raw;
            led_update_kb(context.led_state);
        }

        if (context.layer_state != new_context.layer_state) {
            context.layer_state = new_context.layer_state;
            layer_state_set_kb(context.layer_state);
        }
    }
}

void transport_master_init(void) {
    /* We need to set the SS pin as output as the handshake logic
     * above depends on it and the SPI master driver won't do it
     * before we call spi_start(). */

    writePinHigh(SPI_SS_PIN);
    setPinOutput(SPI_SS_PIN);

    spi_init();

    shake_hands(true);
}

void transport_slave_init(void) {
    /* The datasheet isn't very clear on whether the internal pull-up
     * is selectable when the SS pin is used by the SPI slave, but
     * experimentations shows that it is, at least on the ATMega32u4.
     * We enable the pull-up to guard against the case where both
     * halves end up as slaves.  In that case the SS pin would
     * otherwise be floating and free to fluctuate due to picked up
     * noise, etc. When reading low it would make both halves think
     * they're asserted making the MISO pin an output on both ends and
     * leading to potential shorts. */

    setPinInputHigh(SPI_SS_PIN);
    setPinInput(SPI_SCK_PIN);
    setPinInput(SPI_MOSI_PIN);
    setPinOutput(SPI_MISO_PIN);

    SPCR = _BV(SPE);

    shake_hands(false);
}
