// Copyright 2021 The Bazel Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package math provides basic constants and mathematical functions.
package math // import "go.starlark.net/lib/math"

import (
	"errors"
	"fmt"
	"math"

	"go.starlark.net/starlark"
	"go.starlark.net/starlarkstruct"
)

// Module math is a Starlark module of math-related functions and constants.
// The module defines the following functions:
//
//	ceil(x) - Returns the ceiling of x, the smallest integer greater than or equal to x.
//	copysign(x, y) - Returns a value with the magnitude of x and the sign of y.
//	fabs(x) - Returns the absolute value of x as float.
//	floor(x) - Returns the floor of x, the largest integer less than or equal to x.
//	mod(x, y) - Returns the floating-point remainder of x/y. The magnitude of the result is less than y and its sign agrees with that of x.
//	pow(x, y) - Returns x**y, the base-x exponential of y.
//	remainder(x, y) - Returns the IEEE 754 floating-point remainder of x/y.
//	round(x) - Returns the nearest integer, rounding half away from zero.
//
//	exp(x) - Returns e raised to the power x, where e = 2.718281… is the base of natural logarithms.
//	sqrt(x) - Returns the square root of x.
//
//	acos(x) - Returns the arc cosine of x, in radians.
//	asin(x) - Returns the arc sine of x, in radians.
//	atan(x) - Returns the arc tangent of x, in radians.
//	atan2(y, x) - Returns atan(y / x), in radians.
//	              The result is between -pi and pi.
//	              The vector in the plane from the origin to point (x, y) makes this angle with the positive X axis.
//	              The point of atan2() is that the signs of both inputs are known to it, so it can compute the correct
//	              quadrant for the angle.
//	              For example, atan(1) and atan2(1, 1) are both pi/4, but atan2(-1, -1) is -3*pi/4.
//	cos(x) - Returns the cosine of x, in radians.
//	hypot(x, y) - Returns the Euclidean norm, sqrt(x*x + y*y). This is the length of the vector from the origin to point (x, y).
//	sin(x) - Returns the sine of x, in radians.
//	tan(x) - Returns the tangent of x, in radians.
//
//	degrees(x) - Converts angle x from radians to degrees.
//	radians(x) - Converts angle x from degrees to radians.
//
//	acosh(x) - Returns the inverse hyperbolic cosine of x.
//	asinh(x) - Returns the inverse hyperbolic sine of x.
//	atanh(x) - Returns the inverse hyperbolic tangent of x.
//	cosh(x) - Returns the hyperbolic cosine of x.
//	sinh(x) - Returns the hyperbolic sine of x.
//	tanh(x) - Returns the hyperbolic tangent of x.
//
//	log(x, base) - Returns the logarithm of x in the given base, or natural logarithm by default.
//
//	gamma(x) - Returns the Gamma function of x.
//
// All functions accept both int and float values as arguments.
//
// The module also defines approximations of the following constants:
//
//	e - The base of natural logarithms, approximately 2.71828.
//	pi - The ratio of a circle's circumference to its diameter, approximately 3.14159.
var Module = &starlarkstruct.Module{
	Name: "math",
	Members: starlark.StringDict{
		"ceil":      starlark.NewBuiltin("ceil", ceil),
		"copysign":  newBinaryBuiltin("copysign", math.Copysign),
		"fabs":      newUnaryBuiltin("fabs", math.Abs),
		"floor":     starlark.NewBuiltin("floor", floor),
		"mod":       newBinaryBuiltin("mod", math.Mod),
		"pow":       newBinaryBuiltin("pow", math.Pow),
		"remainder": newBinaryBuiltin("remainder", math.Remainder),
		"round":     newUnaryBuiltin("round", math.Round),

		"exp":  newUnaryBuiltin("exp", math.Exp),
		"sqrt": newUnaryBuiltin("sqrt", math.Sqrt),

		"acos":  newUnaryBuiltin("acos", math.Acos),
		"asin":  newUnaryBuiltin("asin", math.Asin),
		"atan":  newUnaryBuiltin("atan", math.Atan),
		"atan2": newBinaryBuiltin("atan2", math.Atan2),
		"cos":   newUnaryBuiltin("cos", math.Cos),
		"hypot": newBinaryBuiltin("hypot", math.Hypot),
		"sin":   newUnaryBuiltin("sin", math.Sin),
		"tan":   newUnaryBuiltin("tan", math.Tan),

		"degrees": newUnaryBuiltin("degrees", degrees),
		"radians": newUnaryBuiltin("radians", radians),

		"acosh": newUnaryBuiltin("acosh", math.Acosh),
		"asinh": newUnaryBuiltin("asinh", math.Asinh),
		"atanh": newUnaryBuiltin("atanh", math.Atanh),
		"cosh":  newUnaryBuiltin("cosh", math.Cosh),
		"sinh":  newUnaryBuiltin("sinh", math.Sinh),
		"tanh":  newUnaryBuiltin("tanh", math.Tanh),

		"log": starlark.NewBuiltin("log", log),

		"gamma": newUnaryBuiltin("gamma", math.Gamma),

		"e":  starlark.Float(math.E),
		"pi": starlark.Float(math.Pi),
	},
}

// floatOrInt is an Unpacker that converts a Starlark int or float to Go's float64.
type floatOrInt float64

func (p *floatOrInt) Unpack(v starlark.Value) error {
	switch v := v.(type) {
	case starlark.Int:
		*p = floatOrInt(v.Float())
		return nil
	case starlark.Float:
		*p = floatOrInt(v)
		return nil
	}
	return fmt.Errorf("got %s, want float or int", v.Type())
}

// newUnaryBuiltin wraps a unary floating-point Go function
// as a Starlark built-in that accepts int or float arguments.
func newUnaryBuiltin(name string, fn func(float64) float64) *starlark.Builtin {
	return starlark.NewBuiltin(name, func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
		var x floatOrInt
		if err := starlark.UnpackPositionalArgs(name, args, kwargs, 1, &x); err != nil {
			return nil, err
		}
		return starlark.Float(fn(float64(x))), nil
	})
}

// newBinaryBuiltin wraps a binary floating-point Go function
// as a Starlark built-in that accepts int or float arguments.
func newBinaryBuiltin(name string, fn func(float64, float64) float64) *starlark.Builtin {
	return starlark.NewBuiltin(name, func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
		var x, y floatOrInt
		if err := starlark.UnpackPositionalArgs(name, args, kwargs, 2, &x, &y); err != nil {
			return nil, err
		}
		return starlark.Float(fn(float64(x), float64(y))), nil
	})
}

//	log wraps the Log function
//
// as a Starlark built-in that accepts int or float arguments.
func log(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var (
		x    floatOrInt
		base floatOrInt = math.E
	)
	if err := starlark.UnpackPositionalArgs("log", args, kwargs, 1, &x, &base); err != nil {
		return nil, err
	}
	if base == 1 {
		return nil, errors.New("division by zero")
	}
	return starlark.Float(math.Log(float64(x)) / math.Log(float64(base))), nil
}

func ceil(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var x starlark.Value

	if err := starlark.UnpackPositionalArgs("ceil", args, kwargs, 1, &x); err != nil {
		return nil, err
	}

	switch t := x.(type) {
	case starlark.Int:
		return t, nil
	case starlark.Float:
		return starlark.NumberToInt(starlark.Float(math.Ceil(float64(t))))
	}

	return nil, fmt.Errorf("got %s, want float or int", x.Type())
}

func floor(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var x starlark.Value

	if err := starlark.UnpackPositionalArgs("floor", args, kwargs, 1, &x); err != nil {
		return nil, err
	}

	switch t := x.(type) {
	case starlark.Int:
		return t, nil
	case starlark.Float:
		return starlark.NumberToInt(starlark.Float(math.Floor(float64(t))))
	}

	return nil, fmt.Errorf("got %s, want float or int", x.Type())
}

func degrees(x float64) float64 {
	return 360 * x / (2 * math.Pi)
}

func radians(x float64) float64 {
	return 2 * math.Pi * x / 360
}
