#include "gpio.hpp"
#include "dma.hpp"
#include <fstream>
#include <iostream>
#include <syslog.h>

using namespace std;
using namespace wpp;

/********************************
 *                              *
 * Internal singleton class     *
 * to ensure proper teardown    *
 * of dma channel               *
 *                              *
 ********************************/


class DmaPwmGenerator
{
private:
    DmaPwmGenerator()
    {
        int ch = (DmaHardware::GetInstance().host_is_model_pi4)?7:14;
        clog << LOG_DEBUG << endl << " -- Creating DmaPwmGenerator using channel " << ch << " --" << endl;

        DmaChannelConfig config{};
        config.chNum = ch;
        // cycle_time_us: typical value 10240=10.24ms each cycle
        config.cycleTimeUs = 10240; 
        // step_time_us: typical value 10=10us each step 
        //               (total steps = 10240/5=2048 steps)
        config.stepTimeUs = 5;
        // We will only use DELAY_VIA_PWM, since PCM is in use by the audio chip
        config.delay_hw = DelayHardware::DELAY_VIA_PWM;
        // invert: don't invert high/low output
        config.invert = false;

        auto channel = DmaChannel::CreateInstance(config);
        this->channel = channel;

    }

    std::shared_ptr<DmaChannel> channel;
    std::unordered_map<int, std::shared_ptr<PwmPin>> pins;

public:
    // delete copy constructor and assignmen operator to prevent accidentally creating instance doubles
    DmaPwmGenerator(DmaPwmGenerator const&) = delete;
    void operator=(DmaPwmGenerator const&)  = delete;


    static DmaPwmGenerator& Instance()
    {
        static DmaPwmGenerator instance;
        return instance;
    };

    ~DmaPwmGenerator()
    {
        this->channel->DeactivateChannel();
        clog << LOG_DEBUG << " -- DmaPwmGenerator destroyed -- " << endl;
    }
 
    // initialize pin and set default puse width in steps
    bool RegisterPin(uint8_t gpio, uint32_t width = 0)
    {
        if (this->pins.find(gpio) != this->pins.end()) {
            return false;
        }
        DmaPwmPinConfig config{};
        config.gpioPinNum = gpio;
        config.widthInSteps = width;
        auto pin = channel->CreatePin(config);
        pins[gpio] = pin;
        return true;
    }

    // unregister pin from pwm 
    bool UnregisterPin(int gpio) {
        auto pin = this->pins.find(gpio);
        if (pin == this->pins.end()) {
            // pin don't exist?
            return false;
        }
        pin->second->DeactivatePin();
        pins.erase(pin);
        return true;
    }    

    // Change pulse width in steps
    bool SetWidth(uint8_t gpio, uint32_t width)
    {
        auto pin = pins.find(gpio);
        if (pin == pins.end()) {
            // pin don't exist?
            return false;
        }
        pin->second->SetByWidth(width);
        return true;
    }
    
};

/************************
 *                      *
 * GpioBase functions   *
 *                      *
 ************************/


GpioBase::GpioBase()
{
    this->gpioChip.open("gpiochip0",gpiod::chip::OPEN_BY_NAME);
}

GpioBase::~GpioBase()
{

}

gpiod::line_request GpioBase::init_linerq(GpioDirection dir, GpioEdge edge, GpioPullup pu, GpioOutput op, bool active_low)
{
    // find process name from /proc file system
    std::ifstream comm("/proc/self/comm");
    std::string name;
    getline(comm, name);

    // setup line request with process name as consumer name
    gpiod::line_request rq;
    rq.consumer = name;

    // determine the request type from requested Direction and Edge
    rq.request_type = gpiod::line_request::DIRECTION_AS_IS;
    if(dir == GpioDirection::Output || dir == GpioDirection::PwmOutput) {
        rq.request_type = gpiod::line_request::DIRECTION_OUTPUT;
    }
    else if(edge == GpioEdge::Both) {
        rq.request_type = gpiod::line_request::EVENT_BOTH_EDGES;
    }
    else if (edge == GpioEdge::Rising) {
        rq.request_type = gpiod::line_request::EVENT_RISING_EDGE;
    }
    else if (edge == GpioEdge::Falling) {
        rq.request_type = gpiod::line_request::EVENT_FALLING_EDGE;
    }
    else //if (edge == GpioEdge::None) 
    {
        rq.request_type = gpiod::line_request::DIRECTION_INPUT;
    }

    rq.flags = 0;
    // Determine flags from the other 
    if(dir == GpioDirection::Output)
    {
        if(op == GpioOutput::OpenDrain){
            rq.flags |= ::gpiod::line_request::FLAG_OPEN_DRAIN;
        } 
        else if(op == GpioOutput::OpenSource){
            rq.flags |= ::gpiod::line_request::FLAG_OPEN_SOURCE;
        }
    }
    else // if(dir = GpioDirection::Input) 
    {
        // next lines require libgpiod 1.6.2

        if(pu == GpioPullup::PullUp){
         //   rq.flags |= ::gpiod::line_request::FLAG_BIAS_PULL_UP;
        } 
        else if(pu == GpioPullup::PullDown){
         //   rq.flags |= ::gpiod::line_request::FLAG_BIAS_PULL_DOWN;
        }
    }

    if(active_low){
        rq.flags |= ::gpiod::line_request::FLAG_ACTIVE_LOW;
    } 

    return rq;
}

