// Copyright (c) 2018-2022 Omar Al-Ithawi and The MathJax Consortium
// SPDX-License-Identifier: Apache-2.0
// Go translation and modification of MathJax 3.2.2 ColorUtil.ts.

package color

import (
	
	
	
	
	
)

var (
	decimalPattern = regexp.MustCompile(`^(\d+(\.\d*)?|\.\d+)$`)
	integerPattern = regexp.MustCompile(`^\d+$`)
)

// Error is the color extension's source-compatible TeX error. ID is the
// localization key used by MathJax 3.2.2 and Message is its formatted English
// fallback.
type Error struct {
	ID      string
	Message string
}

func ( *Error) () string { return .Message }

func texError(,  string,  ...any) error {
	return &Error{ID: , Message: fmt.Sprintf(, ...)}
}

// Model stores user-defined colors for one parser. A Model must not be shared
// between parser instances; this preserves MathJax's packageData isolation.
// The zero value is ready for use.
type Model struct {
	userColors map[string]string
}

// NewModel creates an empty, parser-local color model.
func () *Model { return &Model{userColors: make(map[string]string)} }

// GetColor resolves def according to model. An empty model has MathJax's
// special named-color semantics.
func ( *Model) (,  string) (string, error) {
	if  == "" ||  == "named" {
		return .getColorByName(), nil
	}
	return normalizeColor(, )
}

// DefineColor creates or replaces one parser-local named color.
func ( *Model) (, ,  string) error {
	,  := normalizeColor(, )
	if  != nil {
		return 
	}
	if .userColors == nil {
		.userColors = make(map[string]string)
	}
	.userColors[] = 
	return nil
}

func ( *Model) ( string) string {
	if  != nil && .userColors != nil {
		if ,  := .userColors[];  {
			return 
		}
	}
	if ,  := NamedColors[];  {
		return 
	}
	// MathJax v2 compatibility: unknown values pass through to CSS. This also
	// permits CSS literals such as #ff0 and browser-known color names.
	return 
}

func normalizeColor(,  string) (string, error) {
	if  == "" ||  == "named" {
		return , nil
	}
	switch  {
	case "rgb":
		return normalizeRGBDecimal()
	case "RGB":
		return normalizeRGBInteger()
	case "gray":
		return normalizeGray()
	default:
		return "", texError("UndefinedColorModel", "Color model '%s' not defined", )
	}
}

func normalizeRGBDecimal( string) (string, error) {
	 := splitComponents()
	if len() != 3 {
		return "", texError("ModelArg1", "Color values for the %s model require 3 numbers", "rgb")
	}
	var  strings.Builder
	.WriteByte('#')
	for ,  := range  {
		if !decimalPattern.MatchString() {
			return "", texError("InvalidDecimalNumber", "Invalid decimal number")
		}
		,  := strconv.ParseFloat(, 64)
		if  < 0 ||  > 1 {
			return "", texError("ModelArg2", "Color values for the %s model must be between %s and %s", "rgb", "0", "1")
		}
		writeHexByte(&, int(math.Floor(*255)))
	}
	return .String(), nil
}

func normalizeRGBInteger( string) (string, error) {
	 := splitComponents()
	if len() != 3 {
		return "", texError("ModelArg1", "Color values for the %s model require 3 numbers", "RGB")
	}
	var  strings.Builder
	.WriteByte('#')
	for ,  := range  {
		if !integerPattern.MatchString() {
			return "", texError("InvalidNumber", "Invalid number")
		}
		// Avoid machine-integer overflow while retaining parseInt's behavior for
		// arbitrarily many leading zeroes.
		 := strings.TrimLeft(, "0")
		if  == "" {
			 = "0"
		}
		if len() > 3 {
			return "", texError("ModelArg2", "Color values for the %s model must be between %s and %s", "RGB", "0", "255")
		}
		,  := strconv.Atoi()
		if  > 255 {
			return "", texError("ModelArg2", "Color values for the %s model must be between %s and %s", "RGB", "0", "255")
		}
		writeHexByte(&, )
	}
	return .String(), nil
}

func normalizeGray( string) (string, error) {
	 = trimECMAScriptSpace()
	if !decimalPattern.MatchString() {
		return "", texError("InvalidDecimalNumber", "Invalid decimal number")
	}
	,  := strconv.ParseFloat(, 64)
	if  < 0 ||  > 1 {
		return "", texError("ModelArg2", "Color values for the %s model must be between %s and %s", "gray", "0", "1")
	}
	 := int(math.Floor( * 255))
	return fmt.Sprintf("#%02x%02x%02x", , , ), nil
}

func splitComponents( string) []string {
	 := strings.Split(trimECMAScriptSpace(), ",")
	for  := range  {
		[] = trimECMAScriptSpace([])
	}
	return 
}

// trimECMAScriptSpace matches the WhiteSpace and LineTerminator code points
// recognized by JavaScript's String.trim() and regular-expression \s in the
// MathJax 3.2.2 implementation. In particular, Go's strings.TrimSpace does not
// include U+FEFF.
func trimECMAScriptSpace( string) string {
	return strings.TrimFunc(, func( rune) bool {
		switch  {
		case '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u0020',
			'\u00A0', '\u1680', '\u2028', '\u2029', '\u202F', '\u205F',
			'\u3000', '\uFEFF':
			return true
		default:
			return  >= '\u2000' &&  <= '\u200A'
		}
	})
}

func writeHexByte( *strings.Builder,  int) {
	fmt.Fprintf(, "%02x", )
}