// 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 spi defines the API to communicate with devices over the SPI
// protocol.
//
// As described in https://periph.io/x/conn/v3#hdr-Concepts, periph.io uses
// the concepts of Bus, Port and Conn.
//
// In the package spi, 'Bus' is not exposed, as it would be SPI bus number
// without a CS line, for example on linux asking for "/dev/spi0" without the
// ".0" suffix.
//
// The OS doesn't allow that so it is counter productive to express this at the
// API layer, so 'Port' is exposed directly instead.
//
// Use Port.Connect() converts the uninitialized Port into a Conn.
//
// See https://en.wikipedia.org/wiki/Serial_Peripheral_Interface for more
// information.
package spi

import (
	"io"
	"strconv"

	"periph.io/x/conn/v3"
	"periph.io/x/conn/v3/gpio"
	"periph.io/x/conn/v3/physic"
)

// Mode determines how communication is done.
//
// The bits can be OR'ed to change the parameters used for
// communication.
//
type Mode int

// Mode determines the SPI communication parameters.
//
// CPOL means the clock polarity. Idle is High when set.
//
// CPHA is the clock phase, sample on trailing edge when set.
const (
	Mode0 Mode = 0x0 // CPOL=0, CPHA=0
	Mode1 Mode = 0x1 // CPOL=0, CPHA=1
	Mode2 Mode = 0x2 // CPOL=1, CPHA=0
	Mode3 Mode = 0x3 // CPOL=1, CPHA=1

	// HalfDuplex specifies that MOSI and MISO use the same wire, and that only
	// one duplex is used at a time.
	HalfDuplex Mode = 0x4
	// NoCS request the driver to not use the CS line.
	NoCS Mode = 0x8
	// LSBFirst requests the words to be encoded in little endian instead of the
	// default big endian.
	LSBFirst = 0x10
)

func (m Mode) String() string {
	s := ""
	switch m & Mode3 {
	case Mode0:
		s = "Mode0"
	case Mode1:
		s = "Mode1"
	case Mode2:
		s = "Mode2"
	case Mode3:
		s = "Mode3"
	}
	m &^= Mode3
	if m&HalfDuplex != 0 {
		s += "|HalfDuplex"
	}
	m &^= HalfDuplex
	if m&NoCS != 0 {
		s += "|NoCS"
	}
	m &^= NoCS
	if m&LSBFirst != 0 {
		s += "|LSBFirst"
	}
	m &^= LSBFirst
	if m != 0 {
		s += "|0x"
		s += strconv.FormatUint(uint64(m), 16)
	}
	return s
}

// Packet represents one packet when sending multiple packets as a transaction.
type Packet struct {
	// W and R are the output and input data. When HalfDuplex is specified to
	// Connect, only one of the two can be set.
	W, R []byte
	// BitsPerWord overrides the default bits per word value set in Connect.
	BitsPerWord uint8
	// KeepCS tells the driver to keep CS asserted after this packet is
	// completed. This can be leveraged to create long transaction as multiple
	// packets like to use 9 bits commands then 8 bits data.
	//
	// Normally during a spi.Conn.TxPackets() call, KeepCS should be set to true
	// for all packets except the last one. If the last one is set to true, the
	// CS line stays asserted, leaving the transaction hanging on the bus.
	//
	// KeepCS is ignored when NoCS was specified to Connect.
	KeepCS bool
}

// Conn defines the interface a concrete SPI driver must implement.
//
// Implementers can optionally implement io.Writer and io.Reader for
// unidirectional operation.
type Conn interface {
	conn.Conn
	// TxPackets does multiple operations over the SPI connection.
	//
	// The maximum number of bytes can be limited depending on the driver. Query
	// conn.Limits.MaxTxSize() can be used to determine the limit.
	//
	// If the last packet has KeepCS:true, the CS line stays asserted. This
	// enables doing SPI transaction over multiple calls.
	//
	// Conversely, if any packet beside the last one has KeepCS:false, the CS
	// line will blip for a short amount of time to force a new transaction.
	//
	// It was observed on RPi3 hardware to have a one clock delay between each
	// packet.
	TxPackets(p []Packet) error
}

// Port is the interface to be provided to device drivers.
//
// The device driver, that is the driver for the peripheral connected over
// this port, calls Connect() to retrieve a configured connection as Conn.
type Port interface {
	String() string
	// Connect sets the communication parameters of the connection for use by a
	// device.
	//
	// The device driver must call this function exactly once.
	//
	// f must specify the maximum rated speed by the device's spec. The lowest
	// speed between the port speed and the device speed is selected. Use 0 for f
	// if there is no known maximum value for this device.
	//
	// mode specifies the clock and signal polarities, if the port is using half
	// duplex (shared MISO and MOSI) or if CS is not needed.
	//
	// bits is the number of bits per word. Generally you should use 8.
	Connect(f physic.Frequency, mode Mode, bits int) (Conn, error)
}

// PortCloser is a SPI port that can be closed.
//
// This interface is meant to be handled by the application.
type PortCloser interface {
	io.Closer
	Port
	// LimitSpeed sets the maximum port speed.
	//
	// It lets an application use a device at a lower speed than the maximum
	// speed as rated by the device driver. This is useful for example when the
	// wires are long or the connection is of poor quality.
	//
	// This function can be called multiple times and resets the previous value.
	// 0 is not a valid value for f. The lowest speed between the port speed and
	// the device speed is selected.
	LimitSpeed(f physic.Frequency) error
}

// Pins defines the pins that a SPI port interconnect is using on the host.
//
// It is expected that a implementer of ConnCloser or Conn also implement Pins
// but this is not a requirement.
type Pins interface {
	// CLK returns the SCK (clock) pin.
	CLK() gpio.PinOut
	// MOSI returns the SDO (master out, slave in) pin.
	MOSI() gpio.PinOut
	// MISO returns the SDI (master in, slave out) pin.
	MISO() gpio.PinIn
	// CS returns the CSN (chip select) pin.
	CS() gpio.PinOut
}