void GpioBase::PinMode(GpioDirection dir, GpioOutput op, bool active_low)
{
    this->PinMode(dir,GpioPullup::None,op, active_low);
}
void GpioBase::PinMode(GpioDirection dir, GpioPullup pu, bool active_low)
{
    this->PinMode(dir,pu,GpioOutput::Both, active_low);
}

void GpioBase::PinMode(GpioDirection dir, bool active_low)
{
    this->PinMode(dir,GpioPullup::None,GpioOutput::Both, active_low);
}

void GpioBase::Listen(GpioEdge edge, bool active_low)
{
    this->Listen(edge, GpioPullup::None, active_low);
}

/************************
 *                      *
 * GpioPin functions    *
 *                      *
 ************************/

GpioPin::GpioPin(uint8_t gpio)
{
    this->line = this->gpioChip.get_line(gpio);
}

GpioPin::~GpioPin()
{
    if(this->dir == GpioDirection::PwmOutput)
    {
        DmaPwmGenerator::Instance().UnregisterPin(this->line.offset());
    }
    this->line.release();
    
}

void GpioPin::DigitalWrite(bool value)
{
    if(this->line.is_requested() && this->line.direction() == gpiod::line::DIRECTION_OUTPUT){
        this->line.set_value(value?1:0);
    }
}

bool GpioPin::DigitalRead()
{
    if(this->line.is_requested()){
        return (bool)(this->line.get_value());
    }
    else
    {
        return false; // fallback
    }
}

bool GpioPin::IsAvailable(){
    return !(this->line.is_used() && !this->line.is_requested());
}

void GpioPin::PinMode(GpioDirection dir, GpioPullup pu, GpioOutput op, bool active_low)
{
    if(this->IsAvailable())
    {
        this->line.release();
        gpiod::line_request cfg = this->init_linerq(dir,GpioEdge::None,pu,op, active_low);
        this->line.request(cfg);
        this->dir = dir; // store directionality

        if(dir == GpioDirection::PwmOutput)
        {
            DmaPwmGenerator::Instance().RegisterPin(this->line.offset());
        }
        else
        {
            DmaPwmGenerator::Instance().UnregisterPin(this->line.offset());
        }
    }
}

void GpioPin::PwmWrite(uint32_t width)
{
    // only do this if the pin was set to PwmOutput
    if(this->dir == GpioDirection::PwmOutput)
    {
        DmaPwmGenerator::Instance().SetWidth(this->line.offset(),width);
    }
}

void GpioPin::Listen(GpioEdge edge, GpioPullup pu, bool active_low)
{
    if(this->IsAvailable())
    {
        this->line.release();
        gpiod::line_request cfg = this->init_linerq(GpioDirection::Input,edge,pu,GpioOutput::Both, active_low);
        this->line.request(cfg);
        
        this->ThreadStart();

    }
}

void GpioPin::ThreadLoop()
{
    // wait 20 ms for an event
    // if it happens, process, otherwise wait another 20 ms
    bool event = this->line.event_wait(std::chrono::milliseconds(20));
    if(event)
    {
        gpiod::line_event evt = this->line.event_read();
        if(evt.event_type == gpiod::line_event::RISING_EDGE)
        {
            this->onChange(evt.source.offset(),GpioEdge::Rising,true);
        }
        else //if(evt.event_type == gpiod::line_event::FALLING_EDGE)
        {
            this->onChange(evt.source.offset(),GpioEdge::Falling,false);
        }
    }
}

/************************
 *                      *
 * GpioPins functions   *
 *                      *
 ************************/

GpioPins::GpioPins(std::vector<uint8_t> gpios)
{
    std::vector<unsigned int> pins;
    for (std::vector<uint8_t>::iterator it = gpios.begin() ; it != gpios.end(); ++it)
    {
        pins.push_back((unsigned int)(*it));
    }

    this->lines = this->gpioChip.get_lines(pins);
}

GpioPins::~GpioPins()
{
    if(this->dir == GpioDirection::PwmOutput)
    {
        for(auto& line : this->lines)
        {
            DmaPwmGenerator::Instance().UnregisterPin(line.offset());
        }
    }

    this->lines.release();
    
}

void GpioPins::DigitalWrite(std::vector<bool> values)
{
    std::vector<int> vs;
    for (std::vector<bool>::iterator it = values.begin() ; it != values.end(); ++it)
    {
        vs.push_back((*it)?1:0);
    }
    
    this->lines.set_values(vs);
}

