package js

import (
	"bytes"
	"encoding/hex"
	stdStrconv "strconv"
	"unicode/utf8"

	"github.com/tdewolff/minify/v2"
	"github.com/tdewolff/parse/v2/js"
	"github.com/tdewolff/parse/v2/strconv"
)

var (
	spaceBytes                 = []byte(" ")
	newlineBytes               = []byte("\n")
	starBytes                  = []byte("*")
	plusBytes                  = []byte("+")
	plusPlusBytes              = []byte("++")
	minMinBytes                = []byte("--")
	expBytes                   = []byte("**")
	bitOrBytes                 = []byte("|")
	colonBytes                 = []byte(":")
	semicolonBytes             = []byte(";")
	commaBytes                 = []byte(",")
	dotBytes                   = []byte(".")
	ellipsisBytes              = []byte("...")
	openBraceBytes             = []byte("{")
	closeBraceBytes            = []byte("}")
	openParenBytes             = []byte("(")
	closeParenBytes            = []byte(")")
	openBracketBytes           = []byte("[")
	closeBracketBytes          = []byte("]")
	openParenBracketBytes      = []byte("({")
	closeParenOpenBracketBytes = []byte("){")
	notBytes                   = []byte("!")
	questionBytes              = []byte("?")
	equalBytes                 = []byte("=")
	optChainBytes              = []byte("?.")
	arrowBytes                 = []byte("=>")
	notEqualBytes              = []byte("!=")
	zeroBytes                  = []byte("0")
	oneBytes                   = []byte("1")
	letBytes                   = []byte("let")
	getBytes                   = []byte("get")
	setBytes                   = []byte("set")
	asyncBytes                 = []byte("async")
	functionBytes              = []byte("function")
	staticBytes                = []byte("static")
	ifOpenBytes                = []byte("if(")
	elseBytes                  = []byte("else")
	withOpenBytes              = []byte("with(")
	doBytes                    = []byte("do")
	whileOpenBytes             = []byte("while(")
	forOpenBytes               = []byte("for(")
	forAwaitOpenBytes          = []byte("for await(")
	inBytes                    = []byte("in")
	ofBytes                    = []byte("of")
	switchOpenBytes            = []byte("switch(")
	throwBytes                 = []byte("throw")
	tryBytes                   = []byte("try")
	catchBytes                 = []byte("catch")
	finallyBytes               = []byte("finally")
	importBytes                = []byte("import")
	exportBytes                = []byte("export")
	fromBytes                  = []byte("from")
	returnBytes                = []byte("return")
	classBytes                 = []byte("class")
	asSpaceBytes               = []byte("as ")
	asyncSpaceBytes            = []byte("async ")
	spaceDefaultBytes          = []byte(" default")
	spaceExtendsBytes          = []byte(" extends")
	yieldBytes                 = []byte("yield")
	newBytes                   = []byte("new")
	openNewBytes               = []byte("(new")
	newTargetBytes             = []byte("new.target")
	importMetaBytes            = []byte("import.meta")
	nanBytes                   = []byte("NaN")
	undefinedBytes             = []byte("undefined")
	infinityBytes              = []byte("Infinity")
	nullBytes                  = []byte("null")
	zeroIndexBytes             = []byte("0[0]")
	groupedZeroIndexBytes      = []byte("(0[0])")
	oneDivZeroBytes            = []byte("1/0")
	groupedOneDivZeroBytes     = []byte("(1/0)")
	notZeroBytes               = []byte("!0")
	groupedNotZeroBytes        = []byte("(!0)")
	notOneBytes                = []byte("!1")
	groupedNotOneBytes         = []byte("(!1)")
	debuggerBytes              = []byte("debugger")
	regExpScriptBytes          = []byte("/script>")
	isNaNBytes                 = []byte("isNaN")
	NumberBytes                = []byte("Number")
	MathBytes                  = []byte("Math")
)

func isEmptyStmt(stmt js.IStmt) bool {
	if stmt == nil {
		return true
	} else if _, ok := stmt.(*js.EmptyStmt); ok {
		return true
	} else if block, ok := stmt.(*js.BlockStmt); ok {
		for _, item := range block.List {
			if ok := isEmptyStmt(item); !ok {
				return false
			}
		}
		return true
	}
	return false
}

func isFlowStmt(stmt js.IStmt) bool {
	if _, ok := stmt.(*js.ReturnStmt); ok {
		return true
	} else if _, ok := stmt.(*js.ThrowStmt); ok {
		return true
	} else if _, ok := stmt.(*js.BranchStmt); ok {
		return true
	}
	return false
}

func lastStmt(stmt js.IStmt) js.IStmt {
	if block, ok := stmt.(*js.BlockStmt); ok && 0 < len(block.List) {
		return lastStmt(block.List[len(block.List)-1])
	}
	return stmt
}

func endsInIf(istmt js.IStmt) bool {
	switch stmt := istmt.(type) {
	case *js.IfStmt:
		if stmt.Else == nil {
			_, ok := optimizeStmt(stmt).(*js.IfStmt)
			return ok
		}
		return endsInIf(stmt.Else)
	case *js.BlockStmt:
		if 0 < len(stmt.List) {
			return endsInIf(stmt.List[len(stmt.List)-1])
		}
	case *js.LabelledStmt:
		return endsInIf(stmt.Value)
	case *js.WithStmt:
		return endsInIf(stmt.Body)
	case *js.WhileStmt:
		return endsInIf(stmt.Body)
	case *js.ForStmt:
		return endsInIf(stmt.Body)
	case *js.ForInStmt:
		return endsInIf(stmt.Body)
	case *js.ForOfStmt:
		return endsInIf(stmt.Body)
	}
	return false
}

// precedence maps for the precedence inside the operation
var unaryPrecMap = map[js.TokenType]js.OpPrec{
	js.PostIncrToken: js.OpLHS,
	js.PostDecrToken: js.OpLHS,
	js.PreIncrToken:  js.OpUnary,
	js.PreDecrToken:  js.OpUnary,
	js.NotToken:      js.OpUnary,
	js.BitNotToken:   js.OpUnary,
	js.TypeofToken:   js.OpUnary,
	js.VoidToken:     js.OpUnary,
	js.DeleteToken:   js.OpUnary,
	js.PosToken:      js.OpUnary,
	js.NegToken:      js.OpUnary,
	js.AwaitToken:    js.OpUnary,
}

