/*
 * Copyright (c) 2019, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef TEST_UTILS_H
#define TEST_UTILS_H

#include <list>

// test function prototypes
typedef void (*TF1)(PinName p0);
typedef void (*TF2)(PinName p0, PinName p1);
typedef void (*TF3)(PinName p0, PinName p1, PinName p2);
typedef void (*TF4)(PinName p0, PinName p1, PinName p2, PinName p3);
typedef void (*TF5)(PinName p0, PinName p1, PinName p2, PinName p3, PinName p4);

template<typename PortType, typename FunctionType, FunctionType f>
struct FunctionCaller {

};

template<typename PortType, TF1 f>
struct FunctionCaller<PortType, TF1, f> {
    void operator()(PortType &port)
    {
        f(port.pins[0]);
    }
};

template<typename PortType, TF2 f>
struct FunctionCaller<PortType, TF2, f> {
    void operator()(PortType &port)
    {
        f(port.pins[0], port.pins[1]);
    }
};

template<typename PortType, TF3 f>
struct FunctionCaller<PortType, TF3, f> {
    void operator()(PortType &port)
    {
        f(port.pins[0], port.pins[1], port.pins[2]);
    }
};

template<typename PortType, TF4 f>
struct FunctionCaller<PortType, TF4, f> {
    void operator()(PortType &port)
    {
        f(port.pins[0], port.pins[1], port.pins[2], port.pins[3]);
    }
};

template<typename PortType, TF5 f>
struct FunctionCaller<PortType, TF5, f> {
    void operator()(PortType &port)
    {
        f(port.pins[0], port.pins[1], port.pins[2], port.pins[3], port.pins[4]);
    }
};

template <typename PortType>
bool peripheral_comparator(const PortType &port1, const PortType &port2)
{
    return port1.peripheral == port2.peripheral;
}

template <typename PortType>
bool peripheral_less(const PortType &port1, const PortType &port2)
{
    return port1.peripheral < port2.peripheral;
}

template<typename PortType, typename FormFactorType>
static bool find_port_pins(PortType &port)
{
    return pinmap_find_peripheral_pins(FormFactorType::pins(), FormFactorType::restricted_pins(),
                                       port.peripheral, PortType::PinMap::maps, port.ppins, PortType::pin_count);
}

template<typename PortType, typename FormFactorType>
void find_ports(std::list<PortType> &matched_ports, std::list<PortType> &not_matched_ports)
{
    // Loop through every pin type
    for (uint32_t i = 0; i < PortType::pin_count; i++) {
        const PinMap *map = PortType::PinMap::maps[i];
        const char *pin_type = PortType::PinMap::pin_type_names[i];

        // Loop through each pin of a given type
        for (; map->pin != NC; map++) {
            PortType port;
            // Set pin being tested
            port.pins[i] = map->pin;
            port.peripheral = map->peripheral;
            // Only form factor pins can be tested
            if (!pinmap_list_has_pin(FormFactorType::pins(), port.pins[i])) {
                continue;
            }
            // Don't test restricted pins
            if (pinmap_list_has_pin(FormFactorType::restricted_pins(), port.pins[i])) {
                utest_printf("Skipping %s pin %s (%i)\r\n", pin_type,
                             FormFactorType::pin_to_string(port.pins[i]), port.pins[i]);
                continue;
            }
            if (pinmap_list_has_peripheral(pinmap_restricted_peripherals(), port.peripheral)) {
                utest_printf("Skipping %s peripheral %i with pin %s (%i)\r\n", pin_type,
                             port.peripheral, FormFactorType::pin_to_string(port.pins[i]), port.pins[i]);
                continue;
            }
            // skipp pin searching if single pin port type
            if (PortType::pin_count > 1) {
                find_port_pins<PortType, FormFactorType>(port);
            }
            if (port.empty()) {
                not_matched_ports.push_back(port);
            } else {
                matched_ports.push_back(port);
            }
        }
    }
}


template<typename PortType, typename FormFactorType, typename FunctionType, FunctionType f>
void test_all_ports(std::list<PortType> &matched_ports, std::list<PortType> &not_matched_ports)
{
    typedef typename std::list<PortType>::iterator Iter;
    utest_printf("***Testing %s on all form factor ports***\n", PortType::PinMap::name);
    const PinList *ff_pins = FormFactorType::pins();
    FunctionCaller<PortType, FunctionType, f> call;

    if (matched_ports.empty() && not_matched_ports.empty()) {
        utest_printf("Could not find pins for %s testing \n", PortType::PinMap::name);
        return;
    }

    for (uint32_t i = 0; i < ff_pins->count; i++) {
        for (Iter it = matched_ports.begin(); it != matched_ports.end(); ++it) {
            PortType &port = *it;
            for (uint32_t j = 0; j < PortType::pin_count; j++) {
                if (ff_pins->pins[i] == port.pins[j]) {
                    utest_printf("%3s - %s pin tested on port: %s...", FormFactorType::pin_to_string(ff_pins->pins[i]),
                                 PortType::PinMap::pin_type_names[j], port.str());
                    if (port.status == PortType::StatusNotTested) {
                        call(port);
                        port.status = PortType::StatusPass;
                    }
                    utest_printf("%s\n", port.status == PortType::StatusPass ? "succeeded" : "failed");
                    goto end_port_iteration;
                }
            }
        }
        for (Iter it = not_matched_ports.begin(); it != not_matched_ports.end(); ++it) {
            PortType &port = *it;
            for (uint32_t j = 0; j < PortType::pin_count; j++) {
                if (ff_pins->pins[i] == port.pins[j]) {
                    utest_printf("%3s - Could not find pins to test %s pin %s (%d)\n",
                                 FormFactorType::pin_to_string(ff_pins->pins[i]),
                                 PortType::PinMap::pin_type_names[j],
                                 FormFactorType::pin_to_string(ff_pins->pins[i]),
                                 ff_pins->pins[i]);
                    goto end_port_iteration;
                }
            }
        }
end_port_iteration:
        ;
    }
}

template<typename PortType, typename FunctionType, FunctionType f>
void test_peripheral(PortType &port)
{
    if (port.empty()) {
        utest_printf("%d - Could not find pins to test peripheral\n", port.peripheral);
    } else {
        utest_printf("%d - peripheral tested on port: %s...", port.peripheral, port.str());
        if (port.status == PortType::StatusNotTested) {
            FunctionCaller<PortType, FunctionType, f> call;
            call(port); // run test
            port.status = PortType::StatusPass;
        }
        utest_printf("%s\n", port.status == PortType::StatusPass ? "succeeded" : "failed");
    }
}

template<typename PortType, typename FunctionType, FunctionType f>
void test_all_peripherals(std::list<PortType> &matched_ports, std::list<PortType> &not_matched_ports)
{
    typedef typename std::list<PortType>::iterator Iter;
    utest_printf("***Testing all %s peripherals***\n", PortType::PinMap::name);

    if (matched_ports.empty() && not_matched_ports.empty()) {
        utest_printf("Could not find pins for %s testing \n", PortType::PinMap::name);
        return;
    }

    matched_ports.sort(peripheral_less<PortType>);
    not_matched_ports.sort(peripheral_less<PortType>);

    for (Iter m_it = matched_ports.begin(), nm_it = not_matched_ports.begin();
            m_it != matched_ports.end() || nm_it != not_matched_ports.end();) {
        if (m_it != matched_ports.end() && nm_it != not_matched_ports.end()) {
            if ((*m_it).peripheral < (*nm_it).peripheral) {
                test_peripheral<PortType, FunctionType, f>(*m_it);
                ++m_it;
            } else {
                test_peripheral<PortType, FunctionType, f>(*nm_it);
                ++nm_it;
            }
        } else if (m_it != matched_ports.end()) {
            test_peripheral<PortType, FunctionType, f>(*m_it);
            ++m_it;
        } else if (nm_it != not_matched_ports.end()) {
            test_peripheral<PortType, FunctionType, f>(*nm_it);
            ++nm_it;
        }
    }
}

/**
 * Test function for all pinouts of all peripherals of a given type
 *
 * This template function takes in three template parameters:
 * - PortType - The type of peripheral to test
 * - FormFactorType - The form factor to test on
 * - f - The test function to run.
 *
 * This function calls the test function multiple times with
 * the appropriate combinations of pins.
 */
