package config

import (
	"math"
	"strings"
	"sync"

	"github.com/evanw/esbuild/internal/js_ast"
	"github.com/evanw/esbuild/internal/logger"
)

var processedGlobalsMutex sync.Mutex
var processedGlobals *ProcessedDefines

var knownGlobals = [][]string{
	// These global identifiers should exist in all JavaScript environments
	{"Array"},
	{"Boolean"},
	{"Function"},
	{"Math"},
	{"Number"},
	{"Object"},
	{"RegExp"},
	{"String"},

	// Object: Static methods
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Static_methods
	{"Object", "assign"},
	{"Object", "create"},
	{"Object", "defineProperties"},
	{"Object", "defineProperty"},
	{"Object", "entries"},
	{"Object", "freeze"},
	{"Object", "fromEntries"},
	{"Object", "getOwnPropertyDescriptor"},
	{"Object", "getOwnPropertyDescriptors"},
	{"Object", "getOwnPropertyNames"},
	{"Object", "getOwnPropertySymbols"},
	{"Object", "getPrototypeOf"},
	{"Object", "is"},
	{"Object", "isExtensible"},
	{"Object", "isFrozen"},
	{"Object", "isSealed"},
	{"Object", "keys"},
	{"Object", "preventExtensions"},
	{"Object", "seal"},
	{"Object", "setPrototypeOf"},
	{"Object", "values"},

	// Object: Instance methods
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Instance_methods
	{"Object", "prototype", "__defineGetter__"},
	{"Object", "prototype", "__defineSetter__"},
	{"Object", "prototype", "__lookupGetter__"},
	{"Object", "prototype", "__lookupSetter__"},
	{"Object", "prototype", "hasOwnProperty"},
	{"Object", "prototype", "isPrototypeOf"},
	{"Object", "prototype", "propertyIsEnumerable"},
	{"Object", "prototype", "toLocaleString"},
	{"Object", "prototype", "toString"},
	{"Object", "prototype", "unwatch"},
	{"Object", "prototype", "valueOf"},
	{"Object", "prototype", "watch"},

	// Math: Static properties
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_properties
	{"Math", "E"},
	{"Math", "LN10"},
	{"Math", "LN2"},
	{"Math", "LOG10E"},
	{"Math", "LOG2E"},
	{"Math", "PI"},
	{"Math", "SQRT1_2"},
	{"Math", "SQRT2"},

	// Math: Static methods
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_methods
	{"Math", "abs"},
	{"Math", "acos"},
	{"Math", "acosh"},
	{"Math", "asin"},
	{"Math", "asinh"},
	{"Math", "atan"},
	{"Math", "atan2"},
	{"Math", "atanh"},
	{"Math", "cbrt"},
	{"Math", "ceil"},
	{"Math", "clz32"},
	{"Math", "cos"},
	{"Math", "cosh"},
	{"Math", "exp"},
	{"Math", "expm1"},
	{"Math", "floor"},
	{"Math", "fround"},
	{"Math", "hypot"},
	{"Math", "imul"},
	{"Math", "log"},
	{"Math", "log10"},
	{"Math", "log1p"},
	{"Math", "log2"},
	{"Math", "max"},
	{"Math", "min"},
	{"Math", "pow"},
	{"Math", "random"},
	{"Math", "round"},
	{"Math", "sign"},
	{"Math", "sin"},
	{"Math", "sinh"},
	{"Math", "sqrt"},
	{"Math", "tan"},
	{"Math", "tanh"},
	{"Math", "trunc"},
}

type FindSymbol func(logger.Loc, string) js_ast.Ref
type DefineFunc func(logger.Loc, FindSymbol) js_ast.E

type DefineData struct {
	DefineFunc DefineFunc

	// True if accessing this value is known to not have any side effects. For
	// example, a bare reference to "Object.create" can be removed because it
	// does not have any observable side effects.
	CanBeRemovedIfUnused bool

	// True if a call to this value is known to not have any side effects. For
	// example, a bare call to "Object()" can be removed because it does not
	// have any observable side effects.
	CallCanBeUnwrappedIfUnused bool

	// Set to true to warn users that this should be defined if it's not already.
	WarnAboutLackOfDefine bool
}