var binaryLeftPrecMap = map[js.TokenType]js.OpPrec{
	js.EqToken:         js.OpLHS,
	js.MulEqToken:      js.OpLHS,
	js.DivEqToken:      js.OpLHS,
	js.ModEqToken:      js.OpLHS,
	js.ExpEqToken:      js.OpLHS,
	js.AddEqToken:      js.OpLHS,
	js.SubEqToken:      js.OpLHS,
	js.LtLtEqToken:     js.OpLHS,
	js.GtGtEqToken:     js.OpLHS,
	js.GtGtGtEqToken:   js.OpLHS,
	js.BitAndEqToken:   js.OpLHS,
	js.BitXorEqToken:   js.OpLHS,
	js.BitOrEqToken:    js.OpLHS,
	js.ExpToken:        js.OpUpdate,
	js.MulToken:        js.OpMul,
	js.DivToken:        js.OpMul,
	js.ModToken:        js.OpMul,
	js.AddToken:        js.OpAdd,
	js.SubToken:        js.OpAdd,
	js.LtLtToken:       js.OpShift,
	js.GtGtToken:       js.OpShift,
	js.GtGtGtToken:     js.OpShift,
	js.LtToken:         js.OpCompare,
	js.LtEqToken:       js.OpCompare,
	js.GtToken:         js.OpCompare,
	js.GtEqToken:       js.OpCompare,
	js.InToken:         js.OpCompare,
	js.InstanceofToken: js.OpCompare,
	js.EqEqToken:       js.OpEquals,
	js.NotEqToken:      js.OpEquals,
	js.EqEqEqToken:     js.OpEquals,
	js.NotEqEqToken:    js.OpEquals,
	js.BitAndToken:     js.OpBitAnd,
	js.BitXorToken:     js.OpBitXor,
	js.BitOrToken:      js.OpBitOr,
	js.AndToken:        js.OpAnd,
	js.OrToken:         js.OpOr,
	js.NullishToken:    js.OpBitOr, // or OpCoalesce
	js.CommaToken:      js.OpExpr,
}

var binaryRightPrecMap = map[js.TokenType]js.OpPrec{
	js.EqToken:         js.OpAssign,
	js.MulEqToken:      js.OpAssign,
	js.DivEqToken:      js.OpAssign,
	js.ModEqToken:      js.OpAssign,
	js.ExpEqToken:      js.OpAssign,
	js.AddEqToken:      js.OpAssign,
	js.SubEqToken:      js.OpAssign,
	js.LtLtEqToken:     js.OpAssign,
	js.GtGtEqToken:     js.OpAssign,
	js.GtGtGtEqToken:   js.OpAssign,
	js.BitAndEqToken:   js.OpAssign,
	js.BitXorEqToken:   js.OpAssign,
	js.BitOrEqToken:    js.OpAssign,
	js.ExpToken:        js.OpExp,
	js.MulToken:        js.OpExp,
	js.DivToken:        js.OpExp,
	js.ModToken:        js.OpExp,
	js.AddToken:        js.OpMul,
	js.SubToken:        js.OpMul,
	js.LtLtToken:       js.OpAdd,
	js.GtGtToken:       js.OpAdd,
	js.GtGtGtToken:     js.OpAdd,
	js.LtToken:         js.OpShift,
	js.LtEqToken:       js.OpShift,
	js.GtToken:         js.OpShift,
	js.GtEqToken:       js.OpShift,
	js.InToken:         js.OpShift,
	js.InstanceofToken: js.OpShift,
	js.EqEqToken:       js.OpCompare,
	js.NotEqToken:      js.OpCompare,
	js.EqEqEqToken:     js.OpCompare,
	js.NotEqEqToken:    js.OpCompare,
	js.BitAndToken:     js.OpEquals,
	js.BitXorToken:     js.OpBitAnd,
	js.BitOrToken:      js.OpBitXor,
	js.AndToken:        js.OpAnd,   // changes order in AST but not in execution
	js.OrToken:         js.OpOr,    // changes order in AST but not in execution
	js.NullishToken:    js.OpBitOr, // or OpCoalesce
	js.CommaToken:      js.OpAssign,
}

// precedence maps of the operation itself
var unaryOpPrecMap = map[js.TokenType]js.OpPrec{
	js.PostIncrToken: js.OpUpdate,
	js.PostDecrToken: js.OpUpdate,
	js.PreIncrToken:  js.OpUpdate,
	js.PreDecrToken:  js.OpUpdate,
	js.NotToken:      js.OpUnary,
	js.BitNotToken:   js.OpUnary,
	js.TypeofToken:   js.OpUnary,
	js.VoidToken:     js.OpUnary,
	js.DeleteToken:   js.OpUnary,
	js.PosToken:      js.OpUnary,
	js.NegToken:      js.OpUnary,
	js.AwaitToken:    js.OpUnary,
}

var binaryOpPrecMap = map[js.TokenType]js.OpPrec{
	js.EqToken:         js.OpAssign,
	js.MulEqToken:      js.OpAssign,
	js.DivEqToken:      js.OpAssign,
	js.ModEqToken:      js.OpAssign,
	js.ExpEqToken:      js.OpAssign,
	js.AddEqToken:      js.OpAssign,
	js.SubEqToken:      js.OpAssign,
	js.LtLtEqToken:     js.OpAssign,
	js.GtGtEqToken:     js.OpAssign,
	js.GtGtGtEqToken:   js.OpAssign,
	js.BitAndEqToken:   js.OpAssign,
	js.BitXorEqToken:   js.OpAssign,
	js.BitOrEqToken:    js.OpAssign,
	js.ExpToken:        js.OpExp,
	js.MulToken:        js.OpMul,
	js.DivToken:        js.OpMul,
	js.ModToken:        js.OpMul,
	js.AddToken:        js.OpAdd,
	js.SubToken:        js.OpAdd,
	js.LtLtToken:       js.OpShift,
	js.GtGtToken:       js.OpShift,
	js.GtGtGtToken:     js.OpShift,
	js.LtToken:         js.OpCompare,
	js.LtEqToken:       js.OpCompare,
	js.GtToken:         js.OpCompare,
	js.GtEqToken:       js.OpCompare,
	js.InToken:         js.OpCompare,
	js.InstanceofToken: js.OpCompare,
	js.EqEqToken:       js.OpEquals,
	js.NotEqToken:      js.OpEquals,
	js.EqEqEqToken:     js.OpEquals,
	js.NotEqEqToken:    js.OpEquals,
	js.BitAndToken:     js.OpBitAnd,
	js.BitXorToken:     js.OpBitXor,
	js.BitOrToken:      js.OpBitOr,
	js.AndToken:        js.OpAnd,
	js.OrToken:         js.OpOr,
	js.NullishToken:    js.OpCoalesce,
	js.CommaToken:      js.OpExpr,
}

