// Copyright 2016 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package sysfs

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"unsafe"

	"periph.io/x/conn/v3/driver/driverreg"
	"periph.io/x/conn/v3/gpio"
	"periph.io/x/conn/v3/gpio/gpioreg"
	"periph.io/x/conn/v3/i2c"
	"periph.io/x/conn/v3/i2c/i2creg"
	"periph.io/x/conn/v3/physic"
)

// I2CSetSpeedHook can be set by a driver to enable changing the I²C buses
// speed.
func I2CSetSpeedHook(h func(f physic.Frequency) error) error {
	if h == nil {
		return errors.New("sysfs-i2c: hook must not be nil")
	}
	drvI2C.mu.Lock()
	defer drvI2C.mu.Unlock()
	if drvI2C.setSpeed != nil {
		return errors.New("sysfs-i2c: a speed hook was already set")
	}
	drvI2C.setSpeed = h
	return nil
}

// NewI2C opens an I²C bus via its sysfs interface as described at
// https://www.kernel.org/doc/Documentation/i2c/dev-interface.
//
// busNumber is the bus number as exported by sysfs. For example if the path is
// /dev/i2c-1, busNumber should be 1.
//
// The resulting object is safe for concurent use.
//
// Do not use sysfs.NewI2C() directly as the package sysfs is providing a
// https://periph.io/x/conn/v3/i2c Linux-specific implementation.
//
// periph.io works on many OSes!
//
// Instead, use https://periph.io/x/conn/v3/i2c/i2creg#Open. This permits
// it to work on all operating systems, or devices like I²C over USB.
func NewI2C(busNumber int) (*I2C, error) {
	if isLinux {
		return newI2C(busNumber)
	}
	return nil, errors.New("sysfs-i2c: is not supported on this platform")
}

// I2C is an open I²C bus via sysfs.
//
// It can be used to communicate with multiple devices from multiple goroutines.
type I2C struct {
	f         ioctlCloser
	busNumber int

	mu  sync.Mutex // In theory the kernel probably has an internal lock but not taking any chance.
	fn  functionality
	scl gpio.PinIO
	sda gpio.PinIO
}

// Close closes the handle to the I²C driver. It is not a requirement to close
// before process termination.
func (i *I2C) Close() error {
	i.mu.Lock()
	defer i.mu.Unlock()
	if err := i.f.Close(); err != nil {
		return fmt.Errorf("sysfs-i2c: %v", err)
	}
	return nil
}

func (i *I2C) String() string {
	return fmt.Sprintf("I2C%d", i.busNumber)
}

// Tx execute a transaction as a single operation unit.
func (i *I2C) Tx(addr uint16, w, r []byte) error {
	if addr >= 0x400 || (addr >= 0x80 && i.fn&func10BitAddr == 0) {
		return errors.New("sysfs-i2c: invalid address")
	}
	if len(w) == 0 && len(r) == 0 {
		return nil
	}

	// Convert the messages to the internal format.
	var buf [2]i2cMsg
	msgs := buf[0:0]
	if len(w) != 0 {
		msgs = buf[:1]
		buf[0].addr = addr
		buf[0].length = uint16(len(w))
		buf[0].buf = uintptr(unsafe.Pointer(&w[0]))
	}
	if len(r) != 0 {
		l := len(msgs)
		msgs = msgs[:l+1] // extend the slice by one
		buf[l].addr = addr
		buf[l].flags = flagRD
		buf[l].length = uint16(len(r))
		buf[l].buf = uintptr(unsafe.Pointer(&r[0]))
	}
	p := rdwrIoctlData{
		msgs:  uintptr(unsafe.Pointer(&msgs[0])),
		nmsgs: uint32(len(msgs)),
	}
	pp := uintptr(unsafe.Pointer(&p))
	i.mu.Lock()
	defer i.mu.Unlock()
	if err := i.f.Ioctl(ioctlRdwr, pp); err != nil {
		return fmt.Errorf("sysfs-i2c: %v", err)
	}
	return nil
}

// SetSpeed implements i2c.Bus.
func (i *I2C) SetSpeed(f physic.Frequency) error {
	if f > 100*physic.MegaHertz {
		return fmt.Errorf("sysfs-i2c: invalid speed %s; maximum supported clock is 100MHz", f)
	}
	if f < physic.KiloHertz {
		return fmt.Errorf("sysfs-i2c: invalid speed %s; minimum supported clock is 1KHz; did you forget to multiply by physic.KiloHertz?", f)
	}
	drvI2C.mu.Lock()
	defer drvI2C.mu.Unlock()
	if drvI2C.setSpeed != nil {
		return drvI2C.setSpeed(f)
	}
	return errors.New("sysfs-i2c: not supported")
}

