/*******************************************************************************
 * Copyright (C) 2017, 2018 MINRES Technologies GmbH
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 *******************************************************************************/

#include "sysc/SiFive/pwm.h"

#include "scc/utilities.h"
#include "sysc/SiFive/gen/pwm_regs.h"

using namespace sysc;
using namespace sc_core;

pwm::pwm(sc_core::sc_module_name nm)
: sc_core::sc_module(nm)
, tlm_target<>(clk)
, NAMED(clk_i)
, NAMED(rst_i)
, NAMED(cmpgpio_o, 4)
, NAMED(cmpip_o, 4)
, NAMEDD(regs, pwm_regs)
, current_cnt(0)
, last_cnt_update() {
    regs->registerResources(*this);

    regs->pwmcfg.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            if (d.value()) wait(d);
            reg.put(data);
            update_counter();
            return true;
        });
    regs->pwmcount.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            if (d.value()) wait(d);
            reg.put(data);
            update_counter();
            current_cnt = data;
            clk_remainder = 0.;
            return true;
        });
    regs->pwmcount.set_read_cb([this](const scc::sc_register<uint32_t> &reg, uint32_t &data,
                                      sc_core::sc_time d) -> bool {
        auto offset = regs->r_pwmcfg.pwmenalways || regs->r_pwmcfg.pwmenoneshot ? static_cast<int>(get_pulses(d)) : 0;
        data = current_cnt + offset;
        regs->r_pwmcount.pwmcount = data;
        return true;
    });
    regs->pwms.set_write_cb(
        [this](scc::sc_register<uint32_t> &reg, uint32_t data, sc_core::sc_time d) -> bool { return false; });
    regs->pwms.set_read_cb([this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
        auto offset = regs->r_pwmcfg.pwmenalways || regs->r_pwmcfg.pwmenoneshot ? static_cast<int>(get_pulses(d)) : 0;
        auto cnt = current_cnt + offset;
        data = (cnt >> regs->r_pwmcfg.pwmscale) & 0xffff;
        regs->r_pwms.pwms = static_cast<uint16_t>(data);
        return true;
    });
    regs->pwmcmp0.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            reg.put(data);
            update_counter();
            return true;
        });
    regs->pwmcmp1.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            reg.put(data);
            update_counter();
            return true;
        });
    regs->pwmcmp2.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            reg.put(data);
            update_counter();
            return true;
        });
    regs->pwmcmp3.set_write_cb(
        [this](const scc::sc_register<uint32_t> &reg, uint32_t &data, sc_core::sc_time d) -> bool {
            reg.put(data);
            update_counter();
            return true;
        });

    SC_METHOD(clock_cb);
    sensitive << clk_i;
    SC_METHOD(reset_cb);
    sensitive << rst_i;
    SC_METHOD(update_counter);
    sensitive << update_counter_evt;
    dont_initialize();
}

void pwm::clock_cb() {
    update_counter();
    clk = clk_i.read();
}

pwm::~pwm() = default;

void pwm::reset_cb() {
    if (rst_i.read()) {
        regs->reset_start();
    } else {
        regs->reset_stop();
    }
}