func exprPrec(i js.IExpr) js.OpPrec {
	switch expr := i.(type) {
	case *js.Var, *js.LiteralExpr, *js.ArrayExpr, *js.ObjectExpr, *js.FuncDecl, *js.ClassDecl:
		return js.OpPrimary
	case *js.UnaryExpr:
		return unaryOpPrecMap[expr.Op]
	case *js.BinaryExpr:
		return binaryOpPrecMap[expr.Op]
	case *js.NewExpr:
		if expr.Args == nil {
			return js.OpNew
		}
		return js.OpMember
	case *js.TemplateExpr:
		if expr.Tag == nil {
			return js.OpPrimary
		}
		return expr.Prec
	case *js.DotExpr:
		return expr.Prec
	case *js.IndexExpr:
		return expr.Prec
	case *js.NewTargetExpr, *js.ImportMetaExpr:
		return js.OpMember
	case *js.CallExpr:
		return js.OpCall
	case *js.CondExpr, *js.YieldExpr, *js.ArrowFunc:
		return js.OpAssign
	case *js.GroupExpr:
		return exprPrec(expr.X)
	}
	return js.OpExpr // CommaExpr
}

func hasSideEffects(i js.IExpr) bool {
	// assume that variable usage and that the index operator themselves have no side effects
	switch expr := i.(type) {
	case *js.Var:
		return true
	case *js.LiteralExpr, *js.FuncDecl, *js.ClassDecl, *js.ArrowFunc, *js.NewTargetExpr, *js.ImportMetaExpr:
		return false
	case *js.NewExpr, *js.CallExpr, *js.YieldExpr:
		return true
	case *js.GroupExpr:
		return hasSideEffects(expr.X)
	case *js.DotExpr:
		return true
	case *js.IndexExpr:
		return true
	case *js.CondExpr:
		return hasSideEffects(expr.Cond) || hasSideEffects(expr.X) || hasSideEffects(expr.Y)
	case *js.CommaExpr:
		for _, item := range expr.List {
			if hasSideEffects(item) {
				return true
			}
		}
	case *js.ArrayExpr:
		for _, item := range expr.List {
			if hasSideEffects(item.Value) {
				return true
			}
		}
		return false
	case *js.ObjectExpr:
		for _, item := range expr.List {
			if hasSideEffects(item.Value) || item.Init != nil && hasSideEffects(item.Init) || item.Name != nil && item.Name.IsComputed() && hasSideEffects(item.Name.Computed) {
				return true
			}
		}
		return false
	case *js.TemplateExpr:
		if hasSideEffects(expr.Tag) {
			return true
		}
		for _, item := range expr.List {
			if hasSideEffects(item.Expr) {
				return true
			}
		}
		return false
	case *js.UnaryExpr:
		if expr.Op == js.DeleteToken || expr.Op == js.PreIncrToken || expr.Op == js.PreDecrToken || expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken {
			return true
		}
		return hasSideEffects(expr.X)
	case *js.BinaryExpr:
		return binaryOpPrecMap[expr.Op] == js.OpAssign
	}
	return true
}

// TODO: use in more cases
func groupExpr(i js.IExpr, prec js.OpPrec) js.IExpr {
	precInside := exprPrec(i)
	if _, ok := i.(*js.GroupExpr); !ok && precInside < prec && (precInside != js.OpCoalesce || prec != js.OpBitOr) {
		return &js.GroupExpr{X: i}
	}
	return i
}

// TODO: use in more cases
func condExpr(cond, x, y js.IExpr) js.IExpr {
	if comma, ok := cond.(*js.CommaExpr); ok {
		comma.List[len(comma.List)-1] = &js.CondExpr{
			Cond: groupExpr(comma.List[len(comma.List)-1], js.OpCoalesce),
			X:    groupExpr(x, js.OpAssign),
			Y:    groupExpr(y, js.OpAssign),
		}
		return comma
	}
	return &js.CondExpr{
		Cond: groupExpr(cond, js.OpCoalesce),
		X:    groupExpr(x, js.OpAssign),
		Y:    groupExpr(y, js.OpAssign),
	}
}

func commaExpr(x, y js.IExpr) js.IExpr {
	comma, ok := x.(*js.CommaExpr)
	if !ok {
		comma = &js.CommaExpr{List: []js.IExpr{x}}
	}
	if comma2, ok := y.(*js.CommaExpr); ok {
		comma.List = append(comma.List, comma2.List...)
	} else {
		comma.List = append(comma.List, y)
	}
	return comma
}

func innerExpr(i js.IExpr) js.IExpr {
	for {
		if group, ok := i.(*js.GroupExpr); ok {
			i = group.X
		} else {
			return i
		}
	}
}

func finalExpr(i js.IExpr) js.IExpr {
	i = innerExpr(i)
	if comma, ok := i.(*js.CommaExpr); ok {
		i = comma.List[len(comma.List)-1]
	}
	if binary, ok := i.(*js.BinaryExpr); ok && binary.Op == js.EqToken {
		i = binary.X // return first
	}
	return i
}

func isTrue(i js.IExpr) bool {
	i = innerExpr(i)
	if lit, ok := i.(*js.LiteralExpr); ok && lit.TokenType == js.TrueToken {
		return true
	} else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.NotToken {
		ret, _ := isFalsy(unary.X)
		return ret
	}
	return false
}

func isFalse(i js.IExpr) bool {
	i = innerExpr(i)
	if lit, ok := i.(*js.LiteralExpr); ok {
		return lit.TokenType == js.FalseToken
	} else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.NotToken {
		ret, _ := isTruthy(unary.X)
		return ret
	}
	return false
}

func isEqualExpr(a, b js.IExpr) bool {
	a = innerExpr(a)
	b = innerExpr(b)
	if left, ok := a.(*js.Var); ok {
		if right, ok := b.(*js.Var); ok {
			return bytes.Equal(left.Name(), right.Name())
		}
	}
	// TODO: use reflect.DeepEqual?
	return false
}

