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

// Package gpiocdev is a library for accessing GPIO pins/lines on Linux platforms
// using the GPIO character device.
//
// This is a Go equivalent of libgpiod.
//
// Supports:
//   - Line direction (input/output)
//   - Line write (active/inactive)
//   - Line read (active/inactive)
//   - Line bias (pull-up/pull-down/disabled)
//   - Line drive (push-pull/open-drain/open-source)
//   - Line level (active-high/active-low)
//   - Line edge detection (rising/falling/both)
//   - Line labels
//   - Collections of lines for near simultaneous reads and writes on multiple lines
//
// Example of use:
//
//	v := 0
//	l, err := gpiocdev.RequestLine("gpiochip0", 4, gpiocdev.AsOutput(v))
//	if err != nil {
//		panic(err)
//	}
//	for {
//		<-time.After(time.Second)
//		v ^= 1
//		l.SetValue(v)
//	}
package gpiocdev

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/warthog618/go-gpiocdev/uapi"
	"golang.org/x/sys/unix"
)

// Chip represents a single GPIO chip that controls a set of lines.
type Chip struct {
	f *os.File
	// The system name for this chip.
	Name string

	// A more individual label for the chip.
	Label string

	// The number of GPIO lines on this chip.
	lines int

	// default options for reserved lines.
	options ChipOptions

	// mutex covers the attributes below it.
	mu sync.Mutex

	// watcher for line info changes
	iw *infoWatcher

	// handlers for info changes in watched lines, keyed by offset.
	ich map[int]InfoChangeHandler

	// indicates the chip has been closed.
	closed bool
}

// LineConfig contains the configuration parameters for the line.
type LineConfig struct {
	// A flag indicating if the line is active low.
	ActiveLow bool

	// The line direction.
	Direction LineDirection

	// The line drive.
	Drive LineDrive

	// The line bias.
	Bias LineBias

	// The line edge detection.
	EdgeDetection LineEdge

	// A flag indicating if the line is debounced.
	Debounced bool

	// The line debounce period.
	DebouncePeriod time.Duration

	// The source clock for events on the line.
	EventClock LineEventClock
}

// LineDirection indicates the direction of a line.
type LineDirection int

const (
	// LineDirectionUnknown indicate the line direction is unknown.
	LineDirectionUnknown LineDirection = iota

	// LineDirectionInput indicates the line is an input.
	LineDirectionInput

	// LineDirectionOutput indicates the line is an output.
	LineDirectionOutput
)

// LineDrive indicates the drive of an output line.
type LineDrive int

const (
	// LineDrivePushPull indicates the line is driven in both directions.
	LineDrivePushPull LineDrive = iota

	// LineDriveOpenDrain indicates the line is an open drain output.
	LineDriveOpenDrain

	// LineDriveOpenSource indicates the line is an open souce output.
	LineDriveOpenSource
)

// LineBias indicates the bias applied to a line.
type LineBias int

const (
	// LineBiasUnknown indicates the line bias is unknown.
	LineBiasUnknown LineBias = iota

	// LineBiasDisabled indicates the line bias is disabled.
	LineBiasDisabled

	// LineBiasPullUp indicates the line has pull up enabled.
	LineBiasPullUp

	// LineBiasPullDown indicates the line has pull down enabled.
	LineBiasPullDown
)

// LineEdge indicates the edges detected by the line.
type LineEdge int

const (
	// LineEdgeNone indicates the line edge detection is disabled.
	LineEdgeNone LineEdge = iota

	// LineEdgeRising indicates the line has rising edge detection enabled.
	LineEdgeRising

	// LineEdgeFalling indicates the line has falling edge detection enabled.
	LineEdgeFalling

	// LineEdgeBoth indicates the line has both rising and falling edge
	// detection enabled.
	LineEdgeBoth = LineEdgeRising | LineEdgeFalling
)

// LineEventClock indicates the source clock used to timestamp edge events.
type LineEventClock int

const (
	// LineEventClockMonotonic indicates the source clock is CLOCK_MONOTONIC.
	LineEventClockMonotonic LineEventClock = iota

	// LineEventClockRealtime indicates the source clock is CLOCK_REALTIME.
	LineEventClockRealtime
)

