// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package xeddata

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Types for XED enum-like constants.
type (
	// OperandSizeMode describes operand size mode (66H prefix).
	OperandSizeMode int

	// AddressSizeMode describes address size mode (67H prefix).
	AddressSizeMode int

	// CPUMode describes availability in certain CPU mode.
	CPUMode int
)

// Possible operand size modes. XED calls it OSZ.
const (
	OpSize16 OperandSizeMode = iota
	OpSize32
	OpSize64
)

// Possible address size modes. XED calls it ASZ.
const (
	AddrSize16 AddressSizeMode = iota
	AddrSize32
	AddrSize64
)

// Possible CPU modes. XED calls it MODE.
const (
	Mode16 CPUMode = iota
	Mode32
	Mode64
)

var sizeStrings = [...]string{"16", "32", "64"}

// sizeString maps size enumeration value to it's string representation.
func sizeString(size int) string {
	// Panic more gracefully than with "index out of range".
	// If client code specified invalid size enumeration,
	// this is programming error that should be fixed, not "handled".
	if size >= len(sizeStrings) {
		panic(fmt.Sprintf("illegal size value: %d", size))
	}
	return sizeStrings[size]
}

// String returns osz bit size string. Panics on illegal enumerations.
func (osz OperandSizeMode) String() string { return sizeString(int(osz)) }

// String returns asz bit size string. Panics on illegal enumerations.
func (asz AddressSizeMode) String() string { return sizeString(int(asz)) }

// Database holds information that is required to
// properly handle XED datafiles.
type Database struct {
	widths map[string]*width // all-widths.txt
	states map[string]string // all-state.txt
	xtypes map[string]*xtype // all-element-types.txt

	// extraWidth is a "all-extra-widths.txt" record.
	//
	// It provides a default mapping from an operand type to a width code.
	//
	// The key is one of three things:
	// - "XED_REG_<name>" for a register (e.g., "XED_REG_EAX")
	// - "<name>()" for a non-terminal (e.g., "GPR32_R()"")
	// - "<name>" for an immediate const (e.g., "AGEN")
	extraWidths map[string]string // all-extra-widths.txt
}

// width is a "all-widths.txt" record.
type width struct {
	// Default xtype name (examples: int, i8, f32).
	xtype string

	// 16, 32 and 64 bit sizes (all may have same value).
	sizes [3]string
}

// xtype is a "all-element-type.txt" record.
type xtype struct {
	// Name is xtype identifier.
	name string

	// baseType specifies xtype base type.
	// See "all-element-type-base.txt".
	baseType string

	// Size is an operand data size in bits.
	size string
}

// NewDatabase returns Database that loads everything
// it can find in xedPath.
// Missing lookup file is not an error, but error during
// parsing of found file is.
//
// Lookup:
//
//	"$xedPath/all-state.txt" => db.LoadStates()
//	"$xedPath/all-widths.txt" => db.LoadWidths()
//	"$xedPath/all-element-types.txt" => db.LoadXtypes()
//
// $xedPath is the interpolated value of function argument.
//
// The call NewDatabase("") is valid and returns empty database.
// Load methods can be used to read lookup files one-by-one.
func NewDatabase(xedPath string) (*Database, error) {
	var db Database

	stat, err := os.Stat(xedPath)
	if err != nil {
		return nil, err
	}
	if !stat.IsDir() {
		return nil, errors.New("xedPath is not directory")
	}

	states, err := os.Open(filepath.Join(xedPath, "all-state.txt"))
	if err == nil {
		err = db.LoadStates(states)
		if err != nil {
			return &db, err
		}
	}

	widths, err := os.Open(filepath.Join(xedPath, "all-widths.txt"))
	if err == nil {
		err = db.LoadWidths(widths)
		if err != nil {
			return &db, err
		}
	}

	extraWidths, err := os.Open(filepath.Join(xedPath, "all-extra-widths.txt"))
	if err == nil {
		db.extraWidths, err = parseExtraWidths(extraWidths)
		if err != nil {
			return &db, err
		}
	}

	xtypes, err := os.Open(filepath.Join(xedPath, "all-element-types.txt"))
	if err == nil {
		err = db.LoadXtypes(xtypes)
		if err != nil {
			return &db, err
		}
	}

	return &db, nil
}

// LoadWidths reads XED widths definitions from r and updates db.
// "widths" are 16/32/64 bit mode type sizes.
// See "$XED/obj/dgen/all-widths.txt".
func (db *Database) LoadWidths(r io.Reader) error {
	var err error
	db.widths, err = parseWidths(r)
	return err
}

