// 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"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"unsafe"

	"periph.io/x/conn/v3"
	"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/physic"
	"periph.io/x/conn/v3/spi"
	"periph.io/x/conn/v3/spi/spireg"
	"periph.io/x/host/v3/fs"
)

// NewSPI opens a SPI port via its devfs interface as described at
// https://www.kernel.org/doc/Documentation/spi/spidev and
// https://www.kernel.org/doc/Documentation/spi/spi-summary
//
// The resulting object is safe for concurrent use.
//
// busNumber is the bus number as exported by devfs. For example if the path is
// /dev/spidev0.1, busNumber should be 0 and chipSelect should be 1.
//
// It is recommended to use https://periph.io/x/conn/v3/spi/spireg#Open
// instead of using NewSPI() directly as the package sysfs is providing a
// Linux-specific implementation. periph.io works on many OSes! This permits
// it to work on all operating systems, or devices like SPI over USB.
func NewSPI(busNumber, chipSelect int) (*SPI, error) {
	if isLinux {
		return newSPI(busNumber, chipSelect)
	}
	return nil, errors.New("sysfs-spi: not implemented on non-linux OSes")
}

// SPI is an open SPI port.
type SPI struct {
	conn spiConn
}

// Close closes the handle to the SPI driver. It is not a requirement to close
// before process termination.
//
// Note that the object is not reusable afterward.
func (s *SPI) Close() error {
	s.conn.mu.Lock()
	defer s.conn.mu.Unlock()
	if err := s.conn.f.Close(); err != nil {
		return fmt.Errorf("sysfs-spi: %v", err)
	}
	s.conn.f = nil
	return nil
}

func (s *SPI) String() string {
	return s.conn.String()
}

// LimitSpeed implements spi.ConnCloser.
func (s *SPI) LimitSpeed(f physic.Frequency) error {
	if f > physic.GigaHertz {
		return fmt.Errorf("sysfs-spi: invalid speed %s; maximum supported clock is 1GHz", f)
	}
	if f < 100*physic.Hertz {
		return fmt.Errorf("sysfs-spi: invalid speed %s; minimum supported clock is 100Hz; did you forget to multiply by physic.MegaHertz?", f)
	}
	s.conn.mu.Lock()
	defer s.conn.mu.Unlock()
	s.conn.freqPort = f
	return nil
}

// Connect implements spi.Port.
//
// It must be called before any I/O.
func (s *SPI) Connect(f physic.Frequency, mode spi.Mode, bits int) (spi.Conn, error) {
	if f > physic.GigaHertz {
		return nil, fmt.Errorf("sysfs-spi: invalid speed %s; maximum supported clock is 1GHz", f)
	}
	if f < 100*physic.Hertz {
		return nil, fmt.Errorf("sysfs-spi: invalid speed %s; minimum supported clock is 100Hz; did you forget to multiply by physic.MegaHertz?", f)
	}
	if mode&^(spi.Mode3|spi.HalfDuplex|spi.NoCS|spi.LSBFirst) != 0 {
		return nil, fmt.Errorf("sysfs-spi: invalid mode %v", mode)
	}
	if bits < 1 || bits >= 256 {
		return nil, fmt.Errorf("sysfs-spi: invalid bits %d", bits)
	}
	s.conn.mu.Lock()
	defer s.conn.mu.Unlock()
	if s.conn.connected {
		return nil, errors.New("sysfs-spi: Connect() can only be called exactly once")
	}
	s.conn.connected = true
	s.conn.freqConn = f
	s.conn.bitsPerWord = uint8(bits)
	// Only mode needs to be set via an IOCTL, others can be specified in the
	// spiIOCTransfer packet, which saves a kernel call.
	m := mode & spi.Mode3
	s.conn.muPins.Lock()
	{
		if mode&spi.HalfDuplex != 0 {
			m |= threeWire
			s.conn.halfDuplex = true
			// In case initPins() had been called before Connect().
			s.conn.mosi = gpio.INVALID
		}
		if mode&spi.NoCS != 0 {
			m |= noCS
			s.conn.noCS = true
			// In case initPins() had been called before Connect().
			s.conn.cs = gpio.INVALID
		}
	}
	s.conn.muPins.Unlock()
	if mode&spi.LSBFirst != 0 {
		m |= lSBFirst
	}
	// Only the first 8 bits are used. This only works because the system is
	// running in little endian.
	if err := s.conn.setFlag(spiIOCMode, uint64(m)); err != nil {
		return nil, fmt.Errorf("sysfs-spi: setting mode %v failed: %v", mode, err)
	}
	return &s.conn, nil
}

