package query

import (
	"fmt"
	"math"

	"github.com/influxdata/influxql"
)

func isMathFunction(call *influxql.Call) bool {
	switch call.Name {
	case "abs", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "exp", "log", "ln", "log2", "log10", "sqrt", "pow", "floor", "ceil", "round":
		return true
	}
	return false
}

type MathTypeMapper struct{}

func (MathTypeMapper) MapType(measurement *influxql.Measurement, field string) influxql.DataType {
	return influxql.Unknown
}

func (MathTypeMapper) CallType(name string, args []influxql.DataType) (influxql.DataType, error) {
	switch name {
	case "sin", "cos", "tan", "atan", "exp", "log", "ln", "log2", "log10", "sqrt":
		var arg0 influxql.DataType
		if len(args) > 0 {
			arg0 = args[0]
		}
		switch arg0 {
		case influxql.Float, influxql.Integer, influxql.Unsigned, influxql.Unknown:
			return influxql.Float, nil
		default:
			return influxql.Unknown, fmt.Errorf("invalid argument type for the first argument in %s(): %s", name, arg0)
		}
	case "asin", "acos":
		var arg0 influxql.DataType
		if len(args) > 0 {
			arg0 = args[0]
		}
		switch arg0 {
		case influxql.Float, influxql.Unknown:
			return influxql.Float, nil
		default:
			return influxql.Unknown, fmt.Errorf("invalid argument type for the first argument in %s(): %s", name, arg0)
		}
	case "atan2", "pow":
		var arg0, arg1 influxql.DataType
		if len(args) > 0 {
			arg0 = args[0]
		}
		if len(args) > 1 {
			arg1 = args[1]
		}

		switch arg0 {
		case influxql.Float, influxql.Integer, influxql.Unsigned, influxql.Unknown:
			// Pass through to verify the second argument.
		default:
			return influxql.Unknown, fmt.Errorf("invalid argument type for the first argument in %s(): %s", name, arg0)
		}

		switch arg1 {
		case influxql.Float, influxql.Integer, influxql.Unsigned, influxql.Unknown:
			return influxql.Float, nil
		default:
			return influxql.Unknown, fmt.Errorf("invalid argument type for the second argument in %s(): %s", name, arg1)
		}
	case "abs", "floor", "ceil", "round":
		var arg0 influxql.DataType
		if len(args) > 0 {
			arg0 = args[0]
		}
		switch arg0 {
		case influxql.Float, influxql.Integer, influxql.Unsigned, influxql.Unknown:
			return args[0], nil
		default:
			return influxql.Unknown, fmt.Errorf("invalid argument type for the first argument in %s(): %s", name, arg0)
		}
	}
	return influxql.Unknown, nil
}

type MathValuer struct{}

var _ influxql.CallValuer = MathValuer{}

func (MathValuer) Value(key string) (interface{}, bool) {
	return nil, false
}

func (v MathValuer) Call(name string, args []interface{}) (interface{}, bool) {
	if len(args) == 1 {
		arg0 := args[0]
		switch name {
		case "abs":
			switch arg0 := arg0.(type) {
			case float64:
				return math.Abs(arg0), true
			case int64:
				sign := arg0 >> 63
				return (arg0 ^ sign) - sign, true
			case uint64:
				return arg0, true
			default:
				return nil, true
			}
		case "sin":
			if arg0, ok := asFloat(arg0); ok {
				return math.Sin(arg0), true
			}
			return nil, true
		case "cos":
			if arg0, ok := asFloat(arg0); ok {
				return math.Cos(arg0), true
			}
			return nil, true
		case "tan":
			if arg0, ok := asFloat(arg0); ok {
				return math.Tan(arg0), true
			}
			return nil, true
		case "floor":
			switch arg0 := arg0.(type) {
			case float64:
				return math.Floor(arg0), true
			case int64, uint64:
				return arg0, true
			default:
				return nil, true
			}
		case "ceil":
			switch arg0 := arg0.(type) {
			case float64:
				return math.Ceil(arg0), true
			case int64, uint64:
				return arg0, true
			default:
				return nil, true
			}
		case "round":
			switch arg0 := arg0.(type) {
			case float64:
				return round(arg0), true
			case int64, uint64:
				return arg0, true
			default:
				return nil, true
			}
		case "asin":
			if arg0, ok := asFloat(arg0); ok {
				return math.Asin(arg0), true
			}
			return nil, true
		case "acos":
			if arg0, ok := asFloat(arg0); ok {
				return math.Acos(arg0), true
			}
			return nil, true
		case "atan":
			if arg0, ok := asFloat(arg0); ok {
				return math.Atan(arg0), true
			}
			return nil, true
		case "exp":
			if arg0, ok := asFloat(arg0); ok {
				return math.Exp(arg0), true
			}
			return nil, true
		case "ln":
			if arg0, ok := asFloat(arg0); ok {
				return math.Log(arg0), true
			}
			return nil, true
		case "log2":
			if arg0, ok := asFloat(arg0); ok {
				return math.Log2(arg0), true
			}
			return nil, true
		case "log10":
			if arg0, ok := asFloat(arg0); ok {
				return math.Log10(arg0), true
			}
			return nil, true
		case "sqrt":
			if arg0, ok := asFloat(arg0); ok {
				return math.Sqrt(arg0), true
			}
			return nil, true
		}
	} else if len(args) == 2 {
		arg0, arg1 := args[0], args[1]
		switch name {
		case "atan2":
			if arg0, arg1, ok := asFloats(arg0, arg1); ok {
				return math.Atan2(arg0, arg1), true
			}
			return nil, true
		case "log":
			if arg0, arg1, ok := asFloats(arg0, arg1); ok {
				return math.Log(arg0) / math.Log(arg1), true
			}
			return nil, true
		case "pow":
			if arg0, arg1, ok := asFloats(arg0, arg1); ok {
				return math.Pow(arg0, arg1), true
			}
			return nil, true
		}
	}
	return nil, false
}

func asFloat(x interface{}) (float64, bool) {
	switch arg0 := x.(type) {
	case float64:
		return arg0, true
	case int64:
		return float64(arg0), true
	case uint64:
		return float64(arg0), true
	default:
		return 0, false
	}
}

func asFloats(x, y interface{}) (float64, float64, bool) {
	arg0, ok := asFloat(x)
	if !ok {
		return 0, 0, false
	}
	arg1, ok := asFloat(y)
	if !ok {
		return 0, 0, false
	}
	return arg0, arg1, true
}

func round(x float64) float64 {
	t := math.Trunc(x)
	if math.Abs(x-t) >= 0.5 {
		return t + math.Copysign(1, x)
	}
	return t
}