template<typename PortType, typename FormFactorType, typename PortType::TestFunctionType f>
void all_ports()
{
    std::list<PortType> matched_ports, not_matched_ports;
    find_ports<PortType, FormFactorType>(matched_ports, not_matched_ports);
    matched_ports.unique();
    not_matched_ports.unique();
    test_all_ports<PortType, FormFactorType, typename PortType::TestFunctionType, f>(matched_ports, not_matched_ports);
}

/**
 * Test function for one pinout of all peripherals of a given type
 *
 * This template function takes in three template parameters:
 * - PortType - The type of peripheral to test
 * - FormFactorType - The form factor to test on
 * - f - The test function to run.
 *
 * This function calls the test function once for each peripheral
 * of the given type.
 */
template<typename PortType, typename FormFactorType, typename PortType::TestFunctionType f>
void all_peripherals()
{
    std::list<PortType> matched_ports, not_matched_ports;
    find_ports<PortType, FormFactorType>(matched_ports, not_matched_ports);

    matched_ports.sort(peripheral_less<PortType>);
    not_matched_ports.sort(peripheral_less<PortType>);
    matched_ports.unique(peripheral_comparator<PortType>);
    not_matched_ports.unique(peripheral_comparator<PortType>);

    test_all_peripherals<PortType, typename PortType::TestFunctionType, f>(matched_ports, not_matched_ports);
}

