package board

import (
	"fmt"
	"log"
	"strings"
	"time"

	"git.openpdu.org/OpenPDU/openpdu/i2c"
	"git.openpdu.org/OpenPDU/openpdu/syslog"
	PahoMQTT "github.com/eclipse/paho.mqtt.golang"
	"github.com/spf13/viper"
	I2C "periph.io/x/periph/conn/i2c"
)

type I2CChannel struct {
	ID                       string
	Num                      uint
	name                     string
	mqttStateTopic           string
	mqttCommandTopic         string
	onboot                   string
	parent                   *I2CBoard
	onChannelUpdateFunctions map[string]onChannelUpdateFunction
}

type I2CBoard struct {
	ID           string
	Name         string
	ChannelCount uint
	channels     []*I2CChannel
	i2cDevice    *I2C.Dev
	inverted     bool
	i2cDataA     []byte
	i2cDataB     []byte
	lastUpdate   time.Time
}

func newI2CChannel(v *viper.Viper, channelID string) I2CChannel {
	v.SetDefault("name", "unknown")
	v.SetDefault("lastValue", false)
	v.SetDefault("onboot", "off")
	v.SetDefault("mqtt.statetopic", v.GetString("name"))

	return I2CChannel{
		ID:               channelID,
		Num:              v.GetUint("num"),
		name:             v.GetString("name"),
		mqttStateTopic:   v.GetString("mqtt.statetopic"),
		mqttCommandTopic: v.GetString("mqtt.commandtopic"),
		onboot:           v.GetString("onboot"),
	}
}

func newI2CBoard(v *viper.Viper, id string) *Board {
	var b *I2CBoard

	v.SetDefault("name", "board "+id)
	v.SetDefault("type", "i2c")
	v.SetDefault("channelCount", 0)
	v.SetDefault("channels", "")
	v.SetDefault("inverted", true)

	if v.GetInt("channelCount") > 0 {
		for i := 0; i < v.GetInt("channelCount"); i++ {
			v.SetDefault("channels."+fmt.Sprint(i)+".num", i)
		}
	}

	b = &I2CBoard{
		ID:           id,
		Name:         v.GetString("name"),
		ChannelCount: v.GetUint("channelCount"),
		inverted:     v.GetBool("inverted"),
	}

	channels := make([]*I2CChannel, v.GetInt("channelCount"))
	if v.GetInt("channelCount") > 0 {
		channelsConfig := v.Sub("channels")
		if channelsConfig != nil {
			for channelid1 := range channelsConfig.AllSettings() {
				channelid := strings.ToLower(channelid1)
				channelConfig := channelsConfig.Sub(channelid)
				c := newI2CChannel(channelConfig, channelid)
				c.parent = b
				if c.Num >= v.GetUint("channelCount") {
					continue
				}
				channels[c.Num] = &c
				AllChannels[c.ID] = &c
			}
		}
	}
	b.channels = channels
	syslog.Info("Created new I2C board")
	var b1 Board = b
	return &b1
}

func (c *I2CChannel) Toggle() (bool, error) {
	oldval := c.Status()
	c.setToggle()
	c.parent.WriteStatus()
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](oldval, c)
	}
	return c.Status(), nil
}

func (c *I2CChannel) setToggle() {
	var data *[]byte
	var mask byte

	if c.Num < 8 {
		data = &c.parent.i2cDataA
	} else {
		data = &c.parent.i2cDataB
	}
	mask = 1 << (c.Num % 8)
	(*data)[0] ^= mask
}

func (c *I2CChannel) On() error {
	oldval := c.Status()
	c.SetOn()
	c.parent.WriteStatus()
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](oldval, c)
	}
	return nil
}

func (c *I2CChannel) SetOn() {
	var data *[]byte
	var mask byte

	if c.Num < 8 {
		data = &c.parent.i2cDataA
	} else {
		data = &c.parent.i2cDataB
	}
	mask = 1 << (c.Num % 8)
	(*data)[0] |= mask
}

func (c *I2CChannel) Off() error {
	oldval := c.Status()
	c.SetOff()
	c.parent.WriteStatus()
	c.SaveLastState()
	for f := range c.onChannelUpdateFunctions {
		c.onChannelUpdateFunctions[f](oldval, c)
	}
	return nil
}

func (c *I2CChannel) SetOff() {
	var data *[]byte
	var mask byte

	if c.Num < 8 {
		data = &c.parent.i2cDataA
	} else {
		data = &c.parent.i2cDataB
	}
	mask = (1 << (c.Num % 8)) ^ 0xFF
	(*data)[0] &= mask
}

func (c *I2CChannel) ToString() string {
	if !c.Status() {
		return "off"
	}
	return "on"
}

func (c *I2CChannel) SaveLastState() {
	if c.onboot == "last" {
		s := fmt.Sprintf("boards.%s.channels.%s.lastvalue", c.parent.ID, c.ID)
		viper.Set(s, c.Status())
		viper.WriteConfig()
	}
}

func (c *I2CChannel) Status() bool {
	var data *[]byte
	c.parent.ReadStatus()
	if c.Num < 8 {
		data = &c.parent.i2cDataA
	} else {
		data = &c.parent.i2cDataB
	}
	value := ((*data)[0] >> (c.Num % 8) & 1) == 1
	return value
}

func (c *I2CChannel) Parent() Board {
	return c.parent
}

func (c *I2CChannel) Dump() {
	log.Printf(" Channel %d (on boot: %s): %s \n", c.Num, c.onboot, c.name)
}