func toNullishExpr(condExpr *js.CondExpr) (js.IExpr, bool) {
	if v, not, ok := isUndefinedOrNullVar(condExpr.Cond); ok {
		left, right := condExpr.X, condExpr.Y
		if not {
			left, right = right, left
		}
		if isEqualExpr(v, right) {
			// convert conditional expression to nullish:  a==null?b:a  =>  a??b
			return &js.BinaryExpr{js.NullishToken, groupExpr(right, binaryLeftPrecMap[js.NullishToken]), groupExpr(left, binaryRightPrecMap[js.NullishToken])}, true
		} else if isUndefined(left) {
			// convert conditional expression to optional expr:  a==null?undefined:a.b  =>  a?.b
			expr := right
			var parent js.IExpr
			for {
				prevExpr := expr
				if callExpr, ok := expr.(*js.CallExpr); ok {
					expr = callExpr.X
				} else if dotExpr, ok := expr.(*js.DotExpr); ok {
					expr = dotExpr.X
				} else if indexExpr, ok := expr.(*js.IndexExpr); ok {
					expr = indexExpr.X
				} else if templateExpr, ok := expr.(*js.TemplateExpr); ok {
					expr = templateExpr.Tag
				} else {
					break
				}
				parent = prevExpr
			}
			if parent != nil && isEqualExpr(v, expr) {
				if callExpr, ok := parent.(*js.CallExpr); ok {
					callExpr.Optional = true
				} else if dotExpr, ok := parent.(*js.DotExpr); ok {
					dotExpr.Optional = true
				} else if indexExpr, ok := parent.(*js.IndexExpr); ok {
					indexExpr.Optional = true
				} else if templateExpr, ok := parent.(*js.TemplateExpr); ok {
					templateExpr.Optional = true
				}
				return right, true
			}
		}
	}
	return nil, false
}

func isUndefinedOrNullVar(i js.IExpr) (*js.Var, bool, bool) {
	i = innerExpr(i)
	if binary, ok := i.(*js.BinaryExpr); ok && (binary.Op == js.OrToken || binary.Op == js.AndToken) {
		eqEqOp := js.EqEqToken
		eqEqEqOp := js.EqEqEqToken
		if binary.Op == js.AndToken {
			eqEqOp = js.NotEqToken
			eqEqEqOp = js.NotEqEqToken
		}

		left, isBinaryX := innerExpr(binary.X).(*js.BinaryExpr)
		right, isBinaryY := innerExpr(binary.Y).(*js.BinaryExpr)
		if isBinaryX && isBinaryY && (left.Op == eqEqOp || left.Op == eqEqEqOp) && (right.Op == eqEqOp || right.Op == eqEqEqOp) {
			var leftVar, rightVar *js.Var
			if v, ok := left.X.(*js.Var); ok && isUndefinedOrNull(left.Y) {
				leftVar = v
			} else if v, ok := left.Y.(*js.Var); ok && isUndefinedOrNull(left.X) {
				leftVar = v
			}
			if v, ok := right.X.(*js.Var); ok && isUndefinedOrNull(right.Y) {
				rightVar = v
			} else if v, ok := right.Y.(*js.Var); ok && isUndefinedOrNull(right.X) {
				rightVar = v
			}
			if leftVar != nil && leftVar == rightVar {
				return leftVar, binary.Op == js.AndToken, true
			}
		}
	} else if ok && (binary.Op == js.EqEqToken || binary.Op == js.NotEqToken) {
		var variable *js.Var
		if v, ok := binary.X.(*js.Var); ok && isUndefinedOrNull(binary.Y) {
			variable = v
		} else if v, ok := binary.Y.(*js.Var); ok && isUndefinedOrNull(binary.X) {
			variable = v
		}
		if variable != nil {
			return variable, binary.Op == js.NotEqToken, true
		}
	}
	return nil, false, false
}

func isUndefinedOrNull(i js.IExpr) bool {
	i = innerExpr(i)
	if lit, ok := i.(*js.LiteralExpr); ok {
		return lit.TokenType == js.NullToken
	}
	return isUndefined(i)
}

func isUndefined(i js.IExpr) bool {
	i = innerExpr(i)
	if v, ok := i.(*js.Var); ok {
		if bytes.Equal(v.Name(), undefinedBytes) { // TODO: only if not defined
			return true
		}
	} else if unary, ok := i.(*js.UnaryExpr); ok && unary.Op == js.VoidToken {
		return !hasSideEffects(unary.X)
	}
	return false
}

// returns whether truthy and whether it could be coerced to a boolean (i.e. when returns (false,true) this means it is falsy)
func isTruthy(i js.IExpr) (bool, bool) {
	if falsy, ok := isFalsy(i); ok {
		return !falsy, true
	}
	return false, false
}

// returns whether falsy and whether it could be coerced to a boolean (i.e. when returns (false,true) this means it is truthy)
func isFalsy(i js.IExpr) (bool, bool) {
	negated := false
	group, isGroup := i.(*js.GroupExpr)
	unary, isUnary := i.(*js.UnaryExpr)
	for isGroup || isUnary && unary.Op == js.NotToken {
		if isGroup {
			i = group.X
		} else {
			i = unary.X
			negated = !negated
		}
		group, isGroup = i.(*js.GroupExpr)
		unary, isUnary = i.(*js.UnaryExpr)
	}
	if lit, ok := i.(*js.LiteralExpr); ok {
		tt := lit.TokenType
		d := lit.Data
		if tt == js.FalseToken || tt == js.NullToken || tt == js.StringToken && len(lit.Data) == 0 {
			return !negated, true // falsy
		} else if tt == js.TrueToken || tt == js.StringToken {
			return negated, true // truthy
		} else if tt == js.DecimalToken || tt == js.BinaryToken || tt == js.OctalToken || tt == js.HexadecimalToken || tt == js.IntegerToken {
			for _, c := range d {
				if c == 'e' || c == 'E' || c == 'n' {
					break
				} else if c != '0' && c != '.' && c != 'x' && c != 'X' && c != 'b' && c != 'B' && c != 'o' && c != 'O' {
					return negated, true // truthy
				}
			}
			return !negated, true // falsy
		}
	} else if isUndefined(i) {
		return !negated, true // falsy
	} else if v, ok := i.(*js.Var); ok && bytes.Equal(v.Name(), nanBytes) {
		return !negated, true // falsy
	}
	return false, false // unknown
}