// SCL implements i2c.Pins.
func (i *I2C) SCL() gpio.PinIO {
	i.initPins()
	return i.scl
}

// SDA implements i2c.Pins.
func (i *I2C) SDA() gpio.PinIO {
	i.initPins()
	return i.sda
}

// Private details.

func newI2C(busNumber int) (*I2C, error) {
	// Use the devfs path for now instead of sysfs path.
	f, err := ioctlOpen(fmt.Sprintf("/dev/i2c-%d", busNumber), os.O_RDWR)
	if err != nil {
		// Try to be helpful here. There are generally two cases:
		// - /dev/i2c-X doesn't exist. In this case, /boot/config.txt has to be
		//   edited to enable I²C then the device must be rebooted.
		// - permission denied. In this case, the user has to be added to plugdev.
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("sysfs-i2c: bus #%d is not configured: %v", busNumber, err)
		}
		// TODO(maruel): This is a debianism.
		return nil, fmt.Errorf("sysfs-i2c: are you member of group 'plugdev'? %v", err)
	}
	i := &I2C{f: f, busNumber: busNumber}

	// TODO(maruel): Changing the speed is currently doing this for all devices.
	// https://github.com/raspberrypi/linux/issues/215
	// Need to access /sys/module/i2c_bcm2708/parameters/baudrate

	// Query to know if 10 bits addresses are supported.
	if err = i.f.Ioctl(ioctlFuncs, uintptr(unsafe.Pointer(&i.fn))); err != nil {
		return nil, fmt.Errorf("sysfs-i2c: %v", err)
	}
	return i, nil
}

func (i *I2C) initPins() {
	i.mu.Lock()
	if i.scl == nil {
		if i.scl = gpioreg.ByName(fmt.Sprintf("I2C%d_SCL", i.busNumber)); i.scl == nil {
			i.scl = gpio.INVALID
		}
		if i.sda = gpioreg.ByName(fmt.Sprintf("I2C%d_SDA", i.busNumber)); i.sda == nil {
			i.sda = gpio.INVALID
		}
	}
	i.mu.Unlock()
}

// i2cdev driver IOCTL control codes.
//
// Constants and structure definition can be found at
// /usr/include/linux/i2c-dev.h and /usr/include/linux/i2c.h.
const (
	ioctlRetries = 0x701 // TODO(maruel): Expose this
	ioctlTimeout = 0x702 // TODO(maruel): Expose this; in units of 10ms
	ioctlSlave   = 0x703
	ioctlTenBits = 0x704 // TODO(maruel): Expose this but the header says it's broken (!?)
	ioctlFuncs   = 0x705
	ioctlRdwr    = 0x707
)

// flags
const (
	flagTEN        = 0x0010 // this is a ten bit chip address
	flagRD         = 0x0001 // read data, from slave to master
	flagSTOP       = 0x8000 // if funcProtocolMangling
	flagNOSTART    = 0x4000 // if I2C_FUNC_NOSTART
	flagRevDirAddr = 0x2000 // if funcProtocolMangling
	flagIgnoreNAK  = 0x1000 // if funcProtocolMangling
	flagNoRDACK    = 0x0800 // if funcProtocolMangling
	flagRecvLen    = 0x0400 // length will be first received byte

)

type functionality uint64

const (
	funcI2C                 = 0x00000001
	func10BitAddr           = 0x00000002
	funcProtocolMangling    = 0x00000004 // I2C_M_IGNORE_NAK etc.
	funcSMBusPEC            = 0x00000008
	funcNOSTART             = 0x00000010 // I2C_M_NOSTART
	funcSMBusBlockProcCall  = 0x00008000 // SMBus 2.0
	funcSMBusQuick          = 0x00010000
	funcSMBusReadByte       = 0x00020000
	funcSMBusWriteByte      = 0x00040000
	funcSMBusReadByteData   = 0x00080000
	funcSMBusWriteByteData  = 0x00100000
	funcSMBusReadWordData   = 0x00200000
	funcSMBusWriteWordData  = 0x00400000
	funcSMBusProcCall       = 0x00800000
	funcSMBusReadBlockData  = 0x01000000
	funcSMBusWriteBlockData = 0x02000000
	funcSMBusReadI2CBlock   = 0x04000000 // I2C-like block xfer
	funcSMBusWriteI2CBlock  = 0x08000000 // w/ 1-byte reg. addr.
)

