utils.gno

package router

import (
	"errors"
	"bytes"
	"strconv"
	"strings"

	gnsmath "gno.land/p/gnoswap/gnsmath"
	"gno.land/p/gnoswap/utils"
	ufmt "gno.land/p/nt/ufmt/v0"
	"gno.land/r/gnoswap/pool"

	i256 "gno.land/p/gnoswap/int256"
	u256 "gno.land/p/gnoswap/uint256"
)

// calculateSwapAmountByQuote calculates swap amount based on quote percentage.
func calculateSwapAmountByQuote(amountSpecified int64, quote string) (int64, error) {
	quoteInt, err := strconv.ParseInt(quote, 10, 64)
	if err != nil {
		return 0, ufmt.Errorf("invalid quote(%s)", quote)
	}

	if quoteInt < MinQuotePercentage || quoteInt > MaxQuotePercentage {
		return 0, ufmt.Errorf(ErrInvalidQuoteRange, quoteInt, MinQuotePercentage, MaxQuotePercentage)
	}

	toSwap := gnsmath.SafeMulDivInt64(amountSpecified, quoteInt, PERCENTAGE_DENOMINATOR)
	if toSwap == 0 {
		return 0, errors.New(errInvalidSwapAmount)
	}

	return toSwap, nil
}

// assertHopsInRange ensures the number of hops is within the valid range of 1-3.
func assertHopsInRange(hops int) {
	switch hops {
	case 1, 2, 3:
		return
	default:
		panic(errors.New(errHopsOutOfRange))
	}
}

// getDataForSinglePath extracts token addresses and fee from a single pool path.
//
// IMPORTANT: This function returns tokens in the order they appear in the route string,
// which represents the swap direction (tokenIn:tokenOut:fee), NOT the canonical pool ordering.
func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) {
	token0, token1, fee, err := getDataForSinglePathWithError(poolPath)
	if err != nil {
		panic(err)
	}

	return token0, token1, fee
}

// getDataForSinglePathWithError extracts token addresses and fee from a single pool path with error handling.
func getDataForSinglePathWithError(poolPath string) (string, string, uint32, error) {
	poolPathSplit := strings.Split(poolPath, ":")
	if len(poolPathSplit) != 3 {
		return "", "", 0, makeErrorWithDetails(
			errInvalidPoolPath,
			ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath),
		)
	}

	poolPathSplit[0] = strings.TrimSpace(poolPathSplit[0])
	poolPathSplit[1] = strings.TrimSpace(poolPathSplit[1])

	if poolPathSplit[0] == "" || poolPathSplit[1] == "" {
		return "", "", 0, makeErrorWithDetails(
			errInvalidPoolPath,
			ufmt.Sprintf("token addresses cannot be empty: %s", poolPath),
		)
	}

	f, err := strconv.Atoi(poolPathSplit[2])
	if err != nil {
		return "", "", 0, makeErrorWithDetails(
			errInvalidPoolPath,
			ufmt.Sprintf("invalid fee: %s", poolPathSplit[2]),
		)
	}

	return poolPathSplit[0], poolPathSplit[1], uint32(f), nil
}

// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index.
func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) {
	pools := strings.Split(possiblePath, POOL_SEPARATOR)

	switch poolIdx {
	case 0:
		return getDataForSinglePath(pools[0])
	case 1:
		return getDataForSinglePath(pools[1])
	case 2:
		return getDataForSinglePath(pools[2])
	default:
		return "", "", uint32(0)
	}
}

// i256MinMax returns the absolute values of x and y in min-max order.
func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) {
	if x.Lt(y) || x.Eq(y) {
		return x.Abs(), y.Abs()
	}
	return y.Abs(), x.Abs()
}

// validateRoutePaths validates multiple route paths to ensure they all start with inputToken and end with outputToken.
// This function processes comma-separated route paths and validates each path individually.
//
// Validates:
// - Each route path starts with the specified inputToken
// - Each route path ends with the specified outputToken
// - Route path format consistency (prevents swap-direction vs alphabetical pool ordering confusion)
//
// Parameters:
// - routePathArrString: comma-separated route paths (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/wugnot:gno.land/r/demo/gns:3000*POOL*gno.land/r/demo/gns:gno.land/r/demo/usdc:500")
// - inputToken: expected first token in all route paths
// - outputToken: expected last token in all route paths
//
// Examples:
// - Single route: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
// - Multi-route: "tokenA:tokenB:500,tokenA:tokenC:3000*POOL*tokenC:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
//
// Returns error if any route path validation fails.
func validateRoutePaths(routePathArrString, inputToken, outputToken string) error {
	routePaths := strings.Split(routePathArrString, ",")

	for _, routePath := range routePaths {
		if err := validateRoutePath(routePath, inputToken, outputToken); err != nil {
			return err
		}
	}

	return nil
}