func isBooleanExpr(expr js.IExpr) bool {
	if unaryExpr, ok := expr.(*js.UnaryExpr); ok {
		return unaryExpr.Op == js.NotToken
	} else if binaryExpr, ok := expr.(*js.BinaryExpr); ok {
		op := binaryOpPrecMap[binaryExpr.Op]
		if op == js.OpAnd || op == js.OpOr {
			return isBooleanExpr(binaryExpr.X) && isBooleanExpr(binaryExpr.Y)
		}
		return op == js.OpCompare || op == js.OpEquals
	} else if litExpr, ok := expr.(*js.LiteralExpr); ok {
		return litExpr.TokenType == js.TrueToken || litExpr.TokenType == js.FalseToken
	} else if groupExpr, ok := expr.(*js.GroupExpr); ok {
		return isBooleanExpr(groupExpr.X)
	}
	return false
}

func invertBooleanOp(op js.TokenType) js.TokenType {
	if op == js.EqEqToken {
		return js.NotEqToken
	} else if op == js.NotEqToken {
		return js.EqEqToken
	} else if op == js.EqEqEqToken {
		return js.NotEqEqToken
	} else if op == js.NotEqEqToken {
		return js.EqEqEqToken
	}
	return js.ErrorToken
}

func optimizeBooleanExpr(expr js.IExpr, invert bool, prec js.OpPrec) js.IExpr {
	if invert {
		// unary !(boolean) has already been handled
		if binaryExpr, ok := expr.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals {
			binaryExpr.Op = invertBooleanOp(binaryExpr.Op)
			return expr
		} else {
			return optimizeUnaryExpr(&js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}, prec)
		}
	} else if isBooleanExpr(expr) {
		return groupExpr(expr, prec)
	} else {
		return &js.UnaryExpr{js.NotToken, &js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}}
	}
}

func optimizeUnaryExpr(expr *js.UnaryExpr, prec js.OpPrec) js.IExpr {
	if expr.Op == js.NotToken {
		invert := true
		var expr2 js.IExpr = expr.X
		for {
			if unary, ok := expr2.(*js.UnaryExpr); ok && unary.Op == js.NotToken {
				invert = !invert
				expr2 = unary.X
			} else if group, ok := expr2.(*js.GroupExpr); ok {
				expr2 = group.X
			} else {
				break
			}
		}
		if !invert && isBooleanExpr(expr2) {
			return groupExpr(expr2, prec)
		} else if binary, ok := expr2.(*js.BinaryExpr); ok && invert {
			if binaryOpPrecMap[binary.Op] == js.OpEquals {
				binary.Op = invertBooleanOp(binary.Op)
				return groupExpr(binary, prec)
			} else if binary.Op == js.AndToken || binary.Op == js.OrToken {
				op := js.AndToken
				if binary.Op == js.AndToken {
					op = js.OrToken
				}
				precInside := binaryOpPrecMap[op]
				needsGroup := precInside < prec && (precInside != js.OpCoalesce || prec != js.OpBitOr)

				// rewrite !(a||b) to !a&&!b
				// rewrite !(a==0||b==0) to a!=0&&b!=0
				score := 3 // savings if rewritten (group parentheses and not-token)
				if needsGroup {
					score -= 2
				}
				score -= 2 // add two not-tokens for left and right

				// == and === can become != and !==
				var isEqX, isEqY bool
				if binaryExpr, ok := binary.X.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals {
					score += 1
					isEqX = true
				}
				if binaryExpr, ok := binary.Y.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals {
					score += 1
					isEqY = true
				}

				// add group if it wasn't already there
				var needsGroupX, needsGroupY bool
				if !isEqX && binaryLeftPrecMap[binary.Op] <= exprPrec(binary.X) && exprPrec(binary.X) < js.OpUnary {
					score -= 2
					needsGroupX = true
				}
				if !isEqY && binaryRightPrecMap[binary.Op] <= exprPrec(binary.Y) && exprPrec(binary.Y) < js.OpUnary {
					score -= 2
					needsGroupY = true
				}

				// remove group
				if op == js.OrToken {
					if exprPrec(binary.X) == js.OpOr {
						score += 2
					}
					if exprPrec(binary.Y) == js.OpAnd {
						score += 2
					}
				}

				if 0 < score {
					binary.Op = op
					if isEqX {
						binary.X.(*js.BinaryExpr).Op = invertBooleanOp(binary.X.(*js.BinaryExpr).Op)
					}
					if isEqY {
						binary.Y.(*js.BinaryExpr).Op = invertBooleanOp(binary.Y.(*js.BinaryExpr).Op)
					}
					if needsGroupX {
						binary.X = &js.GroupExpr{binary.X}
					}
					if needsGroupY {
						binary.Y = &js.GroupExpr{binary.Y}
					}
					if !isEqX {
						binary.X = &js.UnaryExpr{js.NotToken, binary.X}
					}
					if !isEqY {
						binary.Y = &js.UnaryExpr{js.NotToken, binary.Y}
					}
					if needsGroup {
						return &js.GroupExpr{binary}
					}
					return binary
				}
			}
		}
	}
	return expr
}

