// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
//
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for license information.

/*
Package plurals is the pluralform compiler to get the correct translation id of the plural string
*/
package plurals

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

type match struct {
	openPos  int
	closePos int
}

var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)

type testToken interface {
	compile(tokens []string) (test test, err error)
}

type cmpTestBuilder func(val uint32, flipped bool) test
type logicTestBuild func(left test, right test) test

var ternaryToken ternaryStruct

type ternaryStruct struct{}

func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
	main, err := splitTokens(tokens, "?")
	if err != nil {
		return expr, err
	}
	test, err := compileTest(strings.Join(main.Left, ""))
	if err != nil {
		return expr, err
	}
	actions, err := splitTokens(main.Right, ":")
	if err != nil {
		return expr, err
	}
	trueAction, err := compileExpression(strings.Join(actions.Left, ""))
	if err != nil {
		return expr, err
	}
	falseAction, err := compileExpression(strings.Join(actions.Right, ""))
	if err != nil {
		return expr, nil
	}
	return ternary{
		test:      test,
		trueExpr:  trueAction,
		falseExpr: falseAction,
	}, nil
}

var constToken constValStruct

type constValStruct struct{}

func (constValStruct) compile(tokens []string) (expr Expression, err error) {
	if len(tokens) == 0 {
		return expr, errors.New("got nothing instead of constant")
	}
	if len(tokens) != 1 {
		return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
	}
	i, err := strconv.Atoi(tokens[0])
	if err != nil {
		return expr, err
	}
	return constValue{value: i}, nil
}

func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
	split, err := splitTokens(tokens, sep)
	if err != nil {
		return test, err
	}
	left, err := compileTest(strings.Join(split.Left, ""))
	if err != nil {
		return test, err
	}
	right, err := compileTest(strings.Join(split.Right, ""))
	if err != nil {
		return test, err
	}
	return builder(left, right), nil
}

var orToken orStruct

type orStruct struct{}

func (orStruct) compile(tokens []string) (test test, err error) {
	return compileLogicTest(tokens, "||", buildOr)
}
func buildOr(left test, right test) test {
	return or{left: left, right: right}
}

var andToken andStruct

type andStruct struct{}

func (andStruct) compile(tokens []string) (test test, err error) {
	return compileLogicTest(tokens, "&&", buildAnd)
}
func buildAnd(left test, right test) test {
	return and{left: left, right: right}
}

func compileMod(tokens []string) (math math, err error) {
	split, err := splitTokens(tokens, "%")
	if err != nil {
		return math, err
	}
	if len(split.Left) != 1 || split.Left[0] != "n" {
		return math, errors.New("modulus operation requires 'n' as left operand")
	}
	if len(split.Right) != 1 {
		return math, errors.New("modulus operation requires simple integer as right operand")
	}
	i, err := parseUint32(split.Right[0])
	if err != nil {
		return math, err
	}
	return mod{value: uint32(i)}, nil
}

func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
	modifier, err := compileMod(modTokens)
	if err != nil {
		return test, err
	}
	if len(actionTokens) != 1 {
		return test, errors.New("can only get modulus of integer")
	}
	i, err := parseUint32(actionTokens[0])
	if err != nil {
		return test, err
	}
	action := builder(uint32(i), flipped)
	return pipe{
		modifier: modifier,
		action:   action,
	}, nil
}

func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
	split, err := splitTokens(tokens, sep)
	if err != nil {
		return test, err
	}
	if len(split.Left) == 1 && split.Left[0] == "n" {
		if len(split.Right) != 1 {
			return test, errors.New("test can only compare n to integers")
		}
		i, err := parseUint32(split.Right[0])
		if err != nil {
			return test, err
		}
		return builder(i, false), nil
	} else if len(split.Right) == 1 && split.Right[0] == "n" {
		if len(split.Left) != 1 {
			return test, errors.New("test can only compare n to integers")
		}
		i, err := parseUint32(split.Left[0])
		if err != nil {
			return test, err
		}
		return builder(i, true), nil
	} else if contains(split.Left, "n") && contains(split.Left, "%") {
		return subPipe(split.Left, split.Right, builder, false)
	}
	return test, errors.New("equality test must have 'n' as one of the two tests")

}

var eqToken eqStruct

type eqStruct struct{}

func (eqStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, "==", buildEq)
}
func buildEq(val uint32, flipped bool) test {
	return equal{value: val}
}

var neqToken neqStruct

type neqStruct struct{}

func (neqStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, "!=", buildNeq)
}
func buildNeq(val uint32, flipped bool) test {
	return notequal{value: val}
}

var gtToken gtStruct

type gtStruct struct{}

func (gtStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, ">", buildGt)
}
func buildGt(val uint32, flipped bool) test {
	return gt{value: val, flipped: flipped}
}

