package vindecoder

import (
	"regexp"
	"strings"
	"unicode"

	"github.com/pkg/errors"
)

const vin_size = 17

const made_in_start = 0
const made_in_size = 2
const manufacturer_start = 0
const manufacturer_size = 3
const manufacturer_small_start = 11
const manufacturer_small_size = 3
const small_manufacturer_indicator_index = 2
const details_start = 3
const details_size = 5
const check_digit_index = 8
const year_index = 9
const assembly_plant_index = 10
const serial_number_start = 11
const serial_number_size = 6
const serial_number_small_start = 14
const serial_number_small_size = 3

type vinRawInfo struct {
	country           string
	manufacturer      string
	details           string
	checkDigit        string
	year              string
	assemblyPlant     string
	serialNumber      string
	smallManufacturer bool
}

// source: https://en.wikipedia.org/wiki/Vehicle_identification_number
var vinPositionWeights = []int{8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2}

// source: https://en.wikipedia.org/wiki/Vehicle_identification_number
var transliterationArray = []int{
	1 /* A */, 2 /* B */, 3, /* C */
	4 /* D */, 5 /* E */, 6, /* F */
	7 /* G */, 8 /* H */, 0, /* I */
	1 /* J */, 2 /* K */, 3, /* L */
	4 /* M */, 5 /* N */, 0, /* O */
	7 /* P */, 0 /* Q */, 9, /* R */
	/*   */ 2 /* S */, 3, /* T */
	4 /* U */, 5 /* V */, 6, /* W */
	7 /* X */, 8 /* Y */, 9, /* Z */
}

// source: https://en.wikipedia.org/wiki/Vehicle_identification_number
var yearOffsetTable = map[byte]int{
	'A': 0,
	'B': 1,
	'C': 2,
	'D': 3,
	'E': 4,
	'F': 5,
	'G': 6,
	'H': 7,
	'J': 8,
	'K': 9,
	'L': 10,
	'M': 11,
	'N': 12,
	'P': 13,
	'R': 14,
	'S': 15,
	'T': 16,
	'V': 17,
	'W': 18,
	'X': 19,
	'Y': 20,
	'1': 21,
	'2': 22,
	'3': 23,
	'4': 24,
	'5': 25,
	'6': 26,
	'7': 27,
	'8': 28,
	'9': 29,
}

func getData(vin string) vinRawInfo {
	// split VIN into fields
	var rawInfo vinRawInfo

	rawInfo.smallManufacturer = (vin[small_manufacturer_indicator_index] == '9')
	rawInfo.country = vin[made_in_start : made_in_start+made_in_size]
	rawInfo.details = vin[details_start : details_start+details_size]
	rawInfo.checkDigit = vin[check_digit_index : check_digit_index+1]
	rawInfo.year = vin[year_index : year_index+1]
	rawInfo.assemblyPlant = vin[assembly_plant_index : assembly_plant_index+1]

	if rawInfo.smallManufacturer {
		rawInfo.manufacturer = vin[manufacturer_start:manufacturer_start+manufacturer_size] + "-" + vin[manufacturer_small_start:manufacturer_small_start+manufacturer_small_size]
		rawInfo.serialNumber = vin[serial_number_small_start : serial_number_small_start+serial_number_small_size]
	} else {
		rawInfo.manufacturer = vin[manufacturer_start : manufacturer_start+manufacturer_size]
		rawInfo.serialNumber = vin[serial_number_start : serial_number_start+serial_number_size]
	}

	return rawInfo
}

func getYear(vin string) int {
	// decode year
	var year = 0

	var yearChar = vin[9]
	var pos7Char = vin[6]

	offset, found := yearOffsetTable[yearChar]
	if found {
		if unicode.IsDigit(rune(pos7Char)) {
			year = 1980 + offset
		} else {
			year = 2010 + offset
		}
	}

	return year
}

func getModel(vin string) string {
	switch vin[3] {
	case '1':
		return "Ocean"
	default:
		return ""
	}
}