// validateRoutePath validates a single route path to ensure it starts with inputToken and ends with outputToken.
// This function handles both single-hop and multi-hop route paths.
//
// Validates:
// - Route path starts with the specified inputToken
// - Route path ends with the specified outputToken
// - Proper token ordering in swap direction (not alphabetical pool ordering)
//
// Route Path Formats:
// - single-hop: "tokenA:tokenB:fee" (direct swap between two tokens)
// - multi-hop: "tokenA:tokenB:fee1*POOL*tokenB:tokenC:fee2" (swap through intermediate tokens)
//
// Parameters:
// - routePath: single route path string (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500")
// - inputToken: expected first token in the route path
// - outputToken: expected last token in the route path
//
// Examples:
// - single-hop: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
// - multi-hop: "tokenA:tokenB:3000*POOL*tokenB:tokenC:500" with inputToken="tokenA", outputToken="tokenC"
//
// Returns error with specific details if validation fails.
func validateRoutePath(routePath, inputToken, outputToken string) error {
	// Extract first and last tokens from the routePath
	var (
		firstToken, lastToken string
		err                   error
	)

	// multi-hop routePath
	if strings.Contains(routePath, POOL_SEPARATOR) {
		pools := strings.Split(routePath, POOL_SEPARATOR)

		// Get first token from first pool
		firstPool := pools[0]
		firstToken, _, _, err = getDataForSinglePathWithError(firstPool)
		if err != nil {
			return err
		}

		// Validate hop-to-hop continuity: output token of hop N must equal input token of hop N+1
		err = validatePoolPathHopContinuity(pools)
		if err != nil {
			return err
		}

		// Get last token from last pool
		lastPool := pools[len(pools)-1]
		_, lastToken, _, err = getDataForSinglePathWithError(lastPool)
		if err != nil {
			return err
		}
	} else {
		// single-hop routePath
		firstToken, lastToken, _, err = getDataForSinglePathWithError(routePath)
		if err != nil {
			return err
		}
	}

	if firstToken == "" || lastToken == "" {
		return makeErrorWithDetails(errInvalidRoutePath, ufmt.Sprintf("firstToken: %s, lastToken: %s", firstToken, lastToken))
	}

	// Validate consistency
	if firstToken != inputToken {
		return makeErrorWithDetails(errInvalidRouteFirstToken, ufmt.Sprintf("firstToken: %s, inputToken: %s", firstToken, inputToken))
	}

	if lastToken != outputToken {
		return makeErrorWithDetails(errInvalidRouteLastToken, ufmt.Sprintf("lastToken: %s, outputToken: %s", lastToken, outputToken))
	}

	return nil
}

func validatePoolPathHopContinuity(routePoolPaths []string) error {
	if len(routePoolPaths) < 2 {
		return nil
	}

	_, previousOut, _, pErr := getDataForSinglePathWithError(routePoolPaths[0])
	if pErr != nil {
		return pErr
	}

	for i := 1; i < len(routePoolPaths); i++ {
		nextIn, nextOut, _, nErr := getDataForSinglePathWithError(routePoolPaths[i])
		if nErr != nil {
			return nErr
		}

		if previousOut != nextIn {
			return makeErrorWithDetails(
				errRouteHopDisconnected,
				ufmt.Sprintf("hop %d output(%s) != hop %d input(%s)", i-1, previousOut, i, nextIn),
			)
		}

		previousOut = nextOut
	}

	return nil
}

// splitSingleChar splits a string by a single character separator.
// This function is optimized for splitting strings with a single-byte separator
// and is more memory efficient than strings.Split for this use case.
func splitSingleChar(s string, sep byte) []string {
	if s == "" {
		return []string{""}
	}

	result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1)
	start := 0
	for i := range s {
		if s[i] == sep {
			result = append(result, s[start:i])
			start = i + 1
		}
	}
	result = append(result, s[start:])
	return result
}

// parsePoolPathsByRoutePathArr parses route path array string and returns a slice of pool paths.
// This function converts route paths (which maintain swap direction) into canonical pool paths
// (which use alphabetical token ordering).
//
// The function processes comma-separated route paths and extracts individual pool information
// from each route, ensuring tokens are ordered alphabetically for consistent pool identification.
//
// Parameters:
//   - routePathArr: comma-separated route paths string containing single or multi-hop routes
//     Format examples:
//   - Single route: "tokenA:tokenB:500"
//   - Multiple routes: "tokenA:tokenB:500,tokenC:tokenD:3000"
//   - Multi-hop routes: "tokenA:tokenB:500*POOL*tokenB:tokenC:3000"
//
// Returns:
// - []string: slice of canonical pool paths with alphabetically ordered tokens
// - error: parsing error if any route path is invalid
//
// Example:
//
//	input: "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/usdc:gno.land/r/demo/gns:3000"
//	output: ["gno.land/r/demo/usdc:gno.land/r/demo/wugnot:500", "gno.land/r/demo/gns:gno.land/r/demo/usdc:3000"]
func parsePoolPathsByRoutePathArr(routePathArr string) ([]string, error) {
	poolPaths := make([]string, 0)

	for _, routePaths := range strings.Split(routePathArr, ",") {
		for _, routePath := range strings.Split(routePaths, POOL_SEPARATOR) {
			token0Path, token1Path, fee, err := getDataForSinglePathWithError(routePath)
			if err != nil {
				return []string{}, err
			}

			if token0Path > token1Path {
				token0Path, token1Path = token1Path, token0Path
			}

			poolPath := pool.GetPoolPath(token0Path, token1Path, fee)
			poolPaths = append(poolPaths, poolPath)
		}
	}

	return poolPaths, nil
}

// BuildSingleHopPath creates a single-hop route path string.
// Format: "tokenA:tokenB:fee"
//
// Parameters:
// - tokenA: input token address
// - tokenB: output token address
// - fee: pool fee (e.g., 500, 3000, 10000)
//
// Returns:
// - string: formatted single-hop route path
//
// Example:
//   - BuildSingleHopPath("gno.land/r/demo/wugnot", "gno.land/r/demo/usdc", 500)
//     returns "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500"
func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
	if tokenA == "" || tokenB == "" {
		panic("token addresses cannot be empty")
	}
	if tokenA == tokenB {
		panic("tokenA and tokenB cannot be the same")
	}

	return tokenA + ":" + tokenB + ":" + utils.FormatUint(fee)
}