// LineInfo contains a summary of publicly available information about the
// line.
type LineInfo struct {
	// The line offset within the chip.
	Offset int

	// The system name for the line.
	Name string

	// A string identifying the requester of the line, if requested.
	Consumer string

	// The line is in use.
	Used bool

	// The configuration parameters for the line.
	Config LineConfig
}

// Chips returns the names of the available GPIO devices.
func Chips() []string {
	cc := []string(nil)
	for _, name := range chipNames() {
		if IsChip(name) == nil {
			cc = append(cc, name)
		}
	}
	return cc
}

// RequestLine requests control of a single line on a chip.
//
// If granted, control is maintained until the Line is closed.
func RequestLine(chip string, offset int, options ...LineReqOption) (*Line, error) {
	c, err := NewChip(chip)
	if err != nil {
		return nil, err
	}
	defer c.Close()
	return c.RequestLine(offset, options...)
}

// RequestLines requests control of a collection of lines on a chip.
//
// If granted, control is maintained until the Lines are closed.
func RequestLines(chip string, offsets []int, options ...LineReqOption) (*Lines, error) {
	c, err := NewChip(chip)
	if err != nil {
		return nil, err
	}
	defer c.Close()
	return c.RequestLines(offsets, options...)
}

// NewChip opens a GPIO character device.
func NewChip(name string, options ...ChipOption) (*Chip, error) {
	path := nameToPath(name)
	err := IsChip(path)
	if err != nil {
		return nil, err
	}
	co := ChipOptions{
		consumer: fmt.Sprintf("gpiocdev-%d", os.Getpid()),
	}
	for _, option := range options {
		option.applyChipOption(&co)
	}
	f, err := os.OpenFile(path, unix.O_CLOEXEC, unix.O_RDONLY)
	if err != nil {
		// only happens if device removed/locked since IsChip call.
		return nil, err
	}
	ci, err := uapi.GetChipInfo(f.Fd())
	if err != nil {
		// only occurs if IsChip was wrong?
		f.Close()
		return nil, err
	}
	c := Chip{
		f:       f,
		Name:    uapi.BytesToString(ci.Name[:]),
		Label:   uapi.BytesToString(ci.Label[:]),
		lines:   int(ci.Lines),
		options: co,
	}
	if c.options.abi == 0 {
		// probe v2 - should only throw an error if v2 is not supported.
		if _, err = c.LineInfo(0); err == nil {
			c.options.abi = 2
		} else {
			c.options.abi = 1
		}
	}
	if len(c.Label) == 0 {
		c.Label = "unknown"
	}
	return &c, nil
}

// Close releases the Chip.
//
// It does not release any lines which may be requested - they must be closed
// independently.
func (c *Chip) Close() error {
	c.mu.Lock()
	closed := c.closed
	c.closed = true
	c.mu.Unlock()
	if closed {
		return ErrClosed
	}
	if c.iw != nil {
		c.iw.close()
	}
	return c.f.Close()
}

// LineInfo returns the publicly available information on the line.
//
// This is always available and does not require requesting the line.
func (c *Chip) LineInfo(offset int) (info LineInfo, err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		err = ErrClosed
		return
	}
	if offset < 0 || offset >= c.lines {
		err = ErrInvalidOffset
		return
	}
	if c.options.abi == 1 {
		var li uapi.LineInfo
		li, err = uapi.GetLineInfo(c.f.Fd(), offset)
		if err == nil {
			info = newLineInfo(li)
		}
		return
	}
	var li uapi.LineInfoV2
	li, err = uapi.GetLineInfoV2(c.f.Fd(), offset)
	if err == nil {
		info = newLineInfoV2(li)
	}
	return
}

func lineInfoToLineConfig(li uapi.LineInfo) LineConfig {
	lc := LineConfig{}
	lc.ActiveLow = li.Flags.IsActiveLow()

	if li.Flags.IsOut() {
		lc.Direction = LineDirectionOutput
		if li.Flags.IsOpenDrain() {
			lc.Drive = LineDriveOpenDrain
		} else if li.Flags.IsOpenSource() {
			lc.Drive = LineDriveOpenSource
		}
	} else {
		lc.Direction = LineDirectionInput
	}

	if li.Flags.IsPullUp() {
		lc.Bias = LineBiasPullUp
	} else if li.Flags.IsPullDown() {
		lc.Bias = LineBiasPullDown
	} else if li.Flags.IsBiasDisable() {
		lc.Bias = LineBiasDisabled
	}
	return lc
}

