// Copyright 2021 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 ftdi

import (
	"context"
	"errors"
	"fmt"
	"io"

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

//

// bitMode is used by SetBitMode to change the chip behavior.
type bitMode uint8

const (
	// Resets all Pins to their default value
	bitModeReset bitMode = 0x00
	// Sets the DBus to asynchronous bit-bang.
	bitModeAsyncBitbang bitMode = 0x01
	// Switch to MPSSE mode (FT2232, FT2232H, FT4232H and FT232H).
	bitModeMpsse bitMode = 0x02
	// Sets the DBus to synchronous bit-bang (FT232R, FT245R, FT2232, FT2232H,
	// FT4232H and FT232H).
	bitModeSyncBitbang bitMode = 0x04
	// Switch to MCU host bus emulation (FT2232, FT2232H, FT4232H and FT232H).
	bitModeMcuHost bitMode = 0x08
	// Switch to fast opto-isolated serial mode (FT2232, FT2232H, FT4232H and
	// FT232H).
	bitModeFastSerial bitMode = 0x10
	// Sets the CBus in 4 bits bit-bang mode (FT232R and FT232H)
	// In this case, upper nibble controls which pin is output/input, lower
	// controls which of outputs are high and low.
	bitModeCbusBitbang bitMode = 0x20
	// Single Channel Synchronous 245 FIFO mode (FT2232H and FT232H).
	bitModeSyncFifo bitMode = 0x40
)

// numDevices returns the number of detected devices.
func numDevices() (int, error) {
	num, e := d2xx.CreateDeviceInfoList()
	if e != 0 {
		return 0, toErr("GetNumDevices initialization failed", e)
	}
	return num, nil
}

func openHandle(opener func(i int) (d2xx.Handle, d2xx.Err), i int) (*handle, error) {
	h, e := opener(i)
	if e != 0 {
		return nil, toErr("Open", e)
	}
	d := &handle{h: h}
	t, vid, did, e := h.GetDeviceInfo()
	if e != 0 {
		_ = d.Close()
		return nil, toErr("GetDeviceInfo", e)
	}
	d.t = DevType(t)
	d.venID = vid
	d.devID = did
	return d, nil
}

// handle is a thin wrapper around the low level d2xx device handle to make it
// more go-idiomatic.
//
// It also implements many utility functions to help with initialization and
// device management.
type handle struct {
	// It is just above 'handle' which directly maps to D2XX function calls.
	//
	// Dev converts the int error type into Go native error and handles higher
	// level functionality like reading and writing to the USB connection.
	//
	// The content of the struct is immutable after initialization.
	h     d2xx.Handle
	t     DevType
	venID uint16
	devID uint16
}

func (h *handle) Close() error {
	// Not yet called.
	return toErr("Close", h.h.Close())
}

// Init is the general setup for common devices.
//
// It tries first the 'happy path' which doesn't reset the device. By doing so,
// the goal is to reduce the amount of glitches on the GPIO pins, on a best
// effort basis. On all devices, the GPIOs are still reset as inputs, since
// there is no way to determine if each GPIO is an input or output.
func (h *handle) Init() error {
	// Driver: maximum packet size. Note that this clears any data in the buffer,
	// so it is good to do it immediately after a reset. The 'out' parameter is
	// ignored.
	// TODO(maruel): The FT232H doc claims a 512 byte packets support in hi-speed
	// mode, which means that this would likely be better to use this value.
	if e := h.h.SetUSBParameters(65536, 0); e != 0 {
		return toErr("SetUSBParameters", e)
	}
	// Driver: Set I/O timeouts to 15 sec. The reason is that we want the
	// timeouts to be very visible, at least as the driver is being developed.
	if e := h.h.SetTimeouts(15000, 15000); e != 0 {
		return toErr("SetTimeouts", e)
	}
	// Not sure: Disable event/error characters.
	if e := h.h.SetChars(0, false, 0, false); e != 0 {
		return toErr("SetChars", e)
	}
	// Not sure: Latency timer at 1ms.
	if e := h.h.SetLatencyTimer(1); e != 0 {
		return toErr("SetLatencyTimer", e)
	}
	// Not sure: Turn on flow control to synchronize IN requests.
	if e := h.h.SetFlowControl(); e != 0 {
		return toErr("SetFlowControl", e)
	}
	// Just in case. It's a very small cost.
	return h.Flush()
}

// Reset resets the device.
func (h *handle) Reset() error {
	if e := h.h.ResetDevice(); e != 0 {
		return toErr("Reset", e)
	}
	if err := h.SetBitMode(0, bitModeReset); err != nil {
		return err
	}
	// USB/driver: Flush any pending read buffer that had been sent by the device
	// before it reset. Do not return any error there, as the device may spew a
	// read error right after being initialized.
	_ = h.Flush()
	return nil
}

// GetBitMode returns the current bit mode.
//
// This is device-dependent.
func (h *handle) GetBitMode() (byte, error) {
	l, e := h.h.GetBitMode()
	if e != 0 {
		return 0, toErr("GetBitMode", e)
	}
	return l, nil
}

// SetBitMode change the mode of operation of the device.
//
// mask sets which pins are inputs and outputs for bitModeCbusBitbang.
func (h *handle) SetBitMode(mask byte, mode bitMode) error {
	return toErr("SetBitMode", h.h.SetBitMode(mask, byte(mode)))
}

// Flush flushes any data left in the read buffer.
func (h *handle) Flush() error {
	var buf [128]byte
	for {
		p, err := h.Read(buf[:])
		if err != nil {
			return err
		}
		if p == 0 {
			return nil
		}
	}
}

// Read returns as much as available in the read buffer without blocking.
func (h *handle) Read(b []byte) (int, error) {
	// GetQueueStatus() 60µs is relatively slow compared to Read() 4µs,
	// but surprisingly if GetQueueStatus() is *not* called, Read()
	// becomes largely slower (800µs).
	//
	// TODO(maruel): This asks for more perf testing before settling on the best
	// solution.
	// TODO(maruel): Investigate FT_GetStatus().
	p, e := h.h.GetQueueStatus()
	if p == 0 || e != 0 {
		return int(p), toErr("Read/GetQueueStatus", e)
	}
	v := int(p)
	if v > len(b) {
		v = len(b)
	}
	n, e := h.h.Read(b[:v])
	return n, toErr("Read", e)
}

// ReadAll blocks to return all the data.
//
// Similar to ioutil.ReadAll() except that it will stop if the context is
// canceled.
func (h *handle) ReadAll(ctx context.Context, b []byte) (int, error) {
	// TODO(maruel): Use FT_SetEventNotification() instead of looping when
	// waiting for bytes.
	for offset := 0; offset != len(b); {
		if ctx.Err() != nil {
			return offset, io.EOF
		}
		chunk := len(b) - offset
		if chunk > 4096 {
			chunk = 4096
		}
		n, err := h.Read(b[offset : offset+chunk])
		if offset += n; err != nil {
			return offset, err
		}
	}
	return len(b), nil
}

// WriteFast writes to the USB device.
//
// In practice this takes at least 0.1ms, which limits the effective rate.
//
// There's no guarantee that the data is all written, so it is important to
// check the return value.
func (h *handle) WriteFast(b []byte) (int, error) {
	n, e := h.h.Write(b)
	return n, toErr("Write", e)
}

// Write blocks until all data is written.
func (h *handle) Write(b []byte) (int, error) {
	for offset := 0; offset != len(b); {
		chunk := len(b) - offset
		if chunk > 4096 {
			chunk = 4096
		}
		p, err := h.WriteFast(b[offset : offset+chunk])
		if err != nil {
			return offset + p, err
		}
		if p != 0 {
			offset += p
		}
	}
	return len(b), nil
}

// ReadEEPROM reads the EEPROM.
func (h *handle) ReadEEPROM(ee *EEPROM) error {
	// The raw data size must be exactly what the device contains.
	eepromSize := h.t.EEPROMSize()
	if len(ee.Raw) < eepromSize {
		ee.Raw = make([]byte, eepromSize)
	} else if len(ee.Raw) > eepromSize {
		ee.Raw = ee.Raw[:eepromSize]
	}
	ee2 := d2xx.EEPROM{Raw: ee.Raw}
	e := h.h.EEPROMRead(uint32(h.t), &ee2)
	ee.Manufacturer = ee2.Manufacturer
	ee.ManufacturerID = ee2.ManufacturerID
	ee.Desc = ee2.Desc
	ee.Serial = ee2.Serial
	if e != 0 {
		// 15 == FT_EEPROM_NOT_PROGRAMMED
		if e != 15 {
			return toErr("EEPROMRead", e)
		}
		// It's a fresh new device. Devices bought via Adafruit already have
		// their EEPROM programmed with Adafruit branding but fake devices sold by
		// CJMCU are not. Since GetDeviceInfo() above succeeded, we know the
		// device type via the USB descriptor, which is sufficient to load the
		// driver, which permits to program the EEPROM to "bootstrap" it.
		//
		// Fill it with an empty yet valid EEPROM content. We don't want to set
		// VenID or DevID to 0! Nobody would do that, right?
		ee.Raw = make([]byte, h.t.EEPROMSize())
		hdr := ee.AsHeader()
		hdr.DeviceType = h.t
		hdr.VendorID = h.venID
		hdr.ProductID = h.devID
	}
	return nil
}

// WriteEEPROM programs the EEPROM.
func (h *handle) WriteEEPROM(ee *EEPROM) error {
	if err := ee.Validate(); err != nil {
		return err
	}
	if len(ee.Raw) != 0 {
		hdr := ee.AsHeader()
		if hdr == nil {
			return errors.New("ftdi: unexpected EEPROM header size")
		}
		if hdr.DeviceType != h.t {
			return errors.New("ftdi: unexpected device type set while programming EEPROM")
		}
		if hdr.VendorID != h.venID {
			return errors.New("ftdi: unexpected VenID set while programming EEPROM")
		}
		if hdr.ProductID != h.devID {
			return errors.New("ftdi: unexpected DevID set while programming EEPROM")
		}
	}
	ee2 := d2xx.EEPROM{
		Raw:            ee.Raw,
		Manufacturer:   ee.Manufacturer,
		ManufacturerID: ee.ManufacturerID,
		Desc:           ee.Desc,
		Serial:         ee.Serial,
	}
	return toErr("EEPROMWrite", h.h.EEPROMProgram(&ee2))
}

// EraseEEPROM erases all the EEPROM.
//
// Will fail on FT232R and FT245R.
func (h *handle) EraseEEPROM() error {
	return toErr("EraseEE", h.h.EraseEE())
}

// ReadUA reads the EEPROM user area.
//
// May return nil when there's nothing programmed yet.
func (h *handle) ReadUA() ([]byte, error) {
	size, e := h.h.EEUASize()
	if e != 0 {
		return nil, toErr("EEUASize", e)
	}
	if size == 0 {
		// Happens on uninitialized EEPROM.
		return nil, nil
	}
	b := make([]byte, size)
	if e := h.h.EEUARead(b); e != 0 {
		return nil, toErr("EEUARead", e)
	}
	return b, nil
}

// WriteUA writes to the EEPROM user area.
func (h *handle) WriteUA(ua []byte) error {
	size, e := h.h.EEUASize()
	if e != 0 {
		return toErr("EEUASize", e)
	}
	if size == 0 {
		return errors.New("ftdi: please program EEPROM first")
	}
	if size < len(ua) {
		return fmt.Errorf("ftdi: maximum user area size is %d bytes", size)
	}
	if size != len(ua) {
		b := make([]byte, size)
		copy(b, ua)
		ua = b
	}
	if e := h.h.EEUAWrite(ua); e != 0 {
		return toErr("EEUAWrite", e)
	}
	return nil
}

// SetBaudRate sets the baud rate.
func (h *handle) SetBaudRate(f physic.Frequency) error {
	if f >= physic.GigaHertz {
		return errors.New("ftdi: baud rate too high")
	}
	v := uint32(f / physic.Hertz)
	return toErr("SetBaudRate", h.h.SetBaudRate(v))
}

//

func toErr(s string, e d2xx.Err) error {
	if e == 0 {
		return nil
	}
	return errors.New("ftdi: " + s + ": " + e.String())
}