/**
 * Test function for one pinout of one peripheral of a given type
 *
 * This template function takes in three template parameters:
 * - PortType - The type of peripheral to test
 * - FormFactorType - The form factor to test on
 * - f - The test function to run.
 *
 * This function calls the test function once for one peripheral
 * of the given type.
 */
template<typename PortType, typename FormFactorType, typename PortType::TestFunctionType f>
void one_peripheral()
{
    std::list<PortType> matched_ports, not_matched_ports;
    find_ports<PortType, FormFactorType>(matched_ports, not_matched_ports);

    utest_printf("***Testing one %s pin configuration***\n", PortType::PinMap::name);
    if (matched_ports.empty()) {
        utest_printf("Could not find pins for %s testing \n", PortType::PinMap::name);
    } else {
        test_peripheral<PortType, typename PortType::TestFunctionType, f>(matched_ports.front());
    }
}

template <uint32_t N, typename PinMapType, typename FormFactorType, typename TestFunctionType>
class Port;

template <uint32_t N, typename PinMapType, typename FormFactorType, typename TestFunctionType>
bool operator== (const Port<N, PinMapType, FormFactorType, TestFunctionType> &port1,
                 const Port<N, PinMapType, FormFactorType, TestFunctionType> &port2);

template <uint32_t N, typename PinMapType, typename FormFactorType, typename FunctionType>
class Port {
public:
    int peripheral;
    PinName pins[N];
    PinName *ppins[N];

    static const uint32_t pin_count = N;
    typedef PinMapType PinMap;
    typedef FunctionType TestFunctionType;

    enum Status { StatusPass, StatusFail, StatusNotTested };
    Status status;

    Port(): peripheral(NC), status(StatusNotTested)
    {
        init_pins();
    }

    Port(const Port &port)
    {
        init_pins();
        copy_from(port);
    }

    void init_pins()
    {
        for (uint32_t i = 0; i < N; i++) {
            pins[i] = NC;
            ppins[i] = &pins[i];
        }
    }

    void copy_from(const Port &port)
    {
        peripheral = port.peripheral;
        status = port.status;
        for (uint32_t i = 0; i < N; i++) {
            pins[i] = port.pins[i];
        }
    }

    bool empty()
    {
        if (peripheral == NC) {
            return true;
        }
        for (uint32_t i = 0; i < N; i++) {
            if (pins[i] == NC) {
                return true;
            }
        }
        return false;
    }

    const char *str()
    {
        static char port_str[128];
        char pin_str[32];
        sprintf(port_str, "peripheral=(%d) ", peripheral);
        for (uint32_t i = 0; i < N; i++) {
            sprintf(pin_str, "%s=(%s) ", PinMap::pin_type_names[i], FormFactorType::pin_to_string(pins[i]));
            strcat(port_str, pin_str);
        }
        return port_str;
    }

    friend bool operator==<> (const Port<N, PinMapType, FormFactorType, FunctionType> &port1, const Port<N, PinMapType, FormFactorType, FunctionType> &port2);
};

template <uint32_t N, typename PinMapType, typename FormFactorType, typename FunctionType>
const uint32_t Port<N, PinMapType, FormFactorType, FunctionType>::pin_count;

template <uint32_t N, typename PinMapType, typename FormFactorType, typename FunctionType>
bool operator== (const Port<N, PinMapType, FormFactorType, FunctionType> &port1, const Port<N, PinMapType, FormFactorType, FunctionType> &port2)
{
    if (port1.peripheral != port2.peripheral) {
        return false;
    }
    for (uint32_t i = 0; i < N; i++) {
        if (port1.pins[i] != port2.pins[i]) {
            return false;
        }
    }
    return true;
}

/**
 * This is a convenience class for use with the above templates
 *
 * This class can be passed as a template parameter to all_ports,
 * all_peripherals or one_peripheral to choose test pins from
 * the default form factor.
 */
class DefaultFormFactor {
public:
    static const PinList *pins()
    {
        return pinmap_ff_default_pins();
    }

    static const PinList *restricted_pins()
    {
        return pinmap_restricted_pins();
    }

    static const char *pin_to_string(PinName pin)
    {
        return pinmap_ff_default_pin_to_string(pin);
    }
};

/*
 * Peripheral port declarations are given below
 *
 * Each Port type represents a set of pins used by a peripheral.
 * The Port typedef is used as a template parameter to the functions
 * all_ports, all_peripherals and one_peripheral to select the peripheral
 * pin set to use for testing.
 */