// MaxTxSize implements conn.Limits
func (s *SPI) MaxTxSize() int {
	return drvSPI.bufSize
}

// CLK implements spi.Pins.
func (s *SPI) CLK() gpio.PinOut {
	return s.conn.CLK()
}

// MISO implements spi.Pins.
func (s *SPI) MISO() gpio.PinIn {
	return s.conn.MISO()
}

// MOSI implements spi.Pins.
func (s *SPI) MOSI() gpio.PinOut {
	return s.conn.MOSI()
}

// CS implements spi.Pins.
func (s *SPI) CS() gpio.PinOut {
	return s.conn.CS()
}

// Private details.

func newSPI(busNumber, chipSelect int) (*SPI, error) {
	if busNumber < 0 || busNumber >= 1<<16 {
		return nil, fmt.Errorf("sysfs-spi: invalid bus %d", busNumber)
	}
	if chipSelect < 0 || chipSelect > 255 {
		return nil, fmt.Errorf("sysfs-spi: invalid chip select %d", chipSelect)
	}
	// Use the devfs path for now.
	f, err := ioctlOpen(fmt.Sprintf("/dev/spidev%d.%d", busNumber, chipSelect), os.O_RDWR)
	if err != nil {
		return nil, fmt.Errorf("sysfs-spi: %v", err)
	}
	return &SPI{
		spiConn{
			name:       fmt.Sprintf("SPI%d.%d", busNumber, chipSelect),
			f:          f,
			busNumber:  busNumber,
			chipSelect: chipSelect,
		},
	}, nil
}

//

// spiConn implements spi.Conn.
type spiConn struct {
	// Immutable
	name       string
	f          ioctlCloser
	busNumber  int
	chipSelect int

	mu          sync.Mutex
	freqPort    physic.Frequency // Frequency specified at LimitSpeed()
	freqConn    physic.Frequency // Frequency specified at Connect()
	bitsPerWord uint8
	connected   bool
	halfDuplex  bool
	noCS        bool
	// Heap optimization: reduce the amount of memory allocations during
	// transactions.
	io [4]spiIOCTransfer
	p  [2]spi.Packet

	// Use a separate lock for the pins, so that they can be queried while a
	// transaction is happening.
	muPins sync.Mutex
	clk    gpio.PinOut
	mosi   gpio.PinOut
	miso   gpio.PinIn
	cs     gpio.PinOut
}

func (s *spiConn) String() string {
	return s.name
}

// Read implements io.Reader.
func (s *spiConn) Read(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, errors.New("sysfs-spi: Read() with empty buffer")
	}
	if drvSPI.bufSize != 0 && len(b) > drvSPI.bufSize {
		return 0, fmt.Errorf("sysfs-spi: maximum Read length is %d, got %d bytes", drvSPI.bufSize, len(b))
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.p[0].W = nil
	s.p[0].R = b
	if err := s.txPackets(s.p[:1]); err != nil {
		return 0, fmt.Errorf("sysfs-spi: Read() failed: %v", err)
	}
	return len(b), nil
}

// Write implements io.Writer.
func (s *spiConn) Write(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, errors.New("sysfs-spi: Write() with empty buffer")
	}
	if drvSPI.bufSize != 0 && len(b) > drvSPI.bufSize {
		return 0, fmt.Errorf("sysfs-spi: maximum Write length is %d, got %d bytes", drvSPI.bufSize, len(b))
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.p[0].W = b
	s.p[0].R = nil
	if err := s.txPackets(s.p[:1]); err != nil {
		return 0, fmt.Errorf("sysfs-spi: Write() failed: %v", err)
	}
	return len(b), nil
}

