// 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 pinreg

import (
	"errors"
	"strconv"
	"sync"

	"periph.io/x/conn/v3/gpio"
	"periph.io/x/conn/v3/gpio/gpioreg"
	"periph.io/x/conn/v3/pin"
)

// All contains all the on-board headers on a micro computer.
//
// The map key is the header name, e.g. "P1" or "EULER" and the value is a
// slice of slice of pin.Pin. For a 2x20 header, it's going to be a slice of
// [20][2]pin.Pin.
func All() map[string][][]pin.Pin {
	mu.Lock()
	defer mu.Unlock()
	out := make(map[string][][]pin.Pin, len(allHeaders))
	for k, v := range allHeaders {
		outV := make([][]pin.Pin, len(v))
		for i, w := range v {
			outW := make([]pin.Pin, len(w))
			copy(outW, w)
			outV[i] = outW
		}
		out[k] = outV
	}
	return out
}

// Position returns the position on a pin if found.
//
// The header and the pin number. Pin numbers are 1-based.
//
// Returns "", 0 if not connected.
func Position(p pin.Pin) (string, int) {
	mu.Lock()
	defer mu.Unlock()
	pos := byPin[realPin(p).Name()]
	return pos.name, pos.number
}

// IsConnected returns true if the pin is on a header.
func IsConnected(p pin.Pin) bool {
	_, i := Position(p)
	return i != 0
}

// Register registers a physical header.
//
// It automatically registers all gpio pins to gpioreg.
func Register(name string, allPins [][]pin.Pin) error {
	mu.Lock()
	defer mu.Unlock()
	if _, ok := allHeaders[name]; ok {
		return errors.New("pinreg: header " + strconv.Quote(name) + " was already registered")
	}
	for i, line := range allPins {
		for j, pin := range line {
			if pin == nil || len(pin.Name()) == 0 {
				return errors.New("pinreg: invalid pin on header " + name + "[" + strconv.Itoa(i+1) + "][" + strconv.Itoa(j+1) + "]")
			}
		}
	}
	allHeaders[name] = allPins
	number := 1
	for _, line := range allPins {
		for _, p := range line {
			byPin[realPin(p).Name()] = position{name, number}
			number++
		}
	}

	count := 0
	for _, row := range allPins {
		for _, p := range row {
			count++
			if _, ok := p.(gpio.PinIO); ok {
				if err := gpioreg.RegisterAlias(name+"_"+strconv.Itoa(count), p.Name()); err != nil {
					// Unregister as much as possible.
					_ = unregister(name)
					return errors.New("pinreg: " + err.Error())
				}
			}
		}
	}

	return nil
}

// Unregister removes a previously registered header.
//
// This can happen when an USB device, which exposed an header, is unplugged.
// This is also useful for unit testing.
func Unregister(name string) error {
	mu.Lock()
	defer mu.Unlock()
	return unregister(name)
}

//

type position struct {
	name   string // Header name
	number int    // Pin number
}

var (
	mu         sync.Mutex
	allHeaders = map[string][][]pin.Pin{} // every known headers as per internal lookup table
	byPin      = map[string]position{}    // GPIO pin name to position
)

func unregister(name string) error {
	if hdr, ok := allHeaders[name]; ok {
		var err error
		delete(allHeaders, name)
		count := 0
		for _, row := range hdr {
			for _, p := range row {
				count++
				if _, ok := p.(gpio.PinIO); ok {
					if err1 := gpioreg.Unregister(name + "_" + strconv.Itoa(count)); err1 != nil && err == nil {
						// Continue unregistering as much as possible.
						err = errors.New("pinreg: " + err1.Error())
					}
				}
			}
		}
		return err
	}
	return errors.New("pinreg: can't unregister unknown header name " + strconv.Quote(name))
}

// realPin returns the real pin from an alias.
func realPin(p pin.Pin) pin.Pin {
	if r, ok := p.(gpio.RealPin); ok {
		p = r.Real()
	}
	return p
}