const (
	MODEL_OCEAN = "Ocean"
)

func getTrim(vin string) string {
	switch vin[4] {
	case 'S':
		return TRIM_SPORT
	case 'U':
		return TRIM_ULTRA
	case 'E':
		return TRIM_EXTREME
	case 'Z':
		return TRIM_OCEAN_ONE
	default:
		return ""
	}
}

// Equivalent to trim level
const (
	TRIM_SPORT     = "Sport"
	TRIM_ULTRA     = "Ultra"
	TRIM_EXTREME   = "Extreme"
	TRIM_OCEAN_ONE = "Ocean One"
)

func getPowertrainType(vin string) string {
	switch vin[5] {
	case 'A':
		return POWERTRAIN_TYPE_SBP_SM_FWD
	case 'B':
		return POWERTRAIN_TYPE_LBP_DM_AWD
	default:
		return ""
	}
}

const (
	POWERTRAIN_TYPE_SBP_SM_FWD = "SBP/SM/FWD"
	POWERTRAIN_TYPE_LBP_DM_AWD = "LBP/DM/AWD"
)

func getRestraint(vin string) string {
	switch vin[6] {
	case 'U':
		return RESTRAIN_US_SPECS
	case 'E':
		return RESTRAIN_EU_SPECS
	case 'C':
		return RESTRAIN_CN_SPECS
	default:
		return ""
	}
}

const (
	RESTRAIN_US_SPECS = "US Specs"
	RESTRAIN_EU_SPECS = "EU Specs"
	RESTRAIN_CN_SPECS = "CN Specs"
)

func getBodyTypeAndGVWR(vin string) string {
	switch vin[7] {
	case '1':
		return "5-Door MPV, 5-Seater, Class D"
	case '2':
		return "5-Door MPV, 5-Seater, Class E"
	case '3':
		return "5-Door MPV, 7-Seater, Class D"
	case '4':
		return "5-Door MPV, 7-Seater, Class E"
	default:
		return ""
	}
}

func translitChar(character byte) int {
	// numeric digits as their value
	if character >= '0' && character <= '9' {
		return int(character - '0')
	}

	if character >= 'A' && character <= 'Z' {
		var translitAlpha = transliterationArray[character-'A']
		if translitAlpha > 0 {
			return translitAlpha
		}
	}

	return -1
}

func validate(vin string) bool {
	// compare calculated check digit with value found
	calculatedCheckDigit, err := calculateCheckDigit(vin)
	if err != nil {
		return false
	}

	checkDigit := vin[8]
	return (calculatedCheckDigit == checkDigit)
}

func calculateCheckDigit(vin string) (byte, error) {
	// calculate check digit
	if len(vin) != vin_size {
		return '0', errors.New("invalid vin size")
	}

	sum := 0
	for i := range vin {

		value := translitChar(vin[i])
		if value < 0 {
			return '0', errors.New("invalid vin character " + string(vin[i]))
		}

		product := vinPositionWeights[i] * value
		sum += product
	}

	// find the divisor
	calculatedCheckDigit := byte(sum % 11)

	if calculatedCheckDigit == 10 {
		calculatedCheckDigit = 'X'
	} else {
		calculatedCheckDigit = '0' + calculatedCheckDigit
	}

	return calculatedCheckDigit, nil
}

// in north america, last 5 digits must be numeric, here we'll create all numeric digits
// also we'll use natural numeric sort order where 0 is the lowest digit value
func getNextNumber(vinNumber string) (string, error) {
	// cannot process empty string
	if len(vinNumber) == 0 {
		return vinNumber, errors.New("empty vin number")
	}

	nextChars := []byte(vinNumber)
	var i = 0
	for i = len(vinNumber) - 1; i >= 0; i-- {
		num := vinNumber[i]
		// if we encounter a non-numeric SN digit, convert to numeric
		if 'A' <= num && num < 'Z' {
			nextChars[i] = '0'
			break
		}
		if '0' <= num && num < '9' {
			nextChars[i] = num + 1
			break
		}
		nextChars[i] = '0'
	}

	// overflow
	err := (error)(nil)
	if i < 0 {
		err = errors.New("overflow")
	}

	next := string(nextChars)
	return next, err
}