// Tx sends and receives data simultaneously.
//
// It is OK if both w and r point to the same underlying byte slice.
//
// spidev enforces the maximum limit of transaction size. It can be as low as
// 4096 bytes. See the platform documentation to learn how to increase the
// limit.
func (s *spiConn) Tx(w, r []byte) error {
	l := len(w)
	if l == 0 {
		if l = len(r); l == 0 {
			return errors.New("sysfs-spi: Tx() with empty buffers")
		}
	} else {
		// It's not a big deal to read halfDuplex without the lock.
		if !s.halfDuplex && len(r) != 0 && len(r) != len(w) {
			return fmt.Errorf("sysfs-spi: Tx(): when both w and r are used, they must be the same size; got %d and %d bytes", len(w), len(r))
		}
	}
	if drvSPI.bufSize != 0 && l > drvSPI.bufSize {
		return fmt.Errorf("sysfs-spi: maximum Tx length is %d, got %d bytes", drvSPI.bufSize, l)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.p[0].W = w
	s.p[0].R = r
	p := s.p[:1]
	if s.halfDuplex && len(w) != 0 && len(r) != 0 {
		// Create two packets for HalfDuplex operation: one write then one read.
		s.p[0].R = nil
		s.p[0].KeepCS = true
		s.p[1].W = nil
		s.p[1].R = r
		s.p[1].KeepCS = false
		p = s.p[:2]
	} else {
		s.p[0].KeepCS = false
	}
	if err := s.txPackets(p); err != nil {
		return fmt.Errorf("sysfs-spi: Tx() failed: %v", err)
	}
	return nil
}

// TxPackets sends and receives packets as specified by the user.
//
// spidev enforces the maximum limit of transaction size. It can be as low as
// 4096 bytes. See the platform documentation to learn how to increase the
// limit.
func (s *spiConn) TxPackets(p []spi.Packet) error {
	total := 0
	for i := range p {
		lW := len(p[i].W)
		lR := len(p[i].R)
		if lW != lR && lW != 0 && lR != 0 {
			return fmt.Errorf("sysfs-spi: when both w and r are used, they must be the same size; got %d and %d bytes", lW, lR)
		}
		l := lW
		if l == 0 {
			l = lR
		}
		total += l
	}
	if total == 0 {
		return errors.New("sysfs-spi: empty packets")
	}
	if drvSPI.bufSize != 0 && total > drvSPI.bufSize {
		return fmt.Errorf("sysfs-spi: maximum TxPackets length is %d, got %d bytes", drvSPI.bufSize, total)
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	if s.halfDuplex {
		for i := range p {
			if len(p[i].W) != 0 && len(p[i].R) != 0 {
				return errors.New("sysfs-spi: can only specify one of w or r when in half duplex")
			}
		}
	}
	if err := s.txPackets(p); err != nil {
		return fmt.Errorf("sysfs-spi: TxPackets() failed: %v", err)
	}
	return nil
}

// Duplex implements conn.Conn.
func (s *spiConn) Duplex() conn.Duplex {
	if s.halfDuplex {
		return conn.Half
	}
	return conn.Full
}

// MaxTxSize implements conn.Limits.
func (s *spiConn) MaxTxSize() int {
	return drvSPI.bufSize
}

// CLK implements spi.Pins.
func (s *spiConn) CLK() gpio.PinOut {
	s.initPins()
	return s.clk
}

// MISO implements spi.Pins.
func (s *spiConn) MISO() gpio.PinIn {
	s.initPins()
	return s.miso
}

// MOSI implements spi.Pins.
func (s *spiConn) MOSI() gpio.PinOut {
	s.initPins()
	return s.mosi
}

// CS implements spi.Pins.
func (s *spiConn) CS() gpio.PinOut {
	s.initPins()
	return s.cs
}

//

func (s *spiConn) txPackets(p []spi.Packet) error {
	// Convert the packets.
	f := s.freqPort
	if s.freqConn != 0 && (s.freqPort == 0 || s.freqConn < s.freqPort) {
		f = s.freqConn
	}
	var m []spiIOCTransfer
	if len(p) > len(s.io) {
		m = make([]spiIOCTransfer, len(p))
	} else {
		m = s.io[:len(p)]
	}
	for i := range p {
		bits := p[i].BitsPerWord
		if bits == 0 {
			bits = s.bitsPerWord
		}
		csInvert := false
		if !s.noCS {
			// Invert CS behavior when a packet has KeepCS false, except for the last
			// packet when KeepCS is true.
			last := i == len(p)-1
			csInvert = p[i].KeepCS == last
		}
		m[i].reset(p[i].W, p[i].R, f, bits, csInvert)
	}
	return s.f.Ioctl(spiIOCTx(len(m)), uintptr(unsafe.Pointer(&m[0])))
}

func (s *spiConn) setFlag(op uint, arg uint64) error {
	return s.f.Ioctl(op, uintptr(unsafe.Pointer(&arg)))
}

// GetFlag allows to read back flags set via a ioctl, i.e. setFlag. It is
// exported to allow calling it from the smoke test.
func (s *spiConn) GetFlag(op uint) (arg uint64, err error) {
	err = s.f.Ioctl(op, uintptr(unsafe.Pointer(&arg)))
	return
}

func (s *spiConn) initPins() {
	s.muPins.Lock()
	defer s.muPins.Unlock()
	if s.clk != nil {
		return
	}
	if s.clk = gpioreg.ByName(fmt.Sprintf("SPI%d_CLK", s.busNumber)); s.clk == nil {
		s.clk = gpio.INVALID
	}
	if s.miso = gpioreg.ByName(fmt.Sprintf("SPI%d_MISO", s.busNumber)); s.miso == nil {
		s.miso = gpio.INVALID
	}
	// s.mosi is set to INVALID if HalfDuplex was specified.
	if s.mosi != gpio.INVALID {
		if s.mosi = gpioreg.ByName(fmt.Sprintf("SPI%d_MOSI", s.busNumber)); s.mosi == nil {
			s.mosi = gpio.INVALID
		}
	}
	// s.cs is set to INVALID if NoCS was specified.
	if s.cs != gpio.INVALID {
		if s.cs = gpioreg.ByName(fmt.Sprintf("SPI%d_CS%d", s.busNumber, s.chipSelect)); s.cs == nil {
			s.cs = gpio.INVALID
		}
	}
}

const (
	cSHigh    spi.Mode = 0x4  // CS active high instead of default low (not recommended)
	lSBFirst  spi.Mode = 0x8  // Use little endian encoding for each word
	threeWire spi.Mode = 0x10 // half-duplex; MOSI and MISO are shared
	loop      spi.Mode = 0x20 // loopback mode
	noCS      spi.Mode = 0x40 // do not assert CS
	ready     spi.Mode = 0x80 // slave pulls low to pause
	// The driver optionally support dual and quad data lines.
)

// spidev driver IOCTL control codes.
//
// Constants and structure definition can be found at
// /usr/include/linux/spi/spidev.h.
const spiIOCMagic uint = 'k'

var (
	spiIOCMode        = fs.IOW(spiIOCMagic, 1, 1) // SPI_IOC_WR_MODE (8 bits)
	spiIOLSBFirst     = fs.IOW(spiIOCMagic, 2, 1) // SPI_IOC_WR_LSB_FIRST
	spiIOCBitsPerWord = fs.IOW(spiIOCMagic, 3, 1) // SPI_IOC_WR_BITS_PER_WORD
	spiIOCMaxSpeedHz  = fs.IOW(spiIOCMagic, 4, 4) // SPI_IOC_WR_MAX_SPEED_HZ
	spiIOCMode32      = fs.IOW(spiIOCMagic, 5, 4) // SPI_IOC_WR_MODE32 (32 bits)
)

// spiIOCTx(l) calculates the equivalent of SPI_IOC_MESSAGE(l) to execute a
// transaction.
func spiIOCTx(l int) uint {
	return fs.IOW(spiIOCMagic, 0, uint(l)*32)
}

// spiIOCTransfer is spi_ioc_transfer in linux/spi/spidev.h.
//
// Also documented as struct spi_transfer at
// https://www.kernel.org/doc/html/latest/driver-api/spi.html
type spiIOCTransfer struct {
	tx          uint64 // Pointer to byte slice
	rx          uint64 // Pointer to byte slice
	length      uint32 // buffer length of tx and rx in bytes
	speedHz     uint32 // temporarily override the speed
	delayUsecs  uint16 // µs to sleep before selecting the device before the next transfer
	bitsPerWord uint8  // temporarily override the number of bytes per word
	csChange    uint8  // true to deassert CS before next transfer
	txNBits     uint8
	rxNBits     uint8
	pad         uint16
}

func (s *spiIOCTransfer) reset(w, r []byte, f physic.Frequency, bitsPerWord uint8, csInvert bool) {
	s.tx = 0
	s.rx = 0
	s.length = 0
	// w and r must be the same length.
	if l := len(w); l != 0 {
		s.tx = uint64(uintptr(unsafe.Pointer(&w[0])))
		s.length = uint32(l)
	}
	if l := len(r); l != 0 {
		s.rx = uint64(uintptr(unsafe.Pointer(&r[0])))
		s.length = uint32(l)
	}
	s.speedHz = uint32((f + 500*physic.MilliHertz) / physic.Hertz)
	s.delayUsecs = 0
	s.bitsPerWord = bitsPerWord
	if csInvert {
		s.csChange = 1
	} else {
		s.csChange = 0
	}
	s.txNBits = 0
	s.rxNBits = 0
	s.pad = 0
}

//

// driverSPI implements periph.Driver.
type driverSPI struct {
	// bufSize is the maximum number of bytes allowed per I/O on the SPI port.
	bufSize int
}

func (d *driverSPI) String() string {
	return "sysfs-spi"
}

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

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

func (d *driverSPI) Init() (bool, error) {
	// This driver is only registered on linux, so there is no legitimate time to
	// skip it.

	// Do not use "/sys/bus/spi/devices/spi" as Raspbian's provided udev rules
	// only modify the ACL of /dev/spidev* but not the ones in /sys/bus/...
	prefix := "/dev/spidev"
	items, err2 := filepath.Glob(prefix + "*")
	if err2 != nil {
		return true, err2
	}
	if len(items) == 0 {
		return false, errors.New("no SPI port found")
	}
	sort.Strings(items)
	for _, item := range items {
		parts := strings.Split(item[len(prefix):], ".")
		if len(parts) != 2 {
			continue
		}
		bus, err := strconv.Atoi(parts[0])
		if err != nil {
			continue
		}
		cs, err := strconv.Atoi(parts[1])
		if err != nil {
			continue
		}
		name := fmt.Sprintf("/dev/spidev%d.%d", bus, cs)
		aliases := []string{fmt.Sprintf("SPI%d.%d", bus, cs)}
		n := bus
		if cs != 0 {
			n = -1
		}
		if err := spireg.Register(name, aliases, n, (&openerSPI{bus, cs}).Open); err != nil {
			return true, err
		}
	}
	f, err := fs.Open("/sys/module/spidev/parameters/bufsiz", os.O_RDONLY)
	if err != nil {
		return true, err
	}
	defer f.Close()
	b, err := ioutil.ReadAll(f)
	if err != nil {
		return true, err
	}
	// Update the global value.
	drvSPI.bufSize, err = strconv.Atoi(strings.TrimSpace(string(b)))
	return true, err
}

type openerSPI struct {
	bus int
	cs  int
}

func (o *openerSPI) Open() (spi.PortCloser, error) {
	return NewSPI(o.bus, o.cs)
}

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

var drvSPI driverSPI

var _ conn.Limits = &SPI{}
var _ conn.Limits = &spiConn{}
var _ io.Reader = &spiConn{}
var _ io.Writer = &spiConn{}
var _ spi.Conn = &spiConn{}
var _ spi.Pins = &SPI{}
var _ spi.Pins = &spiConn{}
var _ spi.Port = &SPI{}
var _ spi.PortCloser = &SPI{}
var _ fmt.Stringer = &SPI{}