struct GPIOMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *GPIOMaps::maps[] = { gpio_pinmap() };
const char *const GPIOMaps::pin_type_names[] = { "IO" };
const char *const GPIOMaps::name = "GPIO";
typedef Port<1, GPIOMaps, DefaultFormFactor, TF1> GPIOPort;

#if DEVICE_INTERRUPTIN
#include "gpio_irq_api.h"
struct GPIOIRQMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *GPIOIRQMaps::maps[] = { gpio_irq_pinmap() };
const char *const GPIOIRQMaps::pin_type_names[] = { "IRQ_IN" };
const char *const GPIOIRQMaps::name = "GPIO_IRQ";
typedef Port<1, GPIOIRQMaps, DefaultFormFactor, TF1> GPIOIRQPort;
#endif

#if DEVICE_SPI
#include "spi_api.h"
struct SPIMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *SPIMaps::maps[] = { spi_master_mosi_pinmap(), spi_master_miso_pinmap(), spi_master_clk_pinmap(), spi_master_cs_pinmap() };
const char *const SPIMaps::pin_type_names[] = { "MOSI", "MISO", "SCLK", "SSEL" };
const char *const SPIMaps::name = "SPI";
typedef Port<4, SPIMaps, DefaultFormFactor, TF4> SPIPort;

struct SPINoCSMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *SPINoCSMaps::maps[] = { spi_master_mosi_pinmap(), spi_master_miso_pinmap(), spi_master_clk_pinmap()};
const char *const SPINoCSMaps::pin_type_names[] = { "MOSI", "MISO", "SCLK" };
const char *const SPINoCSMaps::name = "SPI";
typedef Port<3, SPINoCSMaps, DefaultFormFactor, TF3> SPINoCSPort;

struct SPISlaveMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *SPISlaveMaps::maps[] = { spi_slave_mosi_pinmap(), spi_slave_miso_pinmap(), spi_slave_clk_pinmap(), spi_slave_cs_pinmap() };
const char *const SPISlaveMaps::pin_type_names[] = { "MOSI", "MISO", "SCLK", "SSEL" };
const char *const SPISlaveMaps::name = "SPISlave";
typedef Port<4, SPISlaveMaps, DefaultFormFactor, TF4> SPISlavePort;
#endif

#if DEVICE_I2C
#include "i2c_api.h"
struct I2CMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *I2CMaps::maps[] = { i2c_master_sda_pinmap(), i2c_master_scl_pinmap() };
const char *const I2CMaps::pin_type_names[] = { "SDA", "SCL" };
const char *const I2CMaps::name = "I2C";
typedef Port<2, I2CMaps, DefaultFormFactor, TF2> I2CPort;
#endif

#if DEVICE_PWMOUT
#include "pwmout_api.h"
struct PWMMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *PWMMaps::maps[] = { pwmout_pinmap() };
const char *const PWMMaps::pin_type_names[] = { "PWM_OUT" };
const char *const PWMMaps::name = "PWM";
typedef Port<1, PWMMaps, DefaultFormFactor, TF1> PWMPort;
#endif

#if DEVICE_ANALOGIN
#include "analogin_api.h"
struct AnaloginMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *AnaloginMaps::maps[] = { analogin_pinmap() };
const char *const AnaloginMaps::pin_type_names[] = { "ADC_IN" };
const char *const AnaloginMaps::name = "ADC";
typedef Port<1, AnaloginMaps, DefaultFormFactor, TF1> AnaloginPort;
#endif

#if DEVICE_ANALOGOUT
#include "analogout_api.h"
struct AnalogoutMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *AnalogoutMaps::maps[] = { analogout_pinmap() };
const char *const AnalogoutMaps::pin_type_names[] = { "DAC_OUT" };
const char *const AnalogoutMaps::name = "DAC";
typedef Port<1, AnalogoutMaps, DefaultFormFactor, TF1> AnalogoutPort;
#endif

#if DEVICE_SERIAL
#if DEVICE_SERIAL_FC
struct UARTMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *UARTMaps::maps[] = { serial_tx_pinmap(), serial_rx_pinmap(), serial_cts_pinmap(), serial_rts_pinmap() };
const char *const UARTMaps::pin_type_names[] = { "TX", "RX", "CLS", "RTS" };
const char *const UARTMaps::name = "UART";
typedef Port<4, UARTMaps, DefaultFormFactor, TF4> UARTPort;
#endif

struct UARTNoFCMaps {
    static const PinMap *maps[];
    static const char *const pin_type_names[];
    static const char *const name;
};
const PinMap *UARTNoFCMaps::maps[] = { serial_tx_pinmap(), serial_rx_pinmap() };
const char *const UARTNoFCMaps::pin_type_names[] = { "TX", "RX" };
const char *const UARTNoFCMaps::name = "UART-no-FC";
typedef Port<2, UARTNoFCMaps, DefaultFormFactor, TF2> UARTNoFCPort;
#endif

#endif