func getNextVin(vin string) (string, error) {
	// get next SN number and mae part of base vin
	data := getData(vin)
	sn := data.serialNumber
	overflow := (error)(nil)
	nextSn, err := getNextNumber(sn)
	if err != nil {
		if err.Error() == "overflow" {
			overflow = err
		} else {
			return "", err
		}
	}

	nextVin := vin[:strings.LastIndex(vin, sn)] + nextSn
	checkDigit, err := calculateCheckDigit(nextVin)
	if err != nil {
		return vin, err
	}

	nextVin = vin[:check_digit_index] + string(checkDigit) + nextVin[check_digit_index+1:]

	return nextVin, overflow
}

func getNextNVins(startVin string, count int) ([]string, error) {
	// check that base vin is valid
	_, err := calculateCheckDigit(startVin)
	if err != nil {
		return nil, err
	}

	if count < 1 {
		return nil, errors.New("count is negative")
	}

	vins := make([]string, count)
	rollover := (error)(nil)
	nextVin, err := getNextVin(startVin)
	for i := 0; i < count; i++ {
		if err != nil {
			if err.Error() == "overflow" {
				rollover = errors.New("rollover")
			} else {
				return nil, err
			}
		}

		vins[i] = nextVin
		nextVin, err = getNextVin(nextVin)
	}

	return vins, rollover
}

type VinInfo struct {
	Manufacturer string
	Country      string
	SerialNumber string
	ModelDetails string
	Model        string
	Trim         string
	Year         int
	Powertrain   string
	Restraint    string
	BodyType     string
	IsValid      bool
}

func decode(vin string) VinInfo {
	// ensure valid vin
	var result VinInfo

	result.IsValid = true

	// simple regex validation
	vs := ValidateVINSimple(vin)
	if !vs {
		result.IsValid = false
		return result
	}

	var data vinRawInfo = getData(vin)
	result.SerialNumber = data.serialNumber
	result.Manufacturer = LookupManufacturerByWmiCode(data.manufacturer)
	result.Country = LookupCountry(data.country)
	result.ModelDetails = data.details

	result.Model = getModel(vin)
	result.Trim = getTrim(vin)
	result.Powertrain = getPowertrainType(vin)
	result.Restraint = getRestraint(vin)
	result.BodyType = getBodyTypeAndGVWR(vin)

	var year = getYear(vin)
	if year >= 1980 {
		result.Year = year
	} else {
		result.IsValid = false
	}

	// checksum digit validation
	if !validate(vin) {
		result.IsValid = false
		return result
	}

	return result
}

// entry point

// VIN should be uppercase
func DecodeVIN(vin string) (info VinInfo, ok bool) {
	vin = strings.ToUpper(vin)
	info = decode(vin)
	return info, info.IsValid
}

func CalculateCheckDigit(vin string) (byte, error) {
	return calculateCheckDigit(vin)
}

func VerifyVinCheckDigit(vin string) bool {
	vin = strings.ToUpper(vin)
	return validate(vin)
}

func NextVIN(startVin string) (string, error) {
	return getNextVin(startVin)
}

func NextNVINs(startVin string, count int) ([]string, error) {
	return getNextNVins(startVin, count)
}

func IsEU(vin string) bool {
	return getRestraint(vin) == "EU Specs"
}

// Pre instantiate vin match regex
var VINSimpleRegexMatch *regexp.Regexp

func init() {
	VINSimpleRegexMatch = regexp.MustCompile(`^[a-hj-npr-zA-HJ-NPR-Z0-9]{17}$`)
}

func ValidateVINSimple(vin string) (matched bool) {
	matched = VINSimpleRegexMatch.Match([]byte(vin))
	return matched
}