func lineInfoV2ToLineConfig(li uapi.LineInfoV2) LineConfig {
	lc := LineConfig{}
	lc.ActiveLow = li.Flags.IsActiveLow()

	if li.Flags.IsOutput() {
		lc.Direction = LineDirectionOutput
		if li.Flags.IsOpenDrain() {
			lc.Drive = LineDriveOpenDrain
		} else if li.Flags.IsOpenSource() {
			lc.Drive = LineDriveOpenSource
		}
	} else {
		lc.Direction = LineDirectionInput
	}

	if li.Flags.IsBothEdges() {
		lc.EdgeDetection = LineEdgeBoth
	} else if li.Flags.IsRisingEdge() {
		lc.EdgeDetection = LineEdgeRising
	} else if li.Flags.IsFallingEdge() {
		lc.EdgeDetection = LineEdgeFalling
	}

	if li.Flags.IsBiasPullUp() {
		lc.Bias = LineBiasPullUp
	} else if li.Flags.IsBiasPullDown() {
		lc.Bias = LineBiasPullDown
	} else if li.Flags.IsBiasDisabled() {
		lc.Bias = LineBiasDisabled
	}

	for i := 0; i < int(li.NumAttrs); i++ {
		if li.Attrs[i].ID == uapi.LineAttributeIDDebounce {
			lc.Debounced = true
			lc.DebouncePeriod = time.Duration(li.Attrs[i].Value32()) * time.Microsecond
		}
	}
	return lc
}

func newLineInfo(li uapi.LineInfo) LineInfo {
	return LineInfo{
		Offset:   int(li.Offset),
		Name:     uapi.BytesToString(li.Name[:]),
		Consumer: uapi.BytesToString(li.Consumer[:]),
		Used:     li.Flags.IsUsed(),
		Config:   lineInfoToLineConfig(li),
	}
}

func newLineInfoV2(li uapi.LineInfoV2) LineInfo {
	return LineInfo{
		Offset:   int(li.Offset),
		Name:     uapi.BytesToString(li.Name[:]),
		Consumer: uapi.BytesToString(li.Consumer[:]),
		Used:     li.Flags.IsUsed(),
		Config:   lineInfoV2ToLineConfig(li),
	}
}

// Lines returns the number of lines that exist on the GPIO chip.
func (c *Chip) Lines() int {
	return c.lines
}

// RequestLine requests control of a single line on the chip.
//
// If granted, control is maintained until the Line is closed.
func (c *Chip) RequestLine(offset int, options ...LineReqOption) (*Line, error) {
	ll, err := c.RequestLines([]int{offset}, options...)
	if err != nil {
		return nil, err
	}
	l := Line{
		baseLine: baseLine{
			offsets: ll.offsets,
			values:  ll.values,
			vfd:     ll.vfd,
			isEvent: ll.isEvent,
			chip:    ll.chip,
			abi:     ll.abi,
			defCfg:  ll.defCfg,
			watcher: ll.watcher,
		},
	}
	return &l, nil
}

// RequestLines requests control of a collection of lines on the chip.
//
// If granted, control is maintained until the Lines are closed.
func (c *Chip) RequestLines(offsets []int, options ...LineReqOption) (*Lines, error) {
	for _, o := range offsets {
		if o < 0 || o >= c.lines {
			return nil, ErrInvalidOffset
		}
	}
	offsets = append([]int(nil), offsets...)
	lro := lineReqOptions{
		lineConfigOptions: lineConfigOptions{
			offsets: offsets,
			values:  map[int]int{},
			defCfg:  c.options.config,
		},
		consumer: c.options.consumer,
		abi:      c.options.abi,
		eh:       c.options.eh,
	}
	for _, option := range options {
		option.applyLineReqOption(&lro)
	}
	ll := Lines{
		baseLine: baseLine{
			offsets: offsets,
			values:  lro.values,
			chip:    c.Name,
			abi:     lro.abi,
			defCfg:  lro.defCfg,
		},
	}
	var err error
	if ll.abi == 2 {
		ll.vfd, ll.watcher, err = c.getLine(ll.offsets, lro)
	} else {
		err = lro.defCfg.v1Validate()
		if err != nil {
			return nil, err
		}
		if lro.eh == nil {
			ll.vfd, err = c.getHandleRequest(ll.offsets, lro)
		} else {
			ll.isEvent = true
			ll.vfd, ll.watcher, err = c.getEventRequest(ll.offsets, lro)
		}
	}
	if err != nil {
		return nil, err
	}
	return &ll, nil
}