func (m *jsMinifier) optimizeCondExpr(expr *js.CondExpr, prec js.OpPrec) js.IExpr {
	// remove double negative !! in condition, or switch cases for single negative !
	if unary1, ok := expr.Cond.(*js.UnaryExpr); ok && unary1.Op == js.NotToken {
		if unary2, ok := unary1.X.(*js.UnaryExpr); ok && unary2.Op == js.NotToken {
			if isBooleanExpr(unary2.X) {
				expr.Cond = unary2.X
			}
		} else {
			expr.Cond = unary1.X
			expr.X, expr.Y = expr.Y, expr.X
		}
	}

	finalCond := finalExpr(expr.Cond)
	if truthy, ok := isTruthy(expr.Cond); truthy && ok {
		// if condition is truthy
		return expr.X
	} else if !truthy && ok {
		// if condition is falsy
		return expr.Y
	} else if isEqualExpr(finalCond, expr.X) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.OrToken] <= exprPrec(finalCond)) && (exprPrec(expr.Y) < js.OpAssign || binaryRightPrecMap[js.OrToken] <= exprPrec(expr.Y)) {
		// if condition is equal to true body
		// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
		return &js.BinaryExpr{js.OrToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.OrToken]), expr.Y}
	} else if isEqualExpr(finalCond, expr.Y) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.AndToken] <= exprPrec(finalCond)) && (exprPrec(expr.X) < js.OpAssign || binaryRightPrecMap[js.AndToken] <= exprPrec(expr.X)) {
		// if condition is equal to false body
		// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
		return &js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), expr.X}
	} else if isEqualExpr(expr.X, expr.Y) {
		// if true and false bodies are equal
		return groupExpr(&js.CommaExpr{[]js.IExpr{expr.Cond, expr.X}}, prec)
	} else {
		if m.o.minVersion(2020) {
			if nullishExpr, ok := toNullishExpr(expr); ok {
				// no need to check whether left/right need to add groups, as the space saving is always more
				return nullishExpr
			}
		}
		callX, isCallX := expr.X.(*js.CallExpr)
		callY, isCallY := expr.Y.(*js.CallExpr)
		if isCallX && isCallY && len(callX.Args.List) == 1 && len(callY.Args.List) == 1 && !callX.Args.List[0].Rest && !callY.Args.List[0].Rest && isEqualExpr(callX.X, callY.X) {
			expr.X = callX.Args.List[0].Value
			expr.Y = callY.Args.List[0].Value
			return &js.CallExpr{callX.X, js.Args{[]js.Arg{{expr, false}}}, false} // recompress the conditional expression inside
		}

		// shorten when true and false bodies are true and false
		trueX, falseX := isTrue(expr.X), isFalse(expr.X)
		trueY, falseY := isTrue(expr.Y), isFalse(expr.Y)
		if trueX && falseY || falseX && trueY {
			return optimizeBooleanExpr(expr.Cond, falseX, prec)
		} else if trueX || trueY {
			// trueX != trueY
			cond := optimizeBooleanExpr(expr.Cond, trueY, binaryLeftPrecMap[js.OrToken])
			if trueY {
				return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.OrToken])}
			} else {
				return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.OrToken])}
			}
		} else if falseX || falseY {
			// falseX != falseY
			cond := optimizeBooleanExpr(expr.Cond, falseX, binaryLeftPrecMap[js.AndToken])
			if falseX {
				return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.AndToken])}
			} else {
				return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.AndToken])}
			}
		} else if condExpr, ok := expr.X.(*js.CondExpr); ok && isEqualExpr(expr.Y, condExpr.Y) {
			// nested conditional expression with same false bodies
			return &js.CondExpr{&js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), groupExpr(condExpr.Cond, binaryRightPrecMap[js.AndToken])}, condExpr.X, expr.Y}
		} else if prec <= js.OpExpr {
			// regular conditional expression
			// convert  (a,b)?c:d  =>  a,b?c:d
			if group, ok := expr.Cond.(*js.GroupExpr); ok {
				if comma, ok := group.X.(*js.CommaExpr); ok && js.OpCoalesce <= exprPrec(comma.List[len(comma.List)-1]) {
					expr.Cond = comma.List[len(comma.List)-1]
					comma.List[len(comma.List)-1] = expr
					return comma // recompress the conditional expression inside
				}
			}
		}
	}
	return expr
}

func isHexDigit(b byte) bool {
	return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}

func mergeBinaryExpr(expr *js.BinaryExpr) {
	// merge string concatenations which may be intertwined with other additions
	var ok bool
	for expr.Op == js.AddToken {
		if lit, ok := expr.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken {
			left := expr
			strings := []*js.LiteralExpr{lit}
			n := len(lit.Data) - 2
			for left.Op == js.AddToken {
				if 50 < len(strings) {
					return // limit recursion
				}
				if lit, ok := left.X.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken {
					strings = append(strings, lit)
					n += len(lit.Data) - 2
					left.X = nil
				} else if newLeft, ok := left.X.(*js.BinaryExpr); ok {
					if lit, ok := newLeft.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken {
						strings = append(strings, lit)
						n += len(lit.Data) - 2
						left = newLeft
						continue
					}
				}
				break
			}

			if 1 < len(strings) {
				// unescaped quotes will be repaired in minifyString later on
				b := make([]byte, 0, n+2)
				b = append(b, strings[len(strings)-1].Data[:len(strings[len(strings)-1].Data)-1]...)
				for i := len(strings) - 2; 0 < i; i-- {
					b = append(b, strings[i].Data[1:len(strings[i].Data)-1]...)
				}
				b = append(b, strings[0].Data[1:]...)
				b[len(b)-1] = b[0]

				expr.X = left.X
				expr.Y.(*js.LiteralExpr).Data = b
			}
		}
		if expr, ok = expr.X.(*js.BinaryExpr); !ok {
			break
		}
	}
}

func minifyString(b []byte, allowTemplate bool) []byte {
	if len(b) < 3 {
		return []byte("\"\"")
	}

	// switch quotes if more optimal
	singleQuotes := 0
	doubleQuotes := 0
	backtickQuotes := 0
	newlines := 0
	dollarSigns := 0
	for i := 1; i < len(b)-1; i++ {
		if b[i] == '\'' {
			singleQuotes++
		} else if b[i] == '"' {
			doubleQuotes++
		} else if b[i] == '`' {
			backtickQuotes++
		} else if b[i] == '$' && i+1 < len(b) && b[i+1] == '{' {
			dollarSigns++
		} else if b[i] == '\\' && i+1 < len(b) {
			if b[i+1] == 'n' {
				newlines++
			} else if '1' <= b[i+1] && b[i+1] <= '9' && i+2 < len(b) {
				if b[i+1] == '1' && b[i+2] == '2' {
					newlines++
				} else if b[i+1] == '4' && b[i+2] == '2' {
					doubleQuotes++
				} else if b[i+1] == '4' && b[i+2] == '7' {
					singleQuotes++
				} else if i+3 < len(b) && b[i+1] == '1' && b[i+2] == '4' && b[i+3] == '0' {
					backtickQuotes++
				}
			} else if b[i+1] == 'x' && i+3 < len(b) {
				if b[i+2] == '0' && b[i+3]|0x20 == 'a' {
					newlines++
				} else if b[i+2] == '2' && b[i+3] == '2' {
					doubleQuotes++
				} else if b[i+2] == '2' && b[i+3] == '7' {
					singleQuotes++
				} else if b[i+2] == '6' && b[i+3] == '0' {
					backtickQuotes++
				}
			} else if b[i+1] == 'u' && i+5 < len(b) && b[i+2] == '0' && b[i+3] == '0' {
				if b[i+4] == '0' && b[i+5]|0x20 == 'a' {
					newlines++
				} else if b[i+4] == '2' && b[i+5] == '2' {
					doubleQuotes++
				} else if b[i+4] == '2' && b[i+5] == '7' {
					singleQuotes++
				} else if b[i+4] == '6' && b[i+5] == '0' {
					backtickQuotes++
				}
			} else if b[i+1] == 'u' && i+4 < len(b) && b[i+2] == '{' {
				j := i + 3
				for j < len(b) && b[j] == '0' {
					j++
				}
				if j+1 < len(b) && b[j]|0x20 == 'a' && b[j+1] == '}' {
					newlines++
				} else if j+2 < len(b) && b[j+2] == '}' {
					if b[j] == '2' && b[j+1] == '2' {
						doubleQuotes++
					} else if b[j] == '2' && b[j+1] == '7' {
						singleQuotes++
					} else if b[j] == '6' && b[j+1] == '0' {
						backtickQuotes++
					}
				}
			}
		}
	}
	quote := byte('"') // default to " for better GZIP compression
	quotes := doubleQuotes
	if doubleQuotes < singleQuotes {
		quote = byte('"')
	} else if singleQuotes < doubleQuotes {
		quote = byte('\'')
		quotes = singleQuotes
	}
	if allowTemplate && backtickQuotes+dollarSigns < quotes+newlines {
		quote = byte('`')
	}
	b[0] = quote
	b[len(b)-1] = quote

	// strip unnecessary escapes
	return replaceEscapes(b, quote, 1, 1)
}

