using Fraunhofer.Fit.Iot.Lora.Events;
using System;
using System.Text;
using Unosquare.RaspberryIO;
using Unosquare.RaspberryIO.Gpio;

namespace Fraunhofer.Fit.Iot.Lora.lib
{
  public class LoraConnector {
    private Int64 _frequency = 0;
    private Byte _packetIndex = 0;
    private Boolean _implictHeaderMode = false;
    private GpioPin ssPin;
    private GpioPin dio0;
    private GpioPin RST;

    public delegate void DataUpdate(Object sender, DeviceUpdateEvent e);
    public event DataUpdate Update;

    enum Registers : Byte {
      FIFO                  = 0x00,
      OP_MODE               = 0x01,
      FRF_MSB               = 0x06,
      FRF_MID               = 0x07,
      FRF_LSB               = 0x08,
      PA_CONFIG             = 0x09,
      PRE_PA_RAMP           = 0x0A,
      LNA                   = 0x0C,
      FIFO_ADDR_PTR         = 0x0D,
      FIFO_TX_BASE_ADDR     = 0x0E,
      FIFO_RX_BASE_ADDR     = 0x0F,
      FIFO_RX_CURRENT_ADDR  = 0x10,
      IRQ_FLAGS             = 0x12,
      RX_NB_BYTES           = 0x13,
      PKT_SNR_VALUE         = 0x19,
      PKT_RSSI_VALUE        = 0x1A,
      RSSI_VALUE            = 0x1B,
      MODEM_CONFIG_1        = 0x1D,
      MODEM_CONFIG_2        = 0x1E,
      PREAMBLE_MSB          = 0x20,
      PREAMBLE_LSB          = 0x21,
      PAYLOAD_LENGTH        = 0x22,
      MODEM_CONFIG_3        = 0x26,
      FREQ_ERROR_MSB        = 0x28,
      FREQ_ERROR_MID        = 0x29,
      FREQ_ERROR_LSB        = 0x2A,
      RSSI_WIDEBAND         = 0x2C,
      DETECTION_OPTIMIZE    = 0x31,
      DEDECTION_THRESHOLD   = 0x37,
      SYNC_WORD             = 0x39,
      DIO_MAPPING_1         = 0x40,
      VERSION               = 0x42
    };

    enum Modes : Byte {
      SLEEP           = 0x00,
      STDBY           = 0x01,
      TX              = 0x03,
      RX_CONTINOUS    = 0x05,
      RX_SINGLE       = 0x06,
      LONG_RANGE_MODE = 0x80
    };

    enum Pa : Byte {
      BOOST = 0x80
    };

    enum Irq : Byte {
      TX_DONE_MASK = 0x08,
      PAYLOAD_CRC_ERROR_MASK = 0x20,
      RX_DONE_MASK = 0x40
    }

    public LoraConnector(Int64 freq, GpioPin ssPin, GpioPin dio0, GpioPin RST) {
      this.ssPin = ssPin;
      this.dio0 = dio0;
      this.RST = RST;
      this.SetupIO();
      this.Reset();
      Byte version = ReadRegister(Registers.VERSION);
      if(version != 0x12) {
        throw new Exception("Wrong Hardware!");
      }
      this.Sleep();
      this.SetFrequency(freq);
      //set base Addr
      this.WriteRegister(Registers.FIFO_TX_BASE_ADDR, 0);
      this.WriteRegister(Registers.FIFO_RX_BASE_ADDR, 0);
      //set LNA boost
      this.WriteRegister(Registers.LNA, (Byte)(ReadRegister(Registers.LNA) | 0x03));
      //set auto AGC
      this.WriteRegister(Registers.MODEM_CONFIG_3, 0x04);
      this.SetTxPower(17);
      this.Ilde();
    }
    
    #region Methods
    public void Sleep() {
      this.WriteRegister(Registers.OP_MODE, (Byte)Modes.LONG_RANGE_MODE | (Byte)Modes.SLEEP);
    }
    private void Ilde() {
      this.WriteRegister(Registers.OP_MODE, (Byte)Modes.LONG_RANGE_MODE | (Byte)Modes.STDBY);
    }
    public void SetFrequency(Int64 freq) {
      this._frequency = freq;

      UInt64 frf = ((UInt64)freq << 19) / 32000000;
      this.WriteRegister(Registers.FRF_MSB, (Byte)(frf >> 16));
      this.WriteRegister(Registers.FRF_MID, (Byte)(frf >> 8));
      this.WriteRegister(Registers.FRF_LSB, (Byte)(frf >> 0));
    }
    public void SetTxPower(Int32 level, Int32 outputPin = 1) {
      if (outputPin == 1) {
        if(level < 0) {
          level = 0;
        } else if(level > 14) {
          level = 14;
        }
        this.WriteRegister(Registers.PA_CONFIG, (Byte)(0x70 | level));
      } else {
        if(level < 2) {
          level = 2;
        } else if(level > 17) {
          level = 17;
        }
        this.WriteRegister(Registers.PA_CONFIG, (Byte)((Byte)Pa.BOOST | (level - 2)));
      }
    }
    public void EnableCrc() {
      this.WriteRegister(Registers.MODEM_CONFIG_2, (Byte)(this.ReadRegister(Registers.MODEM_CONFIG_2) | 0x04));
    }
    public void SetPrePaRamp() {
      this.WriteRegister(Registers.PRE_PA_RAMP, (Byte)((this.ReadRegister(Registers.PRE_PA_RAMP) & 0xF0) | 0x08));
    }
    public void Receive(Byte size) {
      if (size > 0) {
        this.ImplicitHeaderMode();
        this.WriteRegister(Registers.PAYLOAD_LENGTH, (Byte)(size & 0xff));
      } else {
        this.ExplicitHeaderMode();
      }
      this.WriteRegister(Registers.OP_MODE, (Byte)Modes.LONG_RANGE_MODE | (Byte)Modes.RX_CONTINOUS);
    }
    public void ExplicitHeaderMode() {
      this._implictHeaderMode = false;
      this.WriteRegister(Registers.MODEM_CONFIG_1, (Byte)(this.ReadRegister(Registers.MODEM_CONFIG_1) & 0xfe));
    }

