package pciids

import (
	"bufio"
	"bytes"
	_ "embed" // Fallback is the embedded pci.ids db file
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

// token what the Lexer retruns
type token int

const (
	// ILLEGAL a token which the Lexer does not understand
	ILLEGAL token = iota
	// EOF end of file
	EOF
	// WS whitespace
	WS
	// NEWLINE '\n'
	NEWLINE
	// COMMENT '# something'
	COMMENT
	// VENDOR PCI vendor
	VENDOR
	// SUBVENDOR PCI subvendor
	SUBVENDOR
	// DEVICE PCI device
	DEVICE
	// CLASS PCI class
	CLASS
	// SUBCLASS PCI subclass
	SUBCLASS
	// PROGIF PCI programming interface
	PROGIF
)

// literal values from the Lexer
type literal struct {
	ID      string
	name    string
	SubName string
}

// scanner a lexical scanner
type scanner struct {
	r        *bufio.Reader
	isVendor bool
}

// newScanner well a new scanner ...
func newScanner(r io.Reader) *scanner {
	return &scanner{r: bufio.NewReader(r)}
}

// Since the pci.ids is line base we're consuming a whole line rather then only
// a single rune/char
func (s *scanner) readline() []byte {
	ln, err := s.r.ReadBytes('\n')
	if err == io.EOF {
		return []byte{'E', 'O', 'F'}
	}
	if err != nil {
		fmt.Printf("ReadBytes failed with %v", err)
		return []byte{}
	}
	return ln
}

func scanClass(line []byte) (token, literal) {
	class := string(line[1:])
	return CLASS, scanEntry([]byte(class), 2)
}

func scanSubVendor(line []byte) (token, literal) {
	trim0 := strings.TrimSpace(string(line))
	subv := string(trim0[:4])
	trim1 := strings.TrimSpace(trim0[4:])
	subd := string(trim1[:4])
	subn := strings.TrimSpace(trim1[4:])

	return SUBVENDOR, literal{subv, subd, subn}
}

func scanEntry(line []byte, offset uint) literal {
	trim := strings.TrimSpace(string(line))
	id := string(trim[:offset])
	name := strings.TrimSpace(trim[offset:])

	return literal{id, name, ""}
}

func isLeadingOneTab(ln []byte) bool  { return (ln[0] == '\t') && (ln[1] != '\t') }
func isLeadingTwoTabs(ln []byte) bool { return (ln[0] == '\t') && (ln[1] == '\t') }

func isHexDigit(ln []byte) bool  { return (ln[0] >= '0' && ln[0] <= '9') }
func isHexLetter(ln []byte) bool { return (ln[0] >= 'a' && ln[0] <= 'f') }

func isVendor(ln []byte) bool    { return isHexDigit(ln) || isHexLetter(ln) }
func isEOF(ln []byte) bool       { return (ln[0] == 'E' && ln[1] == 'O' && ln[2] == 'F') }
func isComment(ln []byte) bool   { return (ln[0] == '#') }
func isSubVendor(ln []byte) bool { return isLeadingTwoTabs(ln) }
func isDevice(ln []byte) bool    { return isLeadingOneTab(ln) }
func isNewline(ln []byte) bool   { return (ln[0] == '\n') }

// List of known device classes, subclasses and programming interfaces
func isClass(ln []byte) bool    { return (ln[0] == 'C') }
func isProgIf(ln []byte) bool   { return isLeadingTwoTabs(ln) }
func isSubClass(ln []byte) bool { return isLeadingOneTab(ln) }

// unread places the previously read rune back on the reader.
func (s *scanner) unread() { _ = s.r.UnreadRune() }

// scan returns the next token and literal value.
func (s *scanner) scan() (tok token, lit literal) {

	line := s.readline()

	if isEOF(line) {
		return EOF, literal{}
	}

	if isNewline(line) {
		return NEWLINE, literal{ID: string('\n')}
	}

	if isComment(line) {
		return COMMENT, literal{ID: string(line)}
	}

	// vendors
	if isVendor(line) {
		s.isVendor = true
		return VENDOR, scanEntry(line, 4)
	}

	if isSubVendor(line) && s.isVendor {
		return scanSubVendor(line)
	}

	if isDevice(line) && s.isVendor {
		return DEVICE, scanEntry(line, 4)
	}

	// classes
	if isClass(line) {
		s.isVendor = false
		return scanClass(line)
	}
	if isProgIf(line) && !s.isVendor {
		return PROGIF, scanEntry(line, 2)
	}

	if isSubClass(line) && !s.isVendor {
		return SUBCLASS, scanEntry(line, 2)
	}

	return ILLEGAL, literal{ID: string(line)}
}

// parser reads the tokens returned by the Lexer and constructs the AST
type parser struct {
	s   *scanner
	buf struct {
		tok token
		lit literal
		n   int
	}
}

// Various locations of pci.ids for different distributions. These may be more
// up to date then the embedded pci.ids db
var defaultPCIdbPaths = []string{
	"/usr/share/misc/pci.ids",   // Ubuntu
	"/usr/local/share/pci.ids",  // RHEL like with manual update
	"/usr/share/hwdata/pci.ids", // RHEL like
	"/usr/share/pci.ids",        // SUSE
}

// This is a fallback if all of the locations fail
//go:embed default_pci.ids
var defaultPCIdb []byte

// NewDB Parse the PCI DB in its default locations or use the default
// builtin pci.ids db.
func NewDB(opts ...Option) Interface {
	db := &pcidb{}
	for _, opt := range opts {
		opt(db)
	}

	pcidbs := defaultPCIdbPaths
	if db.path != "" {
		pcidbs = append([]string{db.path}, defaultPCIdbPaths...)
	}

	return newParser(pcidbs).parse()
}

// Option defines a function for passing options to the NewDB() call
type Option func(*pcidb)

// WithFilePath provides an Option to set the file path
// for the pciids database used by pciids interface.
// The file path provided takes precedence over all other
// paths.
func WithFilePath(path string) Option {
	return func(db *pcidb) {
		db.path = path
	}
}

// newParser will attempt to read the db pci.ids from well known places or fall
// back to an internal db
func newParser(pcidbs []string) *parser {

	for _, db := range pcidbs {
		file, err := os.ReadFile(db)
		if err != nil {
			continue
		}
		return newParserFromReader(bufio.NewReader(bytes.NewReader(file)))

	}
	// We're using go embed above to have the byte array
	// correctly initialized with the internal shipped db
	// if we cannot find an up to date in the filesystem
	return newParserFromReader(bufio.NewReader(bytes.NewReader(defaultPCIdb)))
}

func newParserFromReader(r *bufio.Reader) *parser {
	return &parser{s: newScanner(r)}
}

func (p *parser) scan() (tok token, lit literal) {

	if p.buf.n != 0 {
		p.buf.n = 0
		return p.buf.tok, p.buf.lit
	}
	tok, lit = p.s.scan()
	p.buf.tok, p.buf.lit = tok, lit
	return
}

func (p *parser) unscan() { p.buf.n = 1 }

var _ Interface = (*pcidb)(nil)

// Interface returns textual description of specific attributes of PCI devices
type Interface interface {
	GetDeviceName(uint16, uint16) (string, error)
	GetClassName(uint32) (string, error)
}

// GetDeviceName return the textual description of the PCI device
func (d *pcidb) GetDeviceName(vendorID uint16, deviceID uint16) (string, error) {
	vendor, ok := d.vendors[vendorID]
	if !ok {
		return "", fmt.Errorf("failed to find vendor with id '%x'", vendorID)
	}

	device, ok := vendor.devices[deviceID]
	if !ok {
		return "", fmt.Errorf("failed to find device with id '%x'", deviceID)
	}

	return device.name, nil
}

// GetClassName resturn the textual description of the PCI device class
func (d *pcidb) GetClassName(classID uint32) (string, error) {
	class, ok := d.classes[classID]
	if !ok {
		return "", fmt.Errorf("failed to find class with id '%x'", classID)
	}
	return class.name, nil
}

// pcidb  The complete set of PCI vendors and  PCI classes
type pcidb struct {
	vendors map[uint16]vendor
	classes map[uint32]class
	path    string
}

// vendor PCI vendors/devices/subVendors/SubDevices
type vendor struct {
	name    string
	devices map[uint16]device
}

// subVendor PCI subVendor
type subVendor struct {
	SubDevices map[uint16]SubDevice
}

// SubDevice PCI SubDevice
type SubDevice struct {
	name string
}

// device PCI device
type device struct {
	name       string
	subVendors map[uint16]subVendor
}

// class PCI classes/subClasses/Programming Interfaces
type class struct {
	name       string
	subClasses map[uint32]subClass
}

// subClass PCI subClass
type subClass struct {
	name    string
	progIfs map[uint8]progIf
}

// progIf PCI Programming Interface
type progIf struct {
	name string
}

// parse parses a PCI IDS entry
func (p *parser) parse() Interface {

	db := &pcidb{
		vendors: map[uint16]vendor{},
		classes: map[uint32]class{},
	}

	// Used for housekeeping, breadcrumb for aggregated types
	var hkVendor vendor
	var hkDevice device

	var hkClass class
	var hkSubClass subClass

	var hkFullID uint32 = 0
	var hkFullName [2]string

	for {
		tok, lit := p.scan()

		// We're ignoring COMMENT, NEWLINE
		// An EOF will break the loop
		if tok == EOF {
			break
		}

		// PCI vendors -------------------------------------------------
		if tok == VENDOR {
			id, _ := strconv.ParseUint(lit.ID, 16, 16)
			db.vendors[uint16(id)] = vendor{
				name:    lit.name,
				devices: map[uint16]device{},
			}
			hkVendor = db.vendors[uint16(id)]
		}

		if tok == DEVICE {
			id, _ := strconv.ParseUint(lit.ID, 16, 16)
			hkVendor.devices[uint16(id)] = device{
				name:       lit.name,
				subVendors: map[uint16]subVendor{},
			}
			hkDevice = hkVendor.devices[uint16(id)]
		}

		if tok == SUBVENDOR {
			id, _ := strconv.ParseUint(lit.ID, 16, 16)
			hkDevice.subVendors[uint16(id)] = subVendor{
				SubDevices: map[uint16]SubDevice{},
			}
			subvendor := hkDevice.subVendors[uint16(id)]
			subid, _ := strconv.ParseUint(lit.name, 16, 16)
			subvendor.SubDevices[uint16(subid)] = SubDevice{
				name: lit.SubName,
			}
		}

		// PCI classes -------------------------------------------------
		if tok == CLASS {
			id, _ := strconv.ParseUint(lit.ID, 16, 32)
			db.classes[uint32(id)] = class{
				name:       lit.name,
				subClasses: map[uint32]subClass{},
			}
			hkClass = db.classes[uint32(id)]

			hkFullID = uint32(id) << 16
			hkFullID = hkFullID & 0xFFFF0000
			hkFullName[0] = fmt.Sprintf("%s (%02x)", lit.name, id)
		}

		if tok == SUBCLASS {
			id, _ := strconv.ParseUint(lit.ID, 16, 8)
			hkClass.subClasses[uint32(id)] = subClass{
				name:    lit.name,
				progIfs: map[uint8]progIf{},
			}
			hkSubClass = hkClass.subClasses[uint32(id)]

			// Clear the last detected sub class
			hkFullID = hkFullID & 0xFFFF0000
			hkFullID = hkFullID | uint32(id)<<8
			// Clear the last detected prog iface
			hkFullID = hkFullID & 0xFFFFFF00
			hkFullName[1] = fmt.Sprintf("%s (%02x)", lit.name, id)

			db.classes[uint32(hkFullID)] = class{
				name: hkFullName[0] + " | " + hkFullName[1],
			}
		}

		if tok == PROGIF {
			id, _ := strconv.ParseUint(lit.ID, 16, 8)
			hkSubClass.progIfs[uint8(id)] = progIf{
				name: lit.name,
			}

			finalID := hkFullID | uint32(id)

			name := fmt.Sprintf("%s (%02x)", lit.name, id)
			finalName := hkFullName[0] + " | " + hkFullName[1] + " | " + name

			db.classes[finalID] = class{
				name: finalName,
			}
		}

		if tok == ILLEGAL {
			fmt.Printf("warning: illegal token %s %s cannot parse PCI IDS, database may be incomplete ", lit.ID, lit.name)
		}
	}
	return db
}