// creates the iw and ich
//
// Assumes c is locked.
func (c *Chip) createInfoWatcher() error {
	iw, err := newInfoWatcher(int(c.f.Fd()),
		func(lic LineInfoChangeEvent) {
			c.mu.Lock()
			ich := c.ich[lic.Info.Offset]
			c.mu.Unlock() // handler called outside lock
			if ich != nil {
				ich(lic)
			}
		},
		c.options.abi)
	if err != nil {
		return err
	}
	c.iw = iw
	c.ich = map[int]InfoChangeHandler{}
	return nil
}

// WatchLineInfo enables watching changes to line info for the specified lines.
//
// The changes are reported via the chip InfoChangeHandler.
// Repeated calls replace the InfoChangeHandler.
//
// Requires Linux v5.7 or later.
func (c *Chip) WatchLineInfo(offset int, lich InfoChangeHandler) (info LineInfo, err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		err = ErrClosed
		return
	}
	if c.iw == nil {
		err = c.createInfoWatcher()
		if err != nil {
			return
		}
	}
	if c.options.abi == 1 {
		li := uapi.LineInfo{Offset: uint32(offset)}
		err = uapi.WatchLineInfo(c.f.Fd(), &li)
		if err != nil {
			return
		}
		c.ich[offset] = lich
		info = newLineInfo(li)
		return
	}
	li := uapi.LineInfoV2{Offset: uint32(offset)}
	err = uapi.WatchLineInfoV2(c.f.Fd(), &li)
	if err != nil {
		return
	}
	c.ich[offset] = lich
	info = newLineInfoV2(li)
	return
}

// UnwatchLineInfo disables watching changes to line info.
//
// Requires Linux v5.7 or later.
func (c *Chip) UnwatchLineInfo(offset int) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return nil
	}
	delete(c.ich, offset)
	return uapi.UnwatchLineInfo(c.f.Fd(), uint32(offset))
}

func (c *Chip) getLine(offsets []int, lro lineReqOptions) (uintptr, io.Closer, error) {

	config, err := lro.toULineConfig()
	if err != nil {
		return 0, nil, err
	}
	lr := uapi.LineRequest{
		Lines:  uint32(len(offsets)),
		Config: config,
	}
	copy(lr.Consumer[:len(lr.Consumer)-1], lro.consumer)
	// copy(hr.Offsets[:], offsets) - with cast
	for i, o := range offsets {
		lr.Offsets[i] = uint32(o)
	}
	err = uapi.GetLine(c.f.Fd(), &lr)
	if err != nil {
		return 0, nil, err
	}
	var w io.Closer
	if lro.eh != nil {
		w, err = newWatcher(lr.Fd, lro.eh)
		if err != nil {
			unix.Close(int(lr.Fd))
			return 0, nil, err
		}
	}
	return uintptr(lr.Fd), w, nil
}

func (lc LineConfig) toHandleFlags() uapi.HandleFlag {
	var flags uapi.HandleFlag

	if lc.ActiveLow {
		flags |= uapi.HandleRequestActiveLow
	}

	switch lc.Direction {
	case LineDirectionOutput:
		flags |= uapi.HandleRequestOutput
	case LineDirectionInput:
		flags |= uapi.HandleRequestInput
	}

	switch lc.Drive {
	case LineDriveOpenDrain:
		flags |= uapi.HandleRequestOpenDrain
	case LineDriveOpenSource:
		flags |= uapi.HandleRequestOpenSource
	}

	switch lc.Bias {
	case LineBiasPullUp:
		flags |= uapi.HandleRequestPullUp
	case LineBiasPullDown:
		flags |= uapi.HandleRequestPullDown
	case LineBiasDisabled:
		flags |= uapi.HandleRequestBiasDisable
	}

	return flags
}