func replaceEscapes(b []byte, quote byte, prefix, suffix int) []byte {
	// strip unnecessary escapes
	j := 0
	start := 0
	for i := prefix; i < len(b)-suffix; i++ {
		if c := b[i]; c == '\\' {
			c = b[i+1]
			if c == quote || c == '\\' || c == 'r' || quote != '`' && c == 'n' || c == '0' && (len(b)-suffix <= i+2 || b[i+2] < '0' || '7' < b[i+2]) {
				// keep escape sequence
				i++
				continue
			}
			n := 1 // number of characters to skip
			if c == '\n' || c == '\r' || c == 0xE2 && i+3 < len(b)-1 && b[i+2] == 0x80 && (b[i+3] == 0xA8 || b[i+3] == 0xA9) {
				// line continuations
				if c == 0xE2 {
					n = 4
				} else if c == '\r' && i+2 < len(b)-1 && b[i+2] == '\n' {
					n = 3
				} else {
					n = 2
				}
			} else if c == 'x' {
				if i+3 < len(b)-1 && isHexDigit(b[i+2]) && b[i+2] < '8' && isHexDigit(b[i+3]) && (!(b[i+2] == '0' && b[i+3] == '0') || i+3 == len(b) || b[i+3] != '\\' && (b[i+3] < '0' && '7' < b[i+3])) {
					// don't convert \x00 to \0 if it may be an octal number
					// hexadecimal escapes
					_, _ = hex.Decode(b[i:i+1:i+1], b[i+2:i+4])
					n = 4
					if b[i] == '\\' || b[i] == quote || b[i] == '\r' || quote != '`' && b[i] == '\n' || b[i] == 0 {
						if b[i] == '\n' {
							b[i+1] = 'n'
						} else if b[i] == '\r' {
							b[i+1] = 'r'
						} else {
							b[i+1] = b[i]
						}
						b[i] = '\\'
						i++
						n--
					}
					i++
					n--
				} else {
					i++
					continue
				}
			} else if c == 'u' && i+2 < len(b) {
				l := i + 2
				if b[i+2] == '{' {
					l++
				}
				r := l
				for ; r < len(b) && (b[i+2] == '{' || r < l+4); r++ {
					if b[r] < '0' || '9' < b[r] && b[r] < 'A' || 'F' < b[r] && b[r] < 'a' || 'f' < b[r] {
						break
					}
				}
				if b[i+2] == '{' && (6 < r-l || len(b) <= r || b[r] != '}') || b[i+2] != '{' && r-l != 4 {
					i++
					continue
				}
				num, err := stdStrconv.ParseInt(string(b[l:r]), 16, 32)
				if err != nil || 0x10FFFF <= num {
					i++
					continue
				}

				n = 2 + r - l
				if b[i+2] == '{' {
					n += 2
				}
				if num == 0 {
					// don't convert NULL to literal NULL (gives JS parsing problems)
					if r == len(b) || b[r] != '\\' && (b[r] < '0' && '7' < b[r]) {
						b[i+1] = '0'
						i += 2
						n -= 2
					} else {
						// don't convert NULL to \0 (may be an octal number)
						b[i+1] = 'x'
						b[i+2] = '0'
						b[i+3] = '0'
						i += 4
						n -= 4
					}
				} else if num != 13 && (quote == '`' || num != 10) {
					// decode unicode character to UTF-8 and put at the end of the escape sequence
					// then skip the first part of the escape sequence until the decoded character
					m := utf8.RuneLen(rune(num))
					if m == -1 {
						i++
						continue
					} else if num < 256 && quote == byte(num) {
						b[i] = '\\'
						i++
						n--
					}
					utf8.EncodeRune(b[i:], rune(num))
					i += m
					n -= m
				} else {
					if num == 10 {
						b[i+1] = 'n'
					} else {
						b[i+1] = 'r'
					}
					i += 2
					n -= 2
				}
			} else if '0' <= c && c <= '7' {
				// octal escapes (legacy), \0 already handled (quote != `)
				num := c - '0'
				n++
				if i+2 < len(b)-1 && '0' <= b[i+2] && b[i+2] <= '7' {
					num = num*8 + b[i+2] - '0'
					n++
					if num < 32 && i+3 < len(b)-1 && '0' <= b[i+3] && b[i+3] <= '7' {
						num = num*8 + b[i+3] - '0'
						n++
					}
				}
				b[i] = num
				if num == 0 || num == '\\' || num == quote || num == '\r' || quote != '`' && num == '\n' {
					if num == 0 {
						b[i+1] = '0'
					} else if num == '\n' {
						b[i+1] = 'n'
					} else if num == '\r' {
						b[i+1] = 'r'
					} else {
						b[i+1] = b[i]
					}
					b[i] = '\\'
					i++
					n--
				}
				i++
				n--
			} else if quote == '`' && c == 'n' {
				b[i] = '\n'
				i++
			} else if c == 't' {
				b[i] = '\t'
				i++
			} else if c == 'f' {
				b[i] = '\f'
				i++
			} else if c == 'v' {
				b[i] = '\v'
				i++
			} else if c == 'b' {
				b[i] = '\b'
				i++
			}
			// remove unnecessary escape character, anything but 0x00, 0x0A, 0x0D, \, ' or "
			if start != 0 {
				j += copy(b[j:], b[start:i])
			} else {
				j = i
			}
			start = i + n
			i += n - 1
		} else if c == quote || c == '$' && quote == '`' && (i+1 < len(b) && b[i+1] == '{' || i+2 < len(b) && b[i+1] == '\\' && b[i+2] == '{') {
			// may not be escaped properly when changing quotes
			if j < start {
				// avoid append
				j += copy(b[j:], b[start:i])
				b[j] = '\\'
				j++
				start = i
			} else {
				b = append(append(b[:i], '\\'), b[i:]...)
				i++
				b[i] = c // was overwritten above
			}
		} else if c == '<' && 9 <= len(b)-1-i {
			if b[i+1] == '\\' && 10 <= len(b)-1-i && bytes.Equal(b[i+2:i+10], []byte("/script>")) {
				i += 9
			} else if bytes.Equal(b[i+1:i+9], []byte("/script>")) {
				i++
				if j < start {
					// avoid append
					j += copy(b[j:], b[start:i])
					b[j] = '\\'
					j++
					start = i
				} else {
					b = append(append(b[:i], '\\'), b[i:]...)
					i++
					b[i] = '/' // was overwritten above
				}
			}
		}
	}
	if start != 0 {
		j += copy(b[j:], b[start:])
		return b[:j]
	}
	return b
}