// LoadStates reads XED states definitions from r and updates db.
// "states" are simple macro substitutions without parameters.
// See "$XED/obj/dgen/all-state.txt".
func (db *Database) LoadStates(r io.Reader) error {
	var err error
	db.states, err = parseStates(r)
	return err
}

// LoadXtypes reads XED xtypes definitions from r and updates db.
// "xtypes" are low-level XED type names.
// See "$XED/obj/dgen/all-element-types.txt".
// See "$XED/obj/dgen/all-element-type-base.txt".
func (db *Database) LoadXtypes(r io.Reader) error {
	var err error
	db.xtypes, err = parseXtypes(r)
	return err
}

// WidthSize translates width string to size string using desired
// SizeMode m. For some widths output is the same for any valid value of m.
//
// The size string may be a decimal number of bytes, like "8". It may of the
// form "%dbits" to indicate a bit width. Or in some cases it's "0" for
// "unusual" registers.
func (db *Database) WidthSize(width string, m OperandSizeMode) string {
	info := db.widths[width]
	if info == nil {
		return ""
	}
	return info.sizes[m]
}

func parseWidths(r io.Reader) (map[string]*width, error) {
	data, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("parse widths: %v", err)
	}

	// Lines have two forms:
	// 1. name xtype size [# comment]
	// 2. name xtype size16, size32, size64 [# comment]
	reLine := regexp.MustCompile(`(^\s*\w+\s+\w+\s+\w+\s+\w+\s+\w+)|(^\s*\w+\s+\w+\s+\w+)`)

	widths := make(map[string]*width, 128)
	for _, l := range bytes.Split(data, []byte("\n")) {
		var name, xtype, size16, size32, size64 string

		if m := reLine.FindSubmatch(l); m != nil {
			var f [][]byte
			if m[1] != nil {
				f = bytes.Fields(m[1])
			} else {
				f = bytes.Fields(m[2])
			}

			name = string(f[0])
			xtype = string(f[1])
			if len(f) > 3 {
				size16 = string(f[2])
				size32 = string(f[3])
				size64 = string(f[4])
			} else {
				size16 = string(f[2])
				size32 = size16
				size64 = size16
			}
		}
		if name != "" {
			widths[name] = &width{
				xtype: xtype,
				sizes: [3]string{size16, size32, size64},
			}
		}
	}

	return widths, nil
}

func parseExtraWidths(r io.Reader) (map[string]string, error) {
	extraWidths := make(map[string]string)
	for line, err := range readLines(r) {
		if err != nil {
			return nil, err
		}
		f := bytes.Fields(line.data)
		if len(f) != 3 {
			return nil, fmt.Errorf("want 3 fields, got %d", len(f))
		}
		switch string(f[0]) {
		default:
			return nil, fmt.Errorf("unknown extra width type %s", f[0])
		case "imm_const":
			extraWidths[string(f[1])] = string(f[2])
		case "reg":
			extraWidths["XED_REG_"+string(f[1])] = string(f[2])
		case "nt":
			extraWidths[string(f[1])+"()"] = string(f[2])
		}
	}
	return extraWidths, nil
}

func parseStates(r io.Reader) (map[string]string, error) {
	data, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("parse states: %v", err)
	}

	// Lines have form of "name ...replacements [# comment]".
	// This regexp captures the name and everything until line end or comment.
	lineRE := regexp.MustCompile(`^\s*(\w+)\s+([^#]+)`)

	states := make(map[string]string, 128)
	for _, l := range strings.Split(string(data), "\n") {
		if m := lineRE.FindStringSubmatch(l); m != nil {
			name, replacements := m[1], m[2]
			states[name] = strings.TrimSpace(replacements)
		}
	}

	return states, nil
}

func parseXtypes(r io.Reader) (map[string]*xtype, error) {
	data, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("parse xtypes: %v", err)
	}

	// Lines have form of "name baseType size [# comment]".
	lineRE := regexp.MustCompile(`^\s*(\w+)\s+(\w+)\s*(\d+)`)

	xtypes := make(map[string]*xtype)
	for _, l := range strings.Split(string(data), "\n") {
		if m := lineRE.FindStringSubmatch(l); m != nil {
			name, baseType, size := m[1], m[2], m[3]
			xtypes[name] = &xtype{
				name:     name,
				baseType: baseType,
				size:     size,
			}
		}
	}

	return xtypes, nil
}