func (lc LineConfig) toEventFlags() uapi.EventFlag {
	switch lc.EdgeDetection {
	case LineEdgeBoth:
		return uapi.EventRequestBothEdges
	case LineEdgeRising:
		return uapi.EventRequestRisingEdge
	case LineEdgeFalling:
		return uapi.EventRequestFallingEdge
	default:
		return 0
	}
}

func (lc LineConfig) toLineFlagV2() (flags uapi.LineFlagV2) {
	if lc.ActiveLow {
		flags |= uapi.LineFlagV2ActiveLow
	}
	switch lc.Direction {
	case LineDirectionOutput:
		flags |= uapi.LineFlagV2Output
		switch lc.Drive {
		case LineDriveOpenDrain:
			flags |= uapi.LineFlagV2OpenDrain
		case LineDriveOpenSource:
			flags |= uapi.LineFlagV2OpenSource
		}
	case LineDirectionInput:
		flags |= uapi.LineFlagV2Input
		if lc.EdgeDetection&LineEdgeRising != 0 {
			flags |= uapi.LineFlagV2EdgeRising
		}
		if lc.EdgeDetection&LineEdgeFalling != 0 {
			flags |= uapi.LineFlagV2EdgeFalling
		}
		if lc.EventClock == LineEventClockRealtime {
			flags |= uapi.LineFlagV2EventClockRealtime
		}
	}

	switch lc.Bias {
	case LineBiasDisabled:
		flags |= uapi.LineFlagV2BiasDisabled
	case LineBiasPullUp:
		flags |= uapi.LineFlagV2BiasPullUp
	case LineBiasPullDown:
		flags |= uapi.LineFlagV2BiasPullDown
	}
	return
}

func (lc LineConfig) toLineAttributes() (attrs []uapi.LineAttribute) {
	flags := lc.toLineFlagV2()
	attr := uapi.LineAttribute{}
	if flags != 0 {
		attr.Encode64(uapi.LineAttributeIDFlags, uint64(flags))
		attrs = append(attrs, attr)
	}
	if lc.Debounced {
		attr = uapi.DebouncePeriod(lc.DebouncePeriod).Encode()
		attrs = append(attrs, attr)
	}
	return
}

func (lc LineConfig) v1Validate() error {
	if lc.Debounced {
		return ErrUapiIncompatibility{"debounce", 1}
	}
	if lc.EventClock != LineEventClockMonotonic {
		return ErrUapiIncompatibility{"event clock", 1}
	}
	return nil
}

func (c *Chip) getEventRequest(offsets []int, lro lineReqOptions) (uintptr, io.Closer, error) {
	var vfd uintptr
	fds := make(map[int]int)
	for i, o := range offsets {
		er := uapi.EventRequest{
			Offset:      uint32(o),
			HandleFlags: lro.defCfg.toHandleFlags(),
			EventFlags:  lro.defCfg.toEventFlags(),
		}
		copy(er.Consumer[:len(er.Consumer)-1], lro.consumer)
		err := uapi.GetLineEvent(c.f.Fd(), &er)
		if err != nil {
			return 0, nil, err
		}
		fd := uintptr(er.Fd)
		if i == 0 {
			vfd = fd
		}
		fds[int(fd)] = o
	}
	w, err := newWatcherV1(fds, lro.eh)
	if err != nil {
		for fd := range fds {
			unix.Close(fd)
		}
		return 0, nil, err
	}
	return vfd, w, nil
}

func (c *Chip) getHandleRequest(offsets []int, lro lineReqOptions) (uintptr, error) {
	hr := uapi.HandleRequest{
		Lines: uint32(len(offsets)),
		Flags: lro.defCfg.toHandleFlags(),
	}
	copy(hr.Consumer[:len(hr.Consumer)-1], lro.consumer)
	// copy(hr.Offsets[:], offsets) - with cast
	for i, o := range offsets {
		hr.Offsets[i] = uint32(o)
	}
	for idx, offset := range lro.offsets {
		hr.DefaultValues[idx] = uint8(lro.values[offset])
	}
	err := uapi.GetLineHandle(c.f.Fd(), &hr)
	if err != nil {
		return 0, err
	}
	return uintptr(hr.Fd), nil
}