void pwm::update_counter() {
    auto now = sc_time_stamp();
    if (now == SC_ZERO_TIME) return;
    update_counter_evt.cancel();
    if (regs->r_pwmcfg.pwmenalways || regs->r_pwmcfg.pwmenoneshot) {
        std::array<bool, 4> pwmcmp_new_ip{false, false, false, false};
        auto dpulses = get_pulses(SC_ZERO_TIME);
        auto pulses = static_cast<int>(dpulses);
        clk_remainder += dpulses - pulses;
        if (clk_remainder > 1) {
            pulses++;
            clk_remainder -= 1.0;
        }
        if (reset_cnt) {
            current_cnt = 0;
            reset_cnt = false;
        } else if (last_enable)
            current_cnt += pulses;
        auto pwms = (current_cnt >> regs->r_pwmcfg.pwmscale) & 0xffff;
        auto next_trigger_time =
            (0xffff - pwms) * (1 << regs->r_pwmcfg.pwmscale) * clk; // next trigger based on wrap around
        if (pwms == 0xffff) {                                       // wrap around calculation
            reset_cnt = true;
            next_trigger_time = clk;
            regs->r_pwmcfg.pwmenoneshot = 0;
        }
        auto pwms0 = (regs->r_pwmcfg.pwmcmp0center && (pwms & 0x8000) == 1) ? pwms ^ 0xffff : pwms;
        if (pwms0 >= regs->r_pwmcmp0.pwmcmp0) {
            pwmcmp_new_ip[0] = true;
            regs->r_pwmcfg.pwmenoneshot = 0;
            if (regs->r_pwmcfg.pwmzerocmp) {
                reset_cnt = true;
                next_trigger_time = clk;
            }
        } else {
            pwmcmp_new_ip[0] = false;
            // TODO: add correct calculation for regs->r_pwmcfg.pwmcmpXcenter==1
            auto nt = (regs->r_pwmcmp0.pwmcmp0 - pwms0) * (1 << regs->r_pwmcfg.pwmscale) * clk;
            next_trigger_time = nt < next_trigger_time ? nt : next_trigger_time;
        }
        auto pwms1 = (regs->r_pwmcfg.pwmcmp0center && (pwms & 0x8000) == 1) ? pwms ^ 0xffff : pwms;
        if (pwms1 >= regs->r_pwmcmp1.pwmcmp0) {
            pwmcmp_new_ip[1] = true;
        } else {
            pwmcmp_new_ip[1] = false;
            // TODO: add correct calculation for regs->r_pwmcfg.pwmcmpXcenter==1
            auto nt = (regs->r_pwmcmp0.pwmcmp0 - pwms0) * (1 << regs->r_pwmcfg.pwmscale) * clk;
            next_trigger_time = nt < next_trigger_time ? nt : next_trigger_time;
        }
        auto pwms2 = (regs->r_pwmcfg.pwmcmp0center && (pwms & 0x8000) == 1) ? pwms ^ 0xffff : pwms;
        if (pwms2 >= regs->r_pwmcmp2.pwmcmp0) {
            pwmcmp_new_ip[2] = true;
        } else {
            pwmcmp_new_ip[2] = false;
            // TODO: add correct calculation for regs->r_pwmcfg.pwmcmpXcenter==1
            auto nt = (regs->r_pwmcmp0.pwmcmp0 - pwms0) * regs->r_pwmcfg.pwmscale * clk;
            next_trigger_time = nt < next_trigger_time ? nt : next_trigger_time;
        }
        auto pwms3 = (regs->r_pwmcfg.pwmcmp0center && (pwms & 0x8000) == 1) ? pwms ^ 0xffff : pwms;
        if (pwms3 >= regs->r_pwmcmp3.pwmcmp0) {
            pwmcmp_new_ip[3] = true;
        } else {
            pwmcmp_new_ip[3] = false;
            // TODO: add correct calculation for regs->r_pwmcfg.pwmcmpXcenter==1
            auto nt = (regs->r_pwmcmp0.pwmcmp0 - pwms0) * (1 << regs->r_pwmcfg.pwmscale) * clk;
            next_trigger_time = nt < next_trigger_time ? nt : next_trigger_time;
        }
        for (size_t i = 0; i < 4; ++i) {
            // write gpio bits depending of gang bit
            if (regs->r_pwmcfg & (1 < (24 + i)))
                write_cmpgpio(i, pwmcmp_new_ip[i] && !pwmcmp_new_ip[(i + 1) % 4]);
            else
                write_cmpgpio(i, pwmcmp_new_ip[i]);
            // detect rising edge and set ip bit if found
            if (!pwmcmp_ip[i] && pwmcmp_new_ip[i]) regs->r_pwmcfg |= 1 << (28 + i);
            pwmcmp_ip[i] = pwmcmp_new_ip[i];
        }
        last_enable = true;
        update_counter_evt.notify(next_trigger_time);
    } else
        last_enable = false;
    cmpip_o[0].write(regs->r_pwmcfg.pwmcmp0ip != 0);
    cmpip_o[1].write(regs->r_pwmcfg.pwmcmp1ip != 0);
    cmpip_o[2].write(regs->r_pwmcfg.pwmcmp2ip != 0);
    cmpip_o[3].write(regs->r_pwmcfg.pwmcmp3ip != 0);
    last_cnt_update = now;
    last_clk = clk;
}

void pwm::write_cmpgpio(size_t index, bool val) {
    if (cmpgpio_o[index].get_interface()) {
        tlm::tlm_phase phase(tlm::BEGIN_REQ);
        tlm::tlm_signal_gp<> gp;
        sc_core::sc_time delay(SC_ZERO_TIME);
        gp.set_value(val);
        cmpgpio_o[index]->nb_transport_fw(gp, phase, delay);
    }
}