func mergeDefineData(old DefineData, new DefineData) DefineData {
	if old.CanBeRemovedIfUnused {
		new.CanBeRemovedIfUnused = true
	}
	if old.CallCanBeUnwrappedIfUnused {
		new.CallCanBeUnwrappedIfUnused = true
	}

	// Don't warn if the user defined this
	new.WarnAboutLackOfDefine = false
	return new
}

type DotDefine struct {
	Parts []string
	Data  DefineData
}

type ProcessedDefines struct {
	IdentifierDefines map[string]DefineData
	DotDefines        map[string][]DotDefine
}

// This transformation is expensive, so we only want to do it once. Make sure
// to only call processDefines() once per compilation. Unfortunately Golang
// doesn't have an efficient way to copy a map and the overhead of copying
// all of the properties into a new map once for every new parser noticeably
// slows down our benchmarks.
func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines {
	// Optimization: reuse known globals if there are no user-specified defines
	hasUserDefines := len(userDefines) != 0
	if !hasUserDefines {
		processedGlobalsMutex.Lock()
		if processedGlobals != nil {
			defer processedGlobalsMutex.Unlock()
			return *processedGlobals
		}
		processedGlobalsMutex.Unlock()
	}

	result := ProcessedDefines{
		IdentifierDefines: make(map[string]DefineData),
		DotDefines:        make(map[string][]DotDefine),
	}

	// Mark these property accesses as free of side effects. That means they can
	// be removed if their result is unused. We can't just remove all unused
	// property accesses since property accesses can have side effects. For
	// example, the property access "a.b.c" has the side effect of throwing an
	// exception if "a.b" is undefined.
	for _, parts := range knownGlobals {
		tail := parts[len(parts)-1]
		if len(parts) == 1 {
			result.IdentifierDefines[tail] = DefineData{CanBeRemovedIfUnused: true}
		} else {
			result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{CanBeRemovedIfUnused: true}})
		}
	}

	// Swap in certain literal values because those can be constant folded
	result.IdentifierDefines["undefined"] = DefineData{
		DefineFunc: func(logger.Loc, FindSymbol) js_ast.E { return &js_ast.EUndefined{} },
	}
	result.IdentifierDefines["NaN"] = DefineData{
		DefineFunc: func(logger.Loc, FindSymbol) js_ast.E { return &js_ast.ENumber{Value: math.NaN()} },
	}
	result.IdentifierDefines["Infinity"] = DefineData{
		DefineFunc: func(logger.Loc, FindSymbol) js_ast.E { return &js_ast.ENumber{Value: math.Inf(1)} },
	}

	// Warn about use of this without a define
	result.DotDefines["NODE_ENV"] = []DotDefine{{
		Parts: []string{"process", "env", "NODE_ENV"},
		Data:  DefineData{WarnAboutLackOfDefine: true},
	}}

	// Then copy the user-specified defines in afterwards, which will overwrite
	// any known globals above.
	for key, data := range userDefines {
		parts := strings.Split(key, ".")

		// Identifier defines are special-cased
		if len(parts) == 1 {
			result.IdentifierDefines[key] = mergeDefineData(result.IdentifierDefines[key], data)
			continue
		}

		tail := parts[len(parts)-1]
		dotDefines := result.DotDefines[tail]
		found := false

		// Try to merge with existing dot defines first
		for i, define := range dotDefines {
			if arePartsEqual(parts, define.Parts) {
				define := &dotDefines[i]
				define.Data = mergeDefineData(define.Data, data)
				found = true
				break
			}
		}

		if !found {
			dotDefines = append(dotDefines, DotDefine{Parts: parts, Data: data})
		}
		result.DotDefines[tail] = dotDefines
	}

	// Potentially cache the result for next time
	if !hasUserDefines {
		processedGlobalsMutex.Lock()
		defer processedGlobalsMutex.Unlock()
		if processedGlobals == nil {
			processedGlobals = &result
		}
	}
	return result
}

func arePartsEqual(a []string, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
