package strcase

import "unicode"

// SplitFn defines how to split a string into words
type SplitFn func(prev, curr, next rune) SplitAction

// NewSplitFn returns a SplitFn based on the options provided.
//
// NewSplitFn covers the majority of common options that other strcase
// libraries provide and should allow you to simply create a custom caser.
// For more complicated use cases, feel free to write your own SplitFn
//
//nolint:gocyclo
func NewSplitFn(
	delimiters []rune,
	splitOptions ...SplitOption,
) SplitFn {
	var splitCase, splitAcronym, splitBeforeNumber, splitAfterNumber, preserveNumberFormatting bool

	for _, option := range splitOptions {
		switch option {
		case SplitCase:
			splitCase = true
		case SplitAcronym:
			splitAcronym = true
		case SplitBeforeNumber:
			splitBeforeNumber = true
		case SplitAfterNumber:
			splitAfterNumber = true
		case PreserveNumberFormatting:
			preserveNumberFormatting = true
		}
	}

	return func(prev, curr, next rune) SplitAction {
		// The most common case will be that it's just a letter
		// There are safe cases to process
		if isLower(curr) && !isNumber(prev) {
			return Noop
		}
		if isUpper(prev) && isUpper(curr) && isUpper(next) {
			return Noop
		}

		if preserveNumberFormatting {
			if (curr == '.' || curr == ',') &&
				isNumber(prev) && isNumber(next) {
				return Noop
			}
		}

		if unicode.IsSpace(curr) {
			return SkipSplit
		}
		for _, d := range delimiters {
			if curr == d {
				return SkipSplit
			}
		}

		if splitBeforeNumber {
			if isNumber(curr) && !isNumber(prev) {
				if preserveNumberFormatting && (prev == '.' || prev == ',') {
					return Noop
				}
				return Split
			}
		}

		if splitAfterNumber {
			if isNumber(prev) && !isNumber(curr) {
				return Split
			}
		}

		if splitCase {
			if !isUpper(prev) && isUpper(curr) {
				return Split
			}
		}

		if splitAcronym {
			if isUpper(prev) && isUpper(curr) && isLower(next) {
				return Split
			}
		}

		return Noop
	}
}

// SplitOption are options that allow for configuring NewSplitFn
type SplitOption int

const (
	// SplitCase - FooBar -> Foo_Bar
	SplitCase SplitOption = iota
	// SplitAcronym - FOOBar -> Foo_Bar
	// It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps
	SplitAcronym
	// SplitBeforeNumber - port80 -> port_80
	SplitBeforeNumber
	// SplitAfterNumber - 200status -> 200_status
	SplitAfterNumber
	// PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c
	PreserveNumberFormatting
)

// SplitAction defines if and how to split a string
type SplitAction int

const (
	// Noop - Continue to next character
	Noop SplitAction = iota
	// Split - Split between words
	// e.g. to split between wordsWithoutDelimiters
	Split
	// SkipSplit - Split the word and drop the character
	// e.g. to split words with delimiters
	SkipSplit
	// Skip - Remove the character completely
	Skip
)

//nolint:gocyclo
func defaultSplitFn(prev, curr, next rune) SplitAction {
	// The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do
	if isLower(curr) {
		return Noop
	}
	// Delimiters are _, -, ., and unicode spaces
	// Handle . lower down as it needs to happen after number exceptions
	if curr == '_' || curr == '-' || isSpace(curr) {
		return SkipSplit
	}

	if isUpper(curr) {
		if isLower(prev) {
			// fooBar
			return Split
		} else if isUpper(prev) && isLower(next) {
			// FOOBar
			return Split
		}
	}

	// Do numeric exceptions last to avoid perf penalty
	if unicode.IsNumber(prev) {
		// v4.3 is not split
		if (curr == '.' || curr == ',') && unicode.IsNumber(next) {
			return Noop
		}
		if !unicode.IsNumber(curr) && curr != '.' {
			return Split
		}
	}
	// While period is a default delimiter, keep it down here to avoid
	// penalty for other delimiters
	if curr == '.' {
		return SkipSplit
	}

	return Noop
}