func (f functionality) String() string {
	var out []string
	if f&funcI2C != 0 {
		out = append(out, "I2C")
	}
	if f&func10BitAddr != 0 {
		out = append(out, "10BIT_ADDR")
	}
	if f&funcProtocolMangling != 0 {
		out = append(out, "PROTOCOL_MANGLING")
	}
	if f&funcSMBusPEC != 0 {
		out = append(out, "SMBUS_PEC")
	}
	if f&funcNOSTART != 0 {
		out = append(out, "NOSTART")
	}
	if f&funcSMBusBlockProcCall != 0 {
		out = append(out, "SMBUS_BLOCK_PROC_CALL")
	}
	if f&funcSMBusQuick != 0 {
		out = append(out, "SMBUS_QUICK")
	}
	if f&funcSMBusReadByte != 0 {
		out = append(out, "SMBUS_READ_BYTE")
	}
	if f&funcSMBusWriteByte != 0 {
		out = append(out, "SMBUS_WRITE_BYTE")
	}
	if f&funcSMBusReadByteData != 0 {
		out = append(out, "SMBUS_READ_BYTE_DATA")
	}
	if f&funcSMBusWriteByteData != 0 {
		out = append(out, "SMBUS_WRITE_BYTE_DATA")
	}
	if f&funcSMBusReadWordData != 0 {
		out = append(out, "SMBUS_READ_WORD_DATA")
	}
	if f&funcSMBusWriteWordData != 0 {
		out = append(out, "SMBUS_WRITE_WORD_DATA")
	}
	if f&funcSMBusProcCall != 0 {
		out = append(out, "SMBUS_PROC_CALL")
	}
	if f&funcSMBusReadBlockData != 0 {
		out = append(out, "SMBUS_READ_BLOCK_DATA")
	}
	if f&funcSMBusWriteBlockData != 0 {
		out = append(out, "SMBUS_WRITE_BLOCK_DATA")
	}
	if f&funcSMBusReadI2CBlock != 0 {
		out = append(out, "SMBUS_READ_I2C_BLOCK")
	}
	if f&funcSMBusWriteI2CBlock != 0 {
		out = append(out, "SMBUS_WRITE_I2C_BLOCK")
	}
	return strings.Join(out, "|")
}

type rdwrIoctlData struct {
	msgs  uintptr // Pointer to i2cMsg
	nmsgs uint32
}

type i2cMsg struct {
	addr   uint16 // Address to communicate with
	flags  uint16 // 1 for read, see i2c.h for more details
	length uint16
	buf    uintptr
}

//

// driverI2C implements periph.Driver.
type driverI2C struct {
	mu       sync.Mutex
	buses    []string
	setSpeed func(f physic.Frequency) error
}

func (d *driverI2C) String() string {
	return "sysfs-i2c"
}

func (d *driverI2C) Prerequisites() []string {
	return nil
}

func (d *driverI2C) After() []string {
	return nil
}

func (d *driverI2C) Init() (bool, error) {
	// Do not use "/sys/bus/i2c/devices/i2c-" as Raspbian's provided udev rules
	// only modify the ACL of /dev/i2c-* but not the ones in /sys/bus/...
	prefix := "/dev/i2c-"
	items, err := filepath.Glob(prefix + "*")
	if err != nil {
		return true, err
	}
	if len(items) == 0 {
		return false, errors.New("no I²C bus found")
	}
	// Make sure they are registered in order.
	sort.Strings(items)
	for _, item := range items {
		bus, err := strconv.Atoi(item[len(prefix):])
		if err != nil {
			continue
		}
		name := fmt.Sprintf("/dev/i2c-%d", bus)
		d.buses = append(d.buses, name)
		aliases := []string{fmt.Sprintf("I2C%d", bus)}
		if err := i2creg.Register(name, aliases, bus, openerI2C(bus).Open); err != nil {
			return true, err
		}
	}
	return true, nil
}

type openerI2C int

func (o openerI2C) Open() (i2c.BusCloser, error) {
	b, err := NewI2C(int(o))
	if err != nil {
		return nil, err
	}
	return b, nil
}

func init() {
	if isLinux {
		driverreg.MustRegister(&drvI2C)
	}
}

var drvI2C driverI2C

var _ i2c.Bus = &I2C{}
var _ i2c.BusCloser = &I2C{}