func (b I2CBoard) Dump() {
	log.Printf("Board '%s' (id: %s): %d channels\n", b.Name, b.ID, b.ChannelCount)
	for c := range b.channels {
		b.channels[c].Dump()
	}
}

func (b *I2CBoard) ReadStatus() {
	now := time.Now()
	diff := now.Sub(b.lastUpdate)
	ns := diff.Nanoseconds()
	if ns < 10*1000000 { // 10ms
		// syslog.Debug(fmt.Sprintf("I2C update skipped because less than 10ms passed (%v)", ns))
		return
	}

	replyA := [1]byte{}
	replyB := [1]byte{}
	b.i2cDevice.Tx([]byte{0x12}, replyA[:])
	b.i2cDevice.Tx([]byte{0x13}, replyB[:])

	if b.inverted {
		replyA[0] ^= 0xFF
		replyB[0] ^= 0xFF
	}

	b.i2cDataA[0] = replyA[0]
	b.i2cDataB[0] = replyB[0]
	b.lastUpdate = time.Now()
}

func (b *I2CBoard) WriteStatus() {
	// var dA, dB, oA, oB []byte
	var dA, dB []byte

	dA = []byte{0x12, 0x00}
	dB = []byte{0x13, 0x00}

	dA[1] = b.i2cDataA[0]

	dB[1] = b.i2cDataB[0]
	if b.inverted {
		dA[1] ^= 0xff
		dB[1] ^= 0xff
	}

	if _, err := b.i2cDevice.Write(dA); err != nil {
		syslog.Err(err.Error())
	}
	if _, err := b.i2cDevice.Write(dB); err != nil {
		syslog.Err(err.Error())
	}

	b.lastUpdate = time.Now()
}

func (b *I2CBoard) Initialize() {
	syslog.Info("Initializing I2C board")

	for {
		if i2c.I2Cbus == nil {
			syslog.Warning("i2cboard: i2cbus not found")
			time.Sleep(1 * time.Second)
			continue
		} else {
			break
		}
	}

	/*
	 * MCP23017 - can work in 8bit mode or 16bit mode, you have to set IOCON.BANK=1 (8bit) or =0 (16bit default)
	 *  16 bit mode:
	 * 0x00  IO direction bank A (1=input 0=output)
	 * 0x01  IO direction bank B (1=input 0=output)
	 * 0x12  GPIO A
	 * 0x13  GPIO B
	 *
	 *   8 bit mode
	 * 0x00  IO direction (1=input 0=output)
	 * 0x09  GPIO
	 */

	mydevice := &I2C.Dev{Addr: 0x27, Bus: i2c.I2Cbus}

	// set all output
	write := []byte{0x0, 0x0}
	if _, err := mydevice.Write(write); err != nil {
		syslog.Err(err.Error())
	}
	write = []byte{0x01, 0x0}
	if _, err := mydevice.Write(write); err != nil {
		syslog.Err(err.Error())
	}

	b.i2cDevice = mydevice
	b.i2cDataA = make([]byte, 1)
	b.i2cDataB = make([]byte, 1)
	syslog.Info("I2C board: reading initial status")
	b.ReadStatus()

	for i := range b.channels {
		c := b.channels[i]
		switch c.onboot {
		case "on":
			c.SetOn()
		case "off":
			c.SetOff()
		case "last":
			s := fmt.Sprintf("boards.%s.channels.%s.lastvalue", c.parent.ID, c.ID)

			switch viper.GetBool(s) {
			case true:
				c.SetOn()
			case false:
				c.SetOff()
			}
		}
		syslog.Info(fmt.Sprintf("Channel %d status: %v", i, c.Status()))
	}

	b.WriteStatus()
}

func (b *I2CBoard) Channel(num uint64) Channel {
	return b.channels[num]
}

func (c *I2CChannel) Name() string {
	return c.name
}

func (c *I2CChannel) OnBoot() string {
	return c.onboot
}

func (c *I2CChannel) SetOnBoot(str string) {
	c.onboot = str
	s := fmt.Sprintf("boards.%s.channels.%s.onboot", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *I2CChannel) SetMQTTStateTopic(str string) {
	c.mqttStateTopic = str
	s := fmt.Sprintf("boards.%s.channels.%s.mqtt.statetopic", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *I2CChannel) SetMQTTCommandTopic(str string) {
	c.mqttCommandTopic = str
	s := fmt.Sprintf("boards.%s.channels.%s.mqtt.commandtopic", c.parent.ID, c.ID)
	viper.Set(s, str)
}

func (c *I2CChannel) MQTTHandler(client PahoMQTT.Client, msg PahoMQTT.Message) {
	switch string(msg.Payload()) {
	case "on":
		if !c.Status() {
			c.On()
		}
	case "off":
		if c.Status() {
			c.Off()
		}
	}
}

func (c *I2CChannel) MQTTStateTopic() string {
	return c.mqttStateTopic
}

func (c *I2CChannel) MQTTCommandTopic() string {
	return c.mqttCommandTopic
}

func init() {
	RegisterBoardType("i2c", newI2CBoard)
}

func (c *I2CChannel) AddOnChannelUpdateFunction(name string, f onChannelUpdateFunction) {
	if c.onChannelUpdateFunctions == nil {
		c.onChannelUpdateFunctions = make(map[string]onChannelUpdateFunction)
	}
	c.onChannelUpdateFunctions[name] = f
}