std::vector<bool> GpioPins::DigitalRead()
{
    std::vector<int> vs = this->lines.get_values();
    std::vector<bool> values;
    for (std::vector<int>::iterator it = vs.begin() ; it != vs.end(); ++it)
    {
        values.push_back((*it)?true:false);
    }

    return values;

}

void GpioPins::PwmWrite(std::vector<uint32_t> widths)
{
    int index = 0;
    for (uint32_t width: widths)
    {
        // 
        if(index < this->lines.size())
        {
            auto& line = this->lines.get(index);
            DmaPwmGenerator::Instance().SetWidth(line.offset(),width);
        }
        index++;
    }
}


void GpioPins::PinMode(GpioDirection dir, GpioPullup pu, GpioOutput op, bool active_low)
{
    this->lines.release();
    gpiod::line_request cfg = this->init_linerq(dir,GpioEdge::None,pu,op, active_low);
    this->lines.request(cfg);
    this->dir = dir; // store directionality

    if(dir == GpioDirection::PwmOutput)
    {
        for(auto& line : this->lines)
        {
            DmaPwmGenerator::Instance().RegisterPin(line.offset(),0);
        }
    }
    else
    {
        for(auto& line : this->lines)
        {
            DmaPwmGenerator::Instance().UnregisterPin(line.offset());
        }
    }

}


void GpioPins::Listen(GpioEdge edge, GpioPullup pu, bool active_low)
{
    this->lines.release();
    gpiod::line_request cfg = this->init_linerq(GpioDirection::Input,edge,pu,GpioOutput::Both, active_low);
    this->lines.request(cfg);
    
    this->ThreadStart();
}

void GpioPins::ThreadLoop()
{
    // wait 20 ms for an event
    // if it happens, process, otherwise wait another 20 ms
    auto event_lines = this->lines.event_wait(std::chrono::milliseconds(20));
    if(event_lines)
    {
        for(auto& it: event_lines)
        {
            gpiod::line_event evt = it.event_read();
            if(evt.event_type == gpiod::line_event::RISING_EDGE)
            {
                this->onChange(evt.source.offset(),GpioEdge::Rising,true);
            }
            else //if(evt.event_type == gpiod::line_event::FALLING_EDGE)
            {
                this->onChange(evt.source.offset(),GpioEdge::Falling,false);
            }
        }
    }
}

/************************
 *                      *
 * RgbLeD functions     *
 *                      *
 ************************/

RgbLed::RgbLed(const uint8_t rPin, const uint8_t gPin, const uint8_t bPin) : GpioPins(std::vector<uint8_t>{rPin,gPin,bPin})
{
    this->PinMode(GpioDirection::PwmOutput);
}

uint32_t RgbLed::gamma(uint8_t value)
{
    const uint16_t gamma_lut[256] = {
     0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,
     1,   1,   1,   1,   2,   2,   2,   2,   3,   3,   3,   4,   4,   5,   5,   6,
     6,   7,   7,   8,   9,   9,  10,  11,  11,  12,  13,  14,  15,  16,  17,  18,
    19,  20,  21,  23,  24,  25,  27,  28,  29,  31,  32,  34,  36,  37,  39,  41,
    43,  45,  47,  49,  51,  53,  55,  57,  59,  62,  64,  67,  69,  72,  74,  77,
    80,  83,  85,  88,  91,  94,  98, 101, 104, 107, 111, 114, 118, 121, 125, 129,
   133, 137, 141, 145, 149, 153, 157, 162, 166, 171, 175, 180, 185, 189, 194, 199,
   204, 210, 215, 220, 226, 231, 237, 242, 248, 254, 260, 266, 272, 278, 284, 291,
   297, 304, 310, 317, 324, 331, 338, 345, 352, 359, 367, 374, 382, 390, 397, 405,
   413, 421, 430, 438, 446, 455, 463, 472, 481, 490, 499, 508, 517, 526, 536, 545,
   555, 565, 575, 585, 595, 605, 615, 626, 636, 647, 658, 669, 680, 691, 702, 713,
   725, 736, 748, 760, 772, 784, 796, 808, 821, 833, 846, 859, 872, 885, 898, 911,
   925, 938, 952, 966, 980, 994,1008,1022,1037,1051,1066,1081,1096,1111,1126,1142,
  1157,1173,1189,1204,1221,1237,1253,1270,1286,1303,1320,1337,1354,1371,1389,1406,
  1424,1442,1460,1478,1496,1515,1533,1552,1571,1590,1609,1629,1648,1668,1687,1707,
  1727,1748,1768,1789,1809,1830,1851,1872,1894,1915,1937,1958,1980,2002,2025,2047,
  };


    return (uint32_t) gamma_lut[value];
}

void RgbLed::SetColor(const uint8_t r, const uint8_t g, const uint8_t b)
{
    std::vector<uint32_t> values = {this->gamma(r),this->gamma(g),this->gamma(b)};
    this->PwmWrite(values);
}