package js

import (
	"bytes"
	"encoding/hex"

	"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("*")
	colonBytes                 = []byte(":")
	semicolonBytes             = []byte(";")
	commaBytes                 = []byte(",")
	dotBytes                   = []byte(".")
	ellipsisBytes              = []byte("...")
	openBraceBytes             = []byte("{")
	closeBraceBytes            = []byte("}")
	openParenBytes             = []byte("(")
	closeParenBytes            = []byte(")")
	openBracketBytes           = []byte("[")
	closeBracketBytes          = []byte("]")
	openParenBracketBytes      = []byte("({")
	closeBracketParenBytes     = []byte("})")
	closeParenOpenBracketBytes = []byte("){")
	notBytes                   = []byte("!")
	questionBytes              = []byte("?")
	equalBytes                 = []byte("=")
	notNotBytes                = []byte("!!")
	andBytes                   = []byte("&&")
	orBytes                    = []byte("||")
	optChainBytes              = []byte("?.")
	nullishBytes               = []byte("??")
	arrowBytes                 = []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")
	varBytes                   = []byte("var")
	varSpaceBytes              = []byte("var ")
	nanBytes                   = []byte("NaN")
	undefinedBytes             = []byte("undefined")
	infinityBytes              = []byte("Infinity")
	voidZeroBytes              = []byte("void 0")
	groupedVoidZeroBytes       = []byte("(void 0)")
	oneDivZeroBytes            = []byte("1/0")
	groupedOneDivZeroBytes     = []byte("(1/0)")
	notZeroBytes               = []byte("!0")
	groupedNotZeroBytes        = []byte("(!0)")
	notOneBytes                = []byte("!1")
	groupedNotOneBytes         = []byte("(!1)")
	useStrictBytes             = []byte(`"use strict"`)
	debuggerBytes              = []byte("debugger")
)

// 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.OptChainExpr, *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 // does not happen
}

// 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 {
	return &js.CondExpr{
		Cond: groupExpr(cond, js.OpCoalesce),
		X:    groupExpr(x, js.OpAssign),
		Y:    groupExpr(y, js.OpAssign),
	}
}

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

func finalExpr(expr js.IExpr) js.IExpr {
	if group, ok := expr.(*js.GroupExpr); ok {
		expr = group.X
	}
	if binary, ok := expr.(*js.BinaryExpr); ok && binary.Op == js.CommaToken {
		expr = binary.Y
	}
	if binary, ok := expr.(*js.BinaryExpr); ok && binary.Op == js.EqToken {
		expr = binary.X
	}
	return expr
}

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 block.List[len(block.List)-1]
	}
	return stmt
}

func (m *jsMinifier) isTrue(i js.IExpr) bool {
	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, _ := m.isFalsy(unary.X)
		return ret
	}
	return false
}

func (m *jsMinifier) isFalse(i js.IExpr) bool {
	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, _ := m.isTruthy(unary.X)
		return ret
	}
	return false
}

func (m *jsMinifier) toNullishExpr(condExpr *js.CondExpr) (js.IExpr, js.IExpr, bool) {
	// convert conditional expression to nullish:  a!=null?a:b  =>  a??b
	if binaryExpr, ok := condExpr.Cond.(*js.BinaryExpr); ok && (binaryExpr.Op == js.EqEqToken || binaryExpr.Op == js.NotEqToken) {
		var left, right js.IExpr
		if binaryExpr.Op == js.EqEqToken {
			left = condExpr.Y
			right = condExpr.X
		} else {
			left = condExpr.X
			right = condExpr.Y
		}
		if lit, ok := binaryExpr.X.(*js.LiteralExpr); ((ok && lit.TokenType == js.NullToken) || m.isUndefined(binaryExpr.X)) && m.isEqualExpr(binaryExpr.Y, left) {
			return left, right, true
		} else if lit, ok := binaryExpr.Y.(*js.LiteralExpr); ((ok && lit.TokenType == js.NullToken) || m.isUndefined(binaryExpr.Y)) && m.isEqualExpr(binaryExpr.X, left) {
			return left, right, true
		}
	}
	return nil, nil, false
}