// UapiAbiVersion returns the version of the GPIO uAPI the chip is using.
func (c *Chip) UapiAbiVersion() int {
	return c.options.abi
}

type baseLine struct {
	offsets []int
	vfd     uintptr
	isEvent bool
	chip    string
	abi     int
	// mu covers all that follow - those above are immutable
	mu      sync.Mutex
	values  map[int]int
	defCfg  LineConfig
	lineCfg map[int]*LineConfig
	info    []*LineInfo
	closed  bool
	watcher io.Closer
}

// UapiAbiVersion returns the version of the GPIO uAPI the line is using.
func (l *baseLine) UapiAbiVersion() int {
	return l.abi
}

// Chip returns the name of the chip from which the line was requested.
func (l *baseLine) Chip() string {
	return l.chip
}

// Close releases all resources held by the requested line.
//
// Note that this includes waiting for any running event handler to return.
// As a consequence the Close must not be called from the context of the event
// handler - the Close should be called from a different goroutine.
func (l *baseLine) Close() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		return ErrClosed
	}
	l.closed = true
	if l.watcher != nil {
		l.watcher.Close()
	}
	if !l.isEvent { // isEvent => v1 => closed by watcher
		unix.Close(int(l.vfd))
	}
	return nil
}

// Reconfigure updates the configuration of the requested line(s).
//
// Configuration for options other than those passed in remain unchanged.
//
// Not valid for lines with edge detection enabled.
//
// Requires Linux v5.5 or later.
func (l *baseLine) Reconfigure(options ...LineConfigOption) error {
	if l.isEvent {
		return unix.EINVAL
	}
	if len(options) == 0 {
		return nil
	}
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		return ErrClosed
	}
	lro := lineReqOptions{
		lineConfigOptions: lineConfigOptions{
			offsets: l.offsets,
			values:  l.values,
			defCfg:  l.defCfg,
			lineCfg: l.lineCfg,
		},
	}
	for _, option := range options {
		option.applyLineConfigOption(&lro.lineConfigOptions)
	}
	if l.abi == 1 {
		err := lro.defCfg.v1Validate()
		if err != nil {
			return err
		}
		hc := uapi.HandleConfig{Flags: lro.defCfg.toHandleFlags()}
		for idx, offset := range lro.offsets {
			hc.DefaultValues[idx] = uint8(lro.values[offset])
		}
		err = uapi.SetLineConfig(l.vfd, &hc)
		if err == nil {
			l.defCfg = lro.defCfg
		}
		return err
	}
	config, err := lro.toULineConfig()
	if err != nil {
		return err
	}
	err = uapi.SetLineConfigV2(l.vfd, &config)
	if err == nil {
		l.defCfg = lro.defCfg
		l.lineCfg = lro.lineCfg
	}
	return err
}

// Line represents a single requested line.
type Line struct {
	baseLine
}

// Offset returns the offset of the line within the chip.
func (l *Line) Offset() int {
	return l.offsets[0]
}

// Info returns the information about the line.
func (l *Line) Info() (info LineInfo, err error) {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		err = ErrClosed
		return
	}
	if l.info != nil {
		info = *l.info[0]
		return
	}
	c, err := NewChip(l.chip, WithABIVersion(l.abi))
	if err != nil {
		return
	}
	defer c.Close()
	inf, err := c.LineInfo(l.offsets[0])
	if err != nil {
		return
	}
	l.info = []*LineInfo{&inf}
	info = *l.info[0]
	return
}

// Value returns the current value (active state) of the line.
func (l *Line) Value() (int, error) {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		return 0, ErrClosed
	}
	if l.abi == 1 {
		hd := uapi.HandleData{}
		err := uapi.GetLineValues(l.vfd, &hd)
		return int(hd[0]), err
	}
	lv := uapi.LineValues{Mask: 1}
	err := uapi.GetLineValuesV2(l.vfd, &lv)
	return lv.Get(0), err
}