var gteToken gteStruct

type gteStruct struct{}

func (gteStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, ">=", buildGte)
}
func buildGte(val uint32, flipped bool) test {
	return gte{value: val, flipped: flipped}
}

var ltToken ltStruct

type ltStruct struct{}

func (ltStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, "<", buildLt)
}
func buildLt(val uint32, flipped bool) test {
	return lt{value: val, flipped: flipped}
}

var lteToken lteStruct

type lteStruct struct{}

func (lteStruct) compile(tokens []string) (test test, err error) {
	return compileEquality(tokens, "<=", buildLte)
}
func buildLte(val uint32, flipped bool) test {
	return lte{value: val, flipped: flipped}
}

type testTokenDef struct {
	op    string
	token testToken
}

var precedence = []testTokenDef{
	{op: "||", token: orToken},
	{op: "&&", token: andToken},
	{op: "==", token: eqToken},
	{op: "!=", token: neqToken},
	{op: ">=", token: gteToken},
	{op: ">", token: gtToken},
	{op: "<=", token: lteToken},
	{op: "<", token: ltToken},
}

type splitted struct {
	Left  []string
	Right []string
}

// Find index of token in list of tokens
func index(tokens []string, sep string) int {
	for index, token := range tokens {
		if token == sep {
			return index
		}
	}
	return -1
}

// Split a list of tokens by a token into a splitted struct holding the tokens
// before and after the token to be split by.
func splitTokens(tokens []string, sep string) (s splitted, err error) {
	index := index(tokens, sep)
	if index == -1 {
		return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
	}
	return splitted{
		Left:  tokens[:index],
		Right: tokens[index+1:],
	}, nil
}

// Scan a string for parenthesis
func scan(s string) <-chan match {
	ch := make(chan match)
	go func() {
		depth := 0
		opener := 0
		for index, char := range s {
			switch char {
			case '(':
				if depth == 0 {
					opener = index
				}
				depth++
			case ')':
				depth--
				if depth == 0 {
					ch <- match{
						openPos:  opener,
						closePos: index + 1,
					}
				}
			}

		}
		close(ch)
	}()
	return ch
}

// Split the string into tokens
func split(s string) <-chan string {
	ch := make(chan string)
	go func() {
		s = strings.ReplaceAll(s, " ", "")
		if !strings.Contains(s, "(") {
			ch <- s
		} else {
			last := 0
			end := len(s)
			for info := range scan(s) {
				if last != info.openPos {
					ch <- s[last:info.openPos]
				}
				ch <- s[info.openPos:info.closePos]
				last = info.closePos
			}
			if last != end {
				ch <- s[last:]
			}
		}
		close(ch)
	}()
	return ch
}

// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
// not split! If the string starts with ( and ends in ), those are stripped.
func tokenize(s string) []string {
	/*
		TODO: Properly detect if the string starts with a ( and ends with a )
		and that those two form a matching pair.

		Eg: (foo) -> true; (foo)(bar) -> false;
	*/
	if len(s) == 0 {
		return []string{}
	}
	if s[0] == '(' && s[len(s)-1] == ')' {
		s = s[1 : len(s)-1]
	}
	ret := []string{}
	for chunk := range split(s) {
		if len(chunk) != 0 {
			if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
				ret = append(ret, chunk)
			} else {
				for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
					ret = append(ret, token[0])
				}
			}
		} else {
			fmt.Printf("Empty chunk in string '%s'\n", s)
		}
	}
	return ret
}

// Compile a string containing a plural form expression to a Expression object.
func Compile(s string) (expr Expression, err error) {
	if s == "0" {
		return constValue{value: 0}, nil
	}
	if !strings.Contains(s, "?") {
		s += "?1:0"
	}
	return compileExpression(s)
}

// Check if a token is in a slice of strings
func contains(haystack []string, needle string) bool {
	for _, s := range haystack {
		if s == needle {
			return true
		}
	}
	return false
}

// Compiles an expression (ternary or constant)
func compileExpression(s string) (expr Expression, err error) {
	tokens := tokenize(s)
	if contains(tokens, "?") {
		return ternaryToken.compile(tokens)
	}
	return constToken.compile(tokens)
}

// Compiles a test (comparison)
func compileTest(s string) (test test, err error) {
	tokens := tokenize(s)
	for _, tokenDef := range precedence {
		if contains(tokens, tokenDef.op) {
			return tokenDef.token.compile(tokens)
		}
	}
	return test, errors.New("cannot compile")
}

func parseUint32(s string) (ui uint32, err error) {
	i, err := strconv.ParseUint(s, 10, 32)
	if err != nil {
		return ui, err
	}
	return uint32(i), nil
}