func (m *jsMinifier) isUndefined(i js.IExpr) bool {
	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 true
	}
	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 (m *jsMinifier) isTruthy(i js.IExpr) (bool, bool) {
	if falsy, ok := m.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 (m *jsMinifier) 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.BigIntToken {
			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 m.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 (m *jsMinifier) isEqualExpr(a, b js.IExpr) bool {
	if group, ok := a.(*js.GroupExpr); ok {
		a = group.X
	}
	if group, ok := b.(*js.GroupExpr); ok {
		b = group.X
	}
	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 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]
		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 (m *jsMinifier) minifyBooleanExpr(expr js.IExpr, invert bool, prec js.OpPrec) {
	if invert {
		// unary !(boolean) has already been handled
		if binaryExpr, ok := expr.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals {
			if binaryExpr.Op == js.EqEqToken {
				binaryExpr.Op = js.NotEqToken
			} else if binaryExpr.Op == js.NotEqToken {
				binaryExpr.Op = js.EqEqToken
			} else if binaryExpr.Op == js.EqEqEqToken {
				binaryExpr.Op = js.NotEqEqToken
			} else if binaryExpr.Op == js.NotEqEqToken {
				binaryExpr.Op = js.EqEqEqToken
			}
			m.minifyExpr(expr, prec)
		} else {
			m.write(notBytes)
			m.minifyExpr(&js.GroupExpr{X: expr}, js.OpUnary)
		}
	} else if isBooleanExpr(expr) {
		m.minifyExpr(&js.GroupExpr{X: expr}, prec)
	} else {
		m.write(notNotBytes)
		m.minifyExpr(&js.GroupExpr{X: expr}, js.OpUnary)
	}
}

func endsInIf(istmt js.IStmt) bool {
	switch stmt := istmt.(type) {
	case *js.IfStmt:
		if stmt.Else == nil {
			return true
		}
		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
}

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

func minifyString(b []byte) []byte {
	if len(b) < 3 {
		return b
	}

	// switch quotes if more optimal
	singleQuotes := 0
	doubleQuotes := 0
	for i := 1; i < len(b)-1; i++ {
		if b[i] == '\'' {
			singleQuotes++
		} else if b[i] == '"' {
			doubleQuotes++
		}
	}
	quote := b[0]
	if singleQuotes < doubleQuotes {
		quote = byte('\'')
	} else if doubleQuotes < singleQuotes {
		quote = byte('"')
	}
	b[0] = quote
	b[len(b)-1] = quote

	// strip unnecessary escapes
	j := 0
	start := 0
	for i := 1; i < len(b)-1; i++ {
		if c := b[i]; c == '\\' {
			c = b[i+1]
			if c == '0' && (i+2 == len(b)-1 || b[i+2] < '0' || '7' < b[i+2]) || c == '\\' || c == quote || c == 'n' || c == 'r' || c == 'u' {
				// keep escape sequence
				i++
				continue
			}
			n := 1
			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]) {
					// hexadecimal escapes
					_, _ = hex.Decode(b[i+3:i+4:i+4], b[i+2:i+4])
					n = 3
					if b[i+3] == 0 || b[i+3] == '\\' || b[i+3] == quote || b[i+3] == '\n' || b[i+3] == '\r' {
						if b[i+3] == 0 {
							b[i+3] = '0'
						} else if b[i+3] == '\n' {
							b[i+3] = 'n'
						} else if b[i+3] == '\r' {
							b[i+3] = 'r'
						}
						n--
						b[i+2] = '\\'
					}
				} else {
					i++
					continue
				}
			} else if '0' <= c && c <= '7' {
				// octal escapes (legacy), \0 already handled
				num := byte(c - '0')
				if i+2 < len(b)-1 && '0' <= b[i+2] && b[i+2] <= '7' {
					num = num*8 + byte(b[i+2]-'0')
					n++
					if num < 32 && i+3 < len(b)-1 && '0' <= b[i+3] && b[i+3] <= '7' {
						num = num*8 + byte(b[i+3]-'0')
						n++
					}
				}
				b[i+n] = num
				if num == 0 || num == '\\' || num == quote || num == '\n' || num == '\r' {
					if num == 0 {
						b[i+n] = '0'
					} else if num == '\n' {
						b[i+n] = 'n'
					} else if num == '\r' {
						b[i+n] = 'r'
					}
					n--
					b[i+n] = '\\'
				}
			} else if c == 't' {
				b[i+1] = '\t'
			} else if c == 'f' {
				b[i+1] = '\f'
			} else if c == 'v' {
				b[i+1] = '\v'
			} else if c == 'b' {
				b[i+1] = '\b'
			}
			// 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 {
			// 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] = quote // 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
}

func binaryNumber(b []byte, prec int) []byte {
	if len(b) <= 2 || 65 < len(b) {
		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--
	}
	return minify.Number(b, prec)
}

func octalNumber(b []byte, prec int) []byte {
	if len(b) <= 2 || 23 < len(b) {
		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--
	}
	return minify.Number(b, prec)
}

func hexadecimalNumber(b []byte, prec int) []byte {
	if len(b) <= 2 || 12 < len(b) || len(b) == 12 && ('D' < b[2] && b[2] <= 'F' || 'd' < b[2]) {
		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--
	}
	return minify.Number(b, prec)
}