// SetValue sets the current value (active state) of the line.
//
// Only valid for output lines.
func (l *Line) SetValue(value int) error {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.defCfg.Direction != LineDirectionOutput {
		return ErrPermissionDenied
	}
	if l.closed {
		return ErrClosed
	}
	if l.abi == 1 {
		hd := uapi.HandleData{}
		hd[0] = uint8(value)
		err := uapi.SetLineValues(l.vfd, hd)
		if err == nil {
			l.values[l.offsets[0]] = value
		}
		return err
	}
	lsv := uapi.LineValues{
		Mask: 1,
		Bits: uapi.NewLineBitmap(value),
	}
	err := uapi.SetLineValuesV2(l.vfd, lsv)
	if err == nil {
		l.values[l.offsets[0]] = value
	}
	return err
}

// Lines represents a collection of requested lines.
type Lines struct {
	baseLine
}

// Offsets returns the offsets of the lines within the chip.
func (l *Lines) Offsets() []int {
	return l.offsets
}

// Info returns the information about the lines.
func (l *Lines) Info() ([]*LineInfo, error) {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		return nil, ErrClosed
	}
	if l.info != nil {
		return l.info, nil
	}
	c, err := NewChip(l.chip, WithABIVersion(l.abi))
	if err != nil {
		return nil, err
	}
	defer c.Close()
	info := make([]*LineInfo, len(l.offsets))
	for i, o := range l.offsets {
		inf, err := c.LineInfo(o)
		if err != nil {
			return nil, err
		}
		info[i] = &inf
	}
	l.info = info
	return l.info, nil
}

// Values returns the current values (active state) of the collection of lines.
//
// Gets as many values from the set, in order, as can be fit in values, up to
// the full set.
func (l *Lines) Values(values []int) error {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closed {
		return ErrClosed
	}
	lines := len(values)
	if lines > len(l.offsets) {
		lines = len(l.offsets)
	}
	if l.abi == 1 {
		hd := uapi.HandleData{}
		err := uapi.GetLineValues(l.vfd, &hd)
		if err != nil {
			return err
		}
		for i := 0; i < lines; i++ {
			values[i] = int(hd[i])
		}
		return nil
	}
	lv := uapi.LineValues{Mask: uapi.NewLineBitMask(lines)}
	err := uapi.GetLineValuesV2(l.vfd, &lv)
	if err != nil {
		return err
	}
	for i := 0; i < lines; i++ {
		values[i] = lv.Get(i)
	}
	return nil
}

// SetValues sets the current active state of the collection of lines.
//
// Only valid for output lines.
//
// All lines in the set are set at once.  If insufficient values are provided
// then the remaining lines are set to inactive. If too many values are provided
// then the surplus values are ignored.
func (l *Lines) SetValues(values []int) error {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.defCfg.Direction != LineDirectionOutput {
		return ErrPermissionDenied
	}
	if l.closed {
		return ErrClosed
	}
	if len(values) > len(l.offsets) {
		values = values[:len(l.offsets)]
	}
	if l.abi == 1 {
		hd := uapi.HandleData{}
		for i, v := range values {
			hd[i] = uint8(v)
		}
		err := uapi.SetLineValues(l.vfd, hd)
		if err == nil {
			for i, v := range values {
				l.values[l.offsets[i]] = v
			}
		}
		return err
	}
	lv := uapi.LineValues{
		Mask: uapi.NewLineBitMask(len(l.offsets)),
		Bits: uapi.NewLineBitmap(values...),
	}
	err := uapi.SetLineValuesV2(l.vfd, lv)
	if err == nil {
		for i, v := range values {
			l.values[l.offsets[i]] = v
		}
	}

	return err
}

// LineEventType indicates the type of change to the line active state.
//
// Note that for active low lines a low line level results in a high active
// state.
type LineEventType int

const (
	_ LineEventType = iota
	// LineEventRisingEdge indicates an inactive to active event.
	LineEventRisingEdge

	// LineEventFallingEdge indicates an active to inactive event.
	LineEventFallingEdge
)

// LineEvent represents a change in the state of a line.
type LineEvent struct {
	// The line offset within the GPIO chip.
	Offset int

	// Timestamp indicates the time the event was detected.
	//
	// The timestamp is intended for accurately measuring intervals between
	// events. It is not guaranteed to be based on a particular clock. It has
	// been based on CLOCK_REALTIME, but from Linux v5.7 it is based on
	// CLOCK_MONOTONIC.
	Timestamp time.Duration

	// The type of state change event this structure represents.
	Type LineEventType

	// The seqno for this event in all events on all lines in this line request.
	//
	// Requires uAPI v2.
	Seqno uint32

	// The seqno for this event in all events in this line.
	//
	// Requires uAPI v2.
	LineSeqno uint32
}

