// SPDX-FileCopyrightText: 2019 Kent Gibson <warthog618@gmail.com>
//
// SPDX-License-Identifier: MIT

//go:build linux
// +build linux

// Package uapi provides the Linux GPIO UAPI definitions for gpiocdev.
package uapi

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"regexp"
	"strconv"
	"unsafe"

	"golang.org/x/sys/unix"
)

// GetChipInfo returns the ChipInfo for the GPIO character device.
//
// The fd is an open GPIO character device.
func GetChipInfo(fd uintptr) (ChipInfo, error) {
	var ci ChipInfo
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(getChipInfoIoctl),
		uintptr(unsafe.Pointer(&ci)))
	if errno != 0 {
		return ci, errno
	}
	return ci, nil
}

// GetLineInfo returns the LineInfo for one line from the GPIO character device.
//
// The fd is an open GPIO character device.
// The offset is zero based.
func GetLineInfo(fd uintptr, offset int) (LineInfo, error) {
	li := LineInfo{Offset: uint32(offset)}
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(getLineInfoIoctl),
		uintptr(unsafe.Pointer(&li)))
	if errno != 0 {
		return LineInfo{}, errno
	}
	return li, nil
}

// GetLineEvent requests a line from the GPIO character device with event
// reporting enabled.
//
// The fd is an open GPIO character device.
// The line must be an input and must not already be requested.
// If successful, the fd for the line is returned in the request.fd.
func GetLineEvent(fd uintptr, request *EventRequest) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(getLineEventIoctl),
		uintptr(unsafe.Pointer(request)))
	if errno != 0 {
		return errno
	}
	return nil
}

// GetLineHandle requests a line from the GPIO character device.
//
// This request is without event reporting.
// The fd is an open GPIO character device.
// The lines must not already be requested.
// The flags in the request will be applied to all lines in the request.
// If successful, the fd for the line is returned in the request.fd.
func GetLineHandle(fd uintptr, request *HandleRequest) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(getLineHandleIoctl),
		uintptr(unsafe.Pointer(request)))
	if errno != 0 {
		return errno
	}
	return nil
}

// GetLineValues returns the values of a set of requested lines.
//
// The fd is a requested line, as returned by GetLineHandle or GetLineEvent.
func GetLineValues(fd uintptr, values *HandleData) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(getLineValuesIoctl),
		uintptr(unsafe.Pointer(&values[0])))
	if errno != 0 {
		return errno
	}
	return nil
}

// SetLineValues sets the values of a set of requested lines.
//
// The fd is a requested line, as returned by GetLineHandle or GetLineEvent.
func SetLineValues(fd uintptr, values HandleData) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(setLineValuesIoctl),
		uintptr(unsafe.Pointer(&values[0])))
	if errno != 0 {
		return errno
	}
	return nil
}

// SetLineConfig sets the config of an existing handle request.
//
// The config flags in the request will be applied to all lines in the handle
// request.
func SetLineConfig(fd uintptr, config *HandleConfig) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(setLineConfigIoctl),
		uintptr(unsafe.Pointer(config)))
	if errno != 0 {
		return errno
	}
	return nil
}

// WatchLineInfo sets a watch on info of a line.
//
// A watch is set on the line indicated by info.Offset. If successful the
// current line info is returned, else an error is returned.
func WatchLineInfo(fd uintptr, info *LineInfo) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(watchLineInfoIoctl),
		uintptr(unsafe.Pointer(info)))
	if errno != 0 {
		return errno
	}
	return nil
}

// UnwatchLineInfo clears a watch on info of a line.
//
// Disables the watch on info for the line.
func UnwatchLineInfo(fd uintptr, offset uint32) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL,
		fd,
		uintptr(unwatchLineInfoIoctl),
		uintptr(unsafe.Pointer(&offset)))
	if errno != 0 {
		return errno
	}
	return nil
}

// BytesToString is a helper function that converts strings stored in byte
// arrays, as returned by GetChipInfo and GetLineInfo, into strings.
func BytesToString(a []byte) string {
	n := bytes.IndexByte(a, 0)
	if n == -1 {
		return string(a)
	}
	return string(a[:n])
}

type fdReader int

func (fd fdReader) Read(b []byte) (int, error) {
	return unix.Read(int(fd), b[:])
}

// ReadEvent reads a single event from a requested line.
//
// The fd is a requested line, as returned by GetLineEvent.
//
// This function is blocking and should only be called when the fd is known to
// be ready to read.
func ReadEvent(fd uintptr) (EventData, error) {
	var ed EventData
	err := binary.Read(fdReader(fd), nativeEndian, &ed)
	return ed, err
}