    public void ImplicitHeaderMode() {
      this._implictHeaderMode = true;
      this.WriteRegister(Registers.MODEM_CONFIG_1, (Byte)(this.ReadRegister(Registers.MODEM_CONFIG_1) | 0x01));
    }

    private void OnDio0Rise() {
      Byte irqFlags = this.ReadRegister(Registers.IRQ_FLAGS);

      // clear IRQ's
      this.WriteRegister(Registers.IRQ_FLAGS, irqFlags);

      if ((irqFlags & (Byte)Irq.PAYLOAD_CRC_ERROR_MASK) == 0) {
        // received a packet
        this._packetIndex = 0;

        // read packet length
        Byte packetLength = this._implictHeaderMode ? this.ReadRegister(Registers.PAYLOAD_LENGTH) : this.ReadRegister(Registers.RX_NB_BYTES);

        // set FIFO address to current RX address
        this.WriteRegister(Registers.FIFO_ADDR_PTR, this.ReadRegister(Registers.FIFO_RX_CURRENT_ADDR));

        Byte[] ms = new Byte[packetLength];
        for(Byte i = 0; i < packetLength; i++) {
          Int16 c = this.Read();
          if(c != -1) {
            ms[i] = (Byte)c;
          } else {
            throw new Exception("Message to Short");
          }
        }
        Double snr = this.PacketSnr();
        Byte prssi = this.PacketRssi();
        Byte rssi = this.Rssi();
        this.Update?.Invoke(this, new DeviceUpdateEvent(packetLength, Encoding.ASCII.GetString(ms), snr, prssi, rssi));
        
        // reset FIFO address
        this.WriteRegister(Registers.FIFO_ADDR_PTR, 0);
      }
    }
    #endregion

    #region Communication
    private Byte ReadRegister(Byte address) {
      return this.SingleTransfer((Byte)(address & 0x7F), 0x00);
    }
    private Byte ReadRegister(Registers reg) {
      return ReadRegister((Byte)reg);
    }
    private void WriteRegister(Byte address, Byte value) {
      this.SingleTransfer((Byte)(address | 0x80), value);
    }
    private void WriteRegister(Registers reg, Byte value) {
      this.WriteRegister((Byte)reg, value);
    }
    public Int16 Read() {
      if (this.Available() == 0) {
        return -1;
      }
      this._packetIndex++;
      return this.ReadRegister(Registers.FIFO);
    }
    public Byte Available() {
      return (Byte)(this.ReadRegister(Registers.RX_NB_BYTES) - this._packetIndex);
    }
    public Double PacketSnr() {
      return ((SByte)this.ReadRegister(Registers.PKT_SNR_VALUE)) * 0.25;
    }
    public Byte PacketRssi() {
      return (Byte)(this.ReadRegister(Registers.PKT_RSSI_VALUE) - (this._frequency < 868E6 ? 164 : 157));
    }
    public Byte Rssi() {
      return (Byte)(this.ReadRegister(Registers.RSSI_VALUE) - (this._frequency < 868E6 ? 164 : 157));
    }
    #endregion

    #region Hardware IO
    private void Reset() {
      this.RST.Write(false);
      System.Threading.Thread.Sleep(100);
      this.RST.Write(true);
      System.Threading.Thread.Sleep(100);
    }

    private void SetupIO() {
      Pi.Spi.Channel0Frequency = SpiChannel.MinFrequency;
      this.ssPin.PinMode = GpioPinDriveMode.Output;
      this.dio0.PinMode = GpioPinDriveMode.Input;
      this.RST.PinMode = GpioPinDriveMode.Output;
    }

    private Byte SingleTransfer(Byte address, Byte value) {
      Selectreceiver();
      Byte[] spibuf = Pi.Spi.Channel0.SendReceive(new Byte[] { address, value });
      Unselectreceiver();
      return spibuf[1];
    }

    private void Selectreceiver() {
      this.ssPin.Write(false);
    }

    private void Unselectreceiver() {
      this.ssPin.Write(true);
    }

    public void OnReceive() {
      if (this.Update != null) {
        this.WriteRegister(Registers.DIO_MAPPING_1, 0x00);
        this.dio0.RegisterInterruptCallback(EdgeDetection.RisingEdge, this.OnDio0Rise);
      }
    }
    #endregion
  }
}