// LineInfoChangeEvent represents a change in the info a line.
type LineInfoChangeEvent struct {
	// Info is the updated line info.
	Info LineInfo

	// Timestamp indicates the time the event was detected.
	//
	// The timestamp is intended for accurately measuring intervals between
	// events. It is not guaranteed to be based on a particular clock, but from
	// Linux v5.7 it is based on CLOCK_MONOTONIC.
	Timestamp time.Duration

	// The type of info change event this structure represents.
	Type LineInfoChangeType
}

// LineInfoChangeType indicates the type of change to the line info.
type LineInfoChangeType int

const (
	_ LineInfoChangeType = iota

	// LineRequested indicates the line has been requested.
	LineRequested

	// LineReleased indicates the line has been released.
	LineReleased

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

// InfoChangeHandler is a receiver for line info change events.
type InfoChangeHandler func(LineInfoChangeEvent)

// IsChip checks if the named device is an accessible GPIO character device.
//
// Returns an error if not.
func IsChip(name string) error {
	path := nameToPath(name)
	fi, err := os.Lstat(path)
	if err != nil {
		return err
	}
	if fi.Mode()&os.ModeCharDevice == 0 {
		return ErrNotCharacterDevice
	}
	sysfspath := fmt.Sprintf("/sys/bus/gpio/devices/%s/dev", fi.Name())
	if err = unix.Access(sysfspath, unix.R_OK); err != nil {
		return ErrNotCharacterDevice
	}
	sysfsf, err := os.Open(sysfspath)
	if err != nil {
		// changed since Access?
		return ErrNotCharacterDevice
	}
	var sysfsdev [16]byte
	n, err := sysfsf.Read(sysfsdev[:])
	sysfsf.Close()
	if err != nil || n <= 0 {
		return ErrNotCharacterDevice
	}
	var stat unix.Stat_t
	if err = unix.Lstat(path, &stat); err != nil {
		return err
	}
	devstr := fmt.Sprintf("%d:%d", unix.Major(uint64(stat.Rdev)), unix.Minor(uint64(stat.Rdev)))
	sysstr := string(sysfsdev[:n-1])
	if devstr != sysstr {
		return ErrNotCharacterDevice
	}
	return nil
}

// chipNames returns the name of potential gpiochips.
//
// Does not open them or check if they are valid.
func chipNames() []string {
	ee, err := ioutil.ReadDir("/dev")
	if err != nil {
		return nil
	}
	cc := []string(nil)
	for _, e := range ee {
		name := e.Name()
		if strings.HasPrefix(name, "gpiochip") {
			cc = append(cc, name)
		}
	}
	return cc
}

func nameToPath(name string) string {
	if strings.HasPrefix(name, "/dev/") {
		return name
	}
	return "/dev/" + name
}

var (
	// ErrClosed indicates the chip or line has already been closed.
	ErrClosed = errors.New("already closed")

	// ErrConfigOverflow indicates the provided configuration is too complicated
	// to be mapped to the kernel uAPI.
	//
	// Reduce the number of line options or split the request into multiple
	// requests for smaller sets of lines.
	ErrConfigOverflow = errors.New("configuration too complex to map to kernel uAPI")

	// ErrInvalidOffset indicates a line offset is invalid.
	ErrInvalidOffset = errors.New("invalid offset")

	// ErrNotCharacterDevice indicates the device is not a character device.
	ErrNotCharacterDevice = errors.New("not a character device")

	// ErrPermissionDenied indicates caller does not have required permissions
	// for the operation.
	ErrPermissionDenied = errors.New("permission denied")
)

// ErrUapiIncompatibility indicates the feature is not supported by the given
// kernel uAPI version.
type ErrUapiIncompatibility struct {
	Feature    string
	AbiVersion int
}

func (e ErrUapiIncompatibility) Error() string {
	return fmt.Sprintf("%s not available in kernel GPIO uAPI v%d", e.Feature, e.AbiVersion)
}