// ReadLineInfoChanged reads a line info changed event from a chip.
//
// The fd is an open GPIO character device.
//
// This function is blocking and should only be called when the fd is known to
// be ready to read.
func ReadLineInfoChanged(fd uintptr) (LineInfoChanged, error) {
	var lic LineInfoChanged
	err := binary.Read(fdReader(fd), nativeEndian, &lic)
	return lic, err
}

// IOCTL command codes
type ioctl uintptr

var (
	getChipInfoIoctl     ioctl
	getLineInfoIoctl     ioctl
	getLineHandleIoctl   ioctl
	getLineEventIoctl    ioctl
	getLineValuesIoctl   ioctl
	setLineValuesIoctl   ioctl
	setLineConfigIoctl   ioctl
	watchLineInfoIoctl   ioctl
	unwatchLineInfoIoctl ioctl
)

// Size of name and consumer strings.
const nameSize = 32

func init() {
	// ioctls require struct sizes which are only available at runtime.
	var ci ChipInfo
	getChipInfoIoctl = ior(0xB4, 0x01, unsafe.Sizeof(ci))
	var li LineInfo
	getLineInfoIoctl = iorw(0xB4, 0x02, unsafe.Sizeof(li))
	var hr HandleRequest
	getLineHandleIoctl = iorw(0xB4, 0x03, unsafe.Sizeof(hr))
	var le EventRequest
	getLineEventIoctl = iorw(0xB4, 0x04, unsafe.Sizeof(le))
	var hd HandleData
	getLineValuesIoctl = iorw(0xB4, 0x08, unsafe.Sizeof(hd))
	setLineValuesIoctl = iorw(0xB4, 0x09, unsafe.Sizeof(hd))
	var hc HandleConfig
	setLineConfigIoctl = iorw(0xB4, 0x0A, unsafe.Sizeof(hc))
	watchLineInfoIoctl = iorw(0xB4, 0x0B, unsafe.Sizeof(li))
	unwatchLineInfoIoctl = iorw(0xB4, 0x0C, unsafe.Sizeof(li.Offset))
}

// ChipInfo contains the details of a GPIO chip.
type ChipInfo struct {
	// The system name of the device.
	Name [nameSize]byte

	// An identifying label added by the device driver.
	Label [nameSize]byte

	// The number of lines supported by this chip.
	Lines uint32
}

// LineInfo contains the details of a single line of a GPIO chip.
type LineInfo struct {
	// The offset of the line within the chip.
	Offset uint32

	// The line flags applied to this line.
	Flags LineFlag

	// The system name for this line.
	Name [nameSize]byte

	// If requested, a string added by the requester to identify the
	// owner of the request.
	Consumer [nameSize]byte
}

// LineInfoChanged contains the details of a change to line info.
//
// This is returned via the chip fd in response to changes to watched lines.
type LineInfoChanged struct {
	// The updated info.
	Info LineInfo

	// The time the change occurred.
	Timestamp uint64

	// The type of change.
	Type ChangeType

	// reserved for future use.
	_ [5]uint32
}

// ChangeType indicates the type of change that has occurred to a line.
type ChangeType uint32

const (
	_ ChangeType = iota

	// LineChangedRequested indicates the line has been requested.
	LineChangedRequested

	// LineChangedReleased indicates the line has been released.
	LineChangedReleased

	// LineChangedConfig indicates the line configuration has changed.
	LineChangedConfig
)

// LineFlag are the flags for a line.
type LineFlag uint32

const (
	// LineFlagUsed indicates that the line has been requested.
	// It may have been requested by this process or another process.
	// The line cannot be requested again until this flag is clear.
	LineFlagUsed LineFlag = 1 << iota

	// LineFlagIsOut indicates that the line is an output.
	LineFlagIsOut

	// LineFlagActiveLow indicates that the line is active low.
	LineFlagActiveLow

	// LineFlagOpenDrain indicates that the line will pull low when set low but
	// float when set high. This flag only applies to output lines.
	// An output cannot be both open drain and open source.
	LineFlagOpenDrain

	// LineFlagOpenSource indicates that the line will pull high when set high
	// but float when set low. This flag only applies to output lines.
	// An output cannot be both open drain and open source.
	LineFlagOpenSource

	// LineFlagPullUp indicates that the internal line pull up is enabled.
	LineFlagPullUp

	// LineFlagPullDown indicates that the internal line pull down is enabled.
	LineFlagPullDown

	// LineFlagBiasDisabled indicates that the internal line bias is disabled.
	LineFlagBiasDisabled
)

// IsUsed returns true if the line is requested.
func (f LineFlag) IsUsed() bool {
	return f&LineFlagUsed != 0
}

// IsOut returns true if the line is an output.
func (f LineFlag) IsOut() bool {
	return f&LineFlagIsOut != 0
}

// IsActiveLow returns true if the line is active low.
func (f LineFlag) IsActiveLow() bool {
	return f&LineFlagActiveLow != 0
}

// IsOpenDrain returns true if the line is open-drain.
func (f LineFlag) IsOpenDrain() bool {
	return f&LineFlagOpenDrain != 0
}

// IsOpenSource returns true if the line is open-source.
func (f LineFlag) IsOpenSource() bool {
	return f&LineFlagOpenSource != 0
}

// IsBiasDisable returns true if the line has bias disabled.
func (f LineFlag) IsBiasDisable() bool {
	return f&LineFlagBiasDisabled != 0
}

// IsPullDown returns true if the line has pull-down enabled.
func (f LineFlag) IsPullDown() bool {
	return f&LineFlagPullDown != 0
}

// IsPullUp returns true if the line has pull-up enabled.
func (f LineFlag) IsPullUp() bool {
	return f&LineFlagPullUp != 0
}

// HandleConfig is a request to change the config of an existing request.
//
// Can be applied to both handle and event requests.
// Event requests cannot be reconfigured to outputs.
type HandleConfig struct {
	// The flags to be applied to the lines.
	Flags HandleFlag

	// The default values to be applied to output lines (when
	// HandleRequestOutput is set in the Flags).
	DefaultValues [HandlesMax]uint8

	// reserved for future use.
	_ [4]uint32
}

// HandleRequest is a request for control of a set of lines.
// The lines must all be on the same GPIO chip.
type HandleRequest struct {
	// The lines to be requested.
	Offsets [HandlesMax]uint32

	// The flags to be applied to the lines.
	Flags HandleFlag

	// The default values to be applied to output lines.
	DefaultValues [HandlesMax]uint8

	// The string identifying the requester to be applied to the lines.
	Consumer [nameSize]byte

	// The number of lines being requested.
	Lines uint32

	// The file handle for the requested lines.
	// Set if the request is successful.
	Fd int32
}

// HandleFlag contains the
type HandleFlag uint32

const (
	// HandleRequestInput requests the line as an input.
	//
	// This is ignored if Output is also set.
	HandleRequestInput HandleFlag = 1 << iota

	// HandleRequestOutput requests the line as an output.
	//
	// This takes precedence over Input, if both are set.
	HandleRequestOutput

	// HandleRequestActiveLow requests the line be made active low.
	HandleRequestActiveLow

	// HandleRequestOpenDrain requests the line be made open drain.
	//
	// This option requires the line to be requested as an Output.
	// This cannot be set at the same time as OpenSource.
	HandleRequestOpenDrain

	// HandleRequestOpenSource requests the line be made open source.
	//
	// This option requires the line to be requested as an Output.
	// This cannot be set at the same time as OpenDrain.
	HandleRequestOpenSource

	// HandleRequestPullUp requests the line have pull-up enabled.
	HandleRequestPullUp

	// HandleRequestPullDown requests the line have pull-down enabled.
	HandleRequestPullDown

	// HandleRequestBiasDisable requests the line have bias disabled.
	HandleRequestBiasDisable

	// HandlesMax is the maximum number of lines that can be requested in a
	// single request.
	HandlesMax = 64
)

// IsInput returns true if the line is requested as an input.
func (f HandleFlag) IsInput() bool {
	return f&HandleRequestInput != 0
}

// IsOutput returns true if the line is requested as an output.
func (f HandleFlag) IsOutput() bool {
	return f&HandleRequestOutput != 0
}

// IsActiveLow returns true if the line is requested as a active low.
func (f HandleFlag) IsActiveLow() bool {
	return f&HandleRequestActiveLow != 0
}

// IsOpenDrain returns true if the line is requested as an open drain.
func (f HandleFlag) IsOpenDrain() bool {
	return f&HandleRequestOpenDrain != 0
}

// IsOpenSource returns true if the line is requested as an open source.
func (f HandleFlag) IsOpenSource() bool {
	return f&HandleRequestOpenSource != 0
}

// HasBiasFlag returns true if any bias flags are set.
func (f HandleFlag) HasBiasFlag() bool {
	return f&(HandleRequestBiasDisable|HandleRequestPullDown|HandleRequestPullUp) != 0

}

// IsBiasDisable returns true if the line is requested with bias disabled.
func (f HandleFlag) IsBiasDisable() bool {
	return f&HandleRequestBiasDisable != 0
}

// IsPullDown returns true if the line is requested with pull-down enabled.
func (f HandleFlag) IsPullDown() bool {
	return f&HandleRequestPullDown != 0
}

// IsPullUp returns true if the line is requested with pull-up enabled.
func (f HandleFlag) IsPullUp() bool {
	return f&HandleRequestPullUp != 0
}

// HandleData contains the logical value for each line.
// Zero is a logical low and any other value is a logical high.
type HandleData [HandlesMax]uint8

// EventRequest is a request for control of a line with event reporting enabled.
type EventRequest struct {
	// The line to be requested.
	Offset uint32

	// The line flags applied to this line.
	HandleFlags HandleFlag

	// The type of events to report.
	EventFlags EventFlag

	// The string identifying the requester to be applied to the line.
	Consumer [nameSize]byte

	// The file handle for the requested line.
	// Set if the request is successful.
	Fd int32
}

// EventFlag indicates the types of events that will be reported.
type EventFlag uint32

const (
	// EventRequestRisingEdge requests rising edge events.
	// This means a transition from a low logical state to a high logical state.
	// For active high lines (the default) this means a transition from a
	// physical low to a physical high.
	// Note that for active low lines this means a transition from a physical
	// high to a physical low.
	EventRequestRisingEdge EventFlag = 1 << iota

	// EventRequestFallingEdge requests falling edge events.
	// This means a transition from a high logical state to a low logical state.
	// For active high lines (the default) this means a transition from a
	// physical high to a physical low.
	// Note that for active low lines this means a transition from a physical
	// low to a physical high.
	EventRequestFallingEdge

	// EventRequestBothEdges requests both rising and falling edge events.
	// This is equivalent to requesting both EventRequestRisingEdge and
	// EventRequestRisingEdge.
	EventRequestBothEdges = EventRequestRisingEdge | EventRequestFallingEdge
)

// IsRisingEdge returns true if rising edge events have been requested.
func (f EventFlag) IsRisingEdge() bool {
	return f&EventRequestRisingEdge != 0
}

// IsFallingEdge returns true if falling edge events have been requested.
func (f EventFlag) IsFallingEdge() bool {
	return f&EventRequestFallingEdge != 0
}

// IsBothEdges returns true if both rising and falling edge events have been
// requested.
func (f EventFlag) IsBothEdges() bool {
	return f&EventRequestBothEdges == EventRequestBothEdges
}

// KernelVersion returns the running kernel version.
func KernelVersion() (semver []byte, err error) {
	uname := unix.Utsname{}
	err = unix.Uname(&uname)
	if err != nil {
		return
	}
	release := string(uname.Release[:])
	r := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`)
	vers := r.FindStringSubmatch(release)
	if len(vers) != 4 {
		err = fmt.Errorf("can't parse uname: %s", release)
		return
	}
	v := []byte{0, 0, 0}
	for i, vf := range vers[1:] {
		var vfi uint64
		vfi, err = strconv.ParseUint(vf, 10, 64)
		if err != nil {
			return
		}
		v[i] = byte(vfi)
	}
	semver = v
	return
}

// CheckKernelVersion returns an error if the kernel version is less than the
// min.
func CheckKernelVersion(min Semver) error {
	kv, err := KernelVersion()
	if err != nil {
		return err
	}
	n := bytes.Compare(kv, min[:])
	if n < 0 {
		return ErrorBadVersion{Need: min, Have: kv}
	}
	return nil
}

// Semver is 3 part version, Major, Minor, Patch.
type Semver []byte

func (v Semver) String() string {
	if len(v) == 0 {
		return ""
	}
	vstr := fmt.Sprintf("%d", v[0])
	for i := 1; i < len(v); i++ {
		vstr += fmt.Sprintf(".%d", v[i])
	}
	return vstr
}

// ErrorBadVersion indicates the kernel version is insufficient.
type ErrorBadVersion struct {
	Need Semver
	Have Semver
}

func (e ErrorBadVersion) Error() string {
	return fmt.Sprintf("require kernel %s or later, but running %s", e.Need, e.Have)
}