var regexpEscapeTable = [256]bool{
	// ASCII
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, true, false, false, false, // $
	true, true, true, true, false, false, true, true, // (, ), *, +, ., /
	true, true, true, true, true, true, true, true, // 0, 1, 2, 3, 4, 5, 6, 7
	true, true, false, false, false, false, false, true, // 8, 9, ?

	false, false, true, false, true, false, false, false, // B, D
	false, false, false, false, false, false, false, false,
	true, false, false, true, false, false, false, true, // P, S, W
	false, false, false, true, true, true, true, false, // [, \, ], ^

	false, false, true, true, true, false, true, false, // b, c, d, f
	false, false, false, true, false, false, true, false, // k, n
	true, false, true, true, true, true, true, true, // p, r, s, t, u, v, w
	true, false, false, true, true, true, false, false, // x, {, |, }

	// non-ASCII
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
}

var regexpClassEscapeTable = [256]bool{
	// ASCII
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	true, true, true, true, true, true, true, true, // 0, 1, 2, 3, 4, 5, 6, 7
	true, true, false, false, false, false, false, false, // 8, 9

	false, false, false, false, true, false, false, false, // D
	false, false, false, false, false, false, false, false,
	true, false, false, true, false, false, false, true, // P, S, W
	false, false, false, false, true, true, false, false, // \, ]

	false, false, true, true, true, false, true, false, // b, c, d, f
	false, false, false, false, false, false, true, false, // n
	true, false, true, true, true, true, true, true, // p, r, s, t, u, v, w
	true, false, false, false, false, false, false, false, // x

	// non-ASCII
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,

	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false,
}

func minifyRegExp(b []byte) []byte {
	inClass := false
	afterDash := 0
	iClass := 0
	for i := 1; i < len(b)-1; i++ {
		if inClass {
			afterDash++
		}
		if b[i] == '\\' {
			c := b[i+1]
			escape := true
			if inClass {
				escape = regexpClassEscapeTable[c] || c == '-' && 2 < afterDash && i+2 < len(b) && b[i+2] != ']' || c == '^' && i == iClass+1
			} else {
				escape = regexpEscapeTable[c]
			}
			if !escape {
				b = append(b[:i], b[i+1:]...)
				if inClass && 2 < afterDash && c == '-' {
					afterDash = 0
				} else if inClass && c == '^' {
					afterDash = 1
				}
			} else {
				i++
			}
		} else if b[i] == '[' {
			if b[i+1] == '^' {
				i++
			}
			afterDash = 1
			inClass = true
			iClass = i
		} else if inClass && b[i] == ']' {
			inClass = false
		} else if b[i] == '/' {
			break
		} else if inClass && 2 < afterDash && b[i] == '-' {
			afterDash = 0
		}
	}
	return b
}

func removeUnderscoresAndSuffix(b []byte) ([]byte, bool) {
	for i := 0; i < len(b); i++ {
		if b[i] == '_' {
			b = append(b[:i], b[i+1:]...)
			i--
		}
	}
	if 0 < len(b) && b[len(b)-1] == 'n' {
		return b[:len(b)-1], true
	}
	return b, false
}

func decimalNumber(num []byte, prec int) []byte {
	b, suffix := removeUnderscoresAndSuffix(num)
	if suffix {
		return append(b, 'n')
	}
	return minify.Number(b, prec)
}

func binaryNumber(num []byte, prec int) []byte {
	b, suffix := removeUnderscoresAndSuffix(num)
	if len(b) <= 2 || 65 < len(b) {
		if suffix {
			return append(b, 'n')
		}
		return b
	}
	var n int64
	for _, c := range b[2:] {
		n *= 2
		n += int64(c - '0')
	}
	i := strconv.LenInt(n) - 1
	b = b[:i+1]
	for 0 <= i {
		b[i] = byte('0' + n%10)
		n /= 10
		i--
	}
	if suffix {
		return append(b, 'n')
	}
	return minify.Number(b, prec)
}

func octalNumber(num []byte, prec int) []byte {
	b, suffix := removeUnderscoresAndSuffix(num)
	if len(b) <= 2 || 23 < len(b) {
		if suffix {
			return append(b, 'n')
		}
		return b
	}
	var n int64
	for _, c := range b[2:] {
		n *= 8
		n += int64(c - '0')
	}
	i := strconv.LenInt(n) - 1
	b = b[:i+1]
	for 0 <= i {
		b[i] = byte('0' + n%10)
		n /= 10
		i--
	}
	if suffix {
		return append(b, 'n')
	}
	return minify.Number(b, prec)
}

func hexadecimalNumber(num []byte, prec int) []byte {
	b, suffix := removeUnderscoresAndSuffix(num)
	if len(b) <= 2 || 12 < len(b) || len(b) == 12 && ('D' < b[2] && b[2] <= 'F' || 'd' < b[2]) {
		if suffix {
			return append(b, 'n')
		}
		return b
	}
	var n int64
	for _, c := range b[2:] {
		n *= 16
		if c <= '9' {
			n += int64(c - '0')
		} else if c <= 'F' {
			n += 10 + int64(c-'A')
		} else {
			n += 10 + int64(c-'a')
		}
	}
	i := strconv.LenInt(n) - 1
	b = b[:i+1]
	for 0 <= i {
		b[i] = byte('0' + n%10)
		n /= 10
		i--
	}
	if suffix {
		return append(b, 'n')
	}
	return minify.Number(b, prec)
}
