// Copyright 2015 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
	"fmt"
	"math"
	"time"

	"github.com/pingcap/errors"
)

// AddUint64 adds uint64 a and b if no overflow, else returns error.
func AddUint64(a uint64, b uint64) (uint64, error) {
	if math.MaxUint64-a < b {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a + b, nil
}

// AddInt64 adds int64 a and b if no overflow, otherwise returns error.
func AddInt64(a int64, b int64) (int64, error) {
	if (a > 0 && b > 0 && math.MaxInt64-a < b) ||
		(a < 0 && b < 0 && math.MinInt64-a > b) {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
	}

	return a + b, nil
}

// AddDuration adds time.Duration a and b if no overflow, otherwise returns error.
func AddDuration(a time.Duration, b time.Duration) (time.Duration, error) {
	if (a > 0 && b > 0 && math.MaxInt64-a < b) ||
		(a < 0 && b < 0 && math.MinInt64-a > b) {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", int64(a), int64(b)))
	}

	return a + b, nil
}

// SubDuration subtracts time.Duration a with b and returns time.Duration if no overflow error.
func SubDuration(a time.Duration, b time.Duration) (time.Duration, error) {
	if (a > 0 && b < 0 && math.MaxInt64-a < -b) ||
		(a < 0 && b > 0 && math.MinInt64-a > -b) ||
		(a == 0 && b == math.MinInt64) {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a - b, nil
}

// AddInteger adds uint64 a and int64 b and returns uint64 if no overflow error.
func AddInteger(a uint64, b int64) (uint64, error) {
	if b >= 0 {
		return AddUint64(a, uint64(b))
	}

	if uint64(-b) > a {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a - uint64(-b), nil
}

// SubUint64 subtracts uint64 a with b and returns uint64 if no overflow error.
func SubUint64(a uint64, b uint64) (uint64, error) {
	if a < b {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a - b, nil
}

// SubInt64 subtracts int64 a with b and returns int64 if no overflow error.
func SubInt64(a int64, b int64) (int64, error) {
	if (a > 0 && b < 0 && math.MaxInt64-a < -b) ||
		(a < 0 && b > 0 && math.MinInt64-a > -b) ||
		(a == 0 && b == math.MinInt64) {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a - b, nil
}

// SubUintWithInt subtracts uint64 a with int64 b and returns uint64 if no overflow error.
func SubUintWithInt(a uint64, b int64) (uint64, error) {
	if b < 0 {
		return AddUint64(a, uint64(-b))
	}
	return SubUint64(a, uint64(b))
}

// SubIntWithUint subtracts int64 a with uint64 b and returns uint64 if no overflow error.
func SubIntWithUint(a int64, b uint64) (uint64, error) {
	if a < 0 || uint64(a) < b {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}
	return uint64(a) - b, nil
}

// MulUint64 multiplies uint64 a and b and returns uint64 if no overflow error.
func MulUint64(a uint64, b uint64) (uint64, error) {
	if b > 0 && a > math.MaxUint64/b {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}
	return a * b, nil
}

// MulInt64 multiplies int64 a and b and returns int64 if no overflow error.
func MulInt64(a int64, b int64) (int64, error) {
	if a == 0 || b == 0 {
		return 0, nil
	}

	var (
		res      uint64
		err      error
		negative = false
	)

	if a > 0 && b > 0 {
		res, err = MulUint64(uint64(a), uint64(b))
	} else if a < 0 && b < 0 {
		res, err = MulUint64(uint64(-a), uint64(-b))
	} else if a < 0 && b > 0 {
		negative = true
		res, err = MulUint64(uint64(-a), uint64(b))
	} else {
		negative = true
		res, err = MulUint64(uint64(a), uint64(-b))
	}

	if err != nil {
		return 0, errors.Trace(err)
	}

	if negative {
		// negative result
		if res > math.MaxInt64+1 {
			return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
		}

		return -int64(res), nil
	}

	// positive result
	if res > math.MaxInt64 {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
	}

	return int64(res), nil
}

// MulInteger multiplies uint64 a and int64 b, and returns uint64 if no overflow error.
func MulInteger(a uint64, b int64) (uint64, error) {
	if a == 0 || b == 0 {
		return 0, nil
	}

	if b < 0 {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
	}

	return MulUint64(a, uint64(b))
}

// DivInt64 divides int64 a with b, returns int64 if no overflow error.
// It just checks overflow, if b is zero, a "divide by zero" panic throws.
func DivInt64(a int64, b int64) (int64, error) {
	if a == math.MinInt64 && b == -1 {
		return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
	}

	return a / b, nil
}

// DivUintWithInt divides uint64 a with int64 b, returns uint64 if no overflow error.
// It just checks overflow, if b is zero, a "divide by zero" panic throws.
func DivUintWithInt(a uint64, b int64) (uint64, error) {
	if b < 0 {
		if a != 0 && uint64(-b) <= a {
			return 0, ErrOverflow.GenWithStackByArgs("BIGINT UNSIGNED", fmt.Sprintf("(%d, %d)", a, b))
		}

		return 0, nil
	}

	return a / uint64(b), nil
}

// DivIntWithUint divides int64 a with uint64 b, returns uint64 if no overflow error.
// It just checks overflow, if b is zero, a "divide by zero" panic throws.
func DivIntWithUint(a int64, b uint64) (uint64, error) {
	if a < 0 {
		if uint64(-a) >= b {
			return 0, ErrOverflow.GenWithStackByArgs("BIGINT", fmt.Sprintf("(%d, %d)", a, b))
		}

		return 0, nil
	}

	return uint64(a) / b, nil
}
