package matcher

import (
	"fmt"
	"reflect"

	"github.com/onsi/gomega/format"
	"github.com/onsi/gomega/types"
)

type collectionMatched struct {
	assertions       []types.GomegaMatcher
	successElements  []reflect.Value
	successPositions []int
	failedElements   []reflect.Value
	failedPositions  []int
}

func match(caller string, assertionGen assertionGen, actual interface{}) (*collectionMatched, *reflect.Value, error) {
	t := reflect.TypeOf(actual)
	if t.Kind() != reflect.Array && t.Kind() != reflect.Slice {
		return nil, nil, fmt.Errorf("unsupported type for %s: %T, only type Array and Slice are supportted.", caller, t)
	}

	v := reflect.ValueOf(actual)
	var result collectionMatched

	for i := 0; i < v.Len(); i++ {
		eRaw := v.Index(i)
		e := eRaw.Interface()

		result.assertions = append(result.assertions, assertionGen())
		r, err := result.assertions[i].Match(e)
		if err != nil {
			return nil, nil, err
		}

		if r {
			result.successElements = append(result.successElements, eRaw)
			result.successPositions = append(result.successPositions, i)
		} else {
			result.failedElements = append(result.failedElements, eRaw)
			result.failedPositions = append(result.failedPositions, i)
		}
	}

	return &result, &v, nil
}

func (matched *collectionMatched) failedElementsFailure(expectOn string, yetAddendum string, actual interface{}) string {
	var diagnose string
	for i, eRaw := range matched.failedElements {
		e := eRaw.Interface()
		diagnose += "\n"
		pos := matched.failedPositions[i]
		diagnose += fmt.Sprintf("At position %d: \n", pos)
		diagnose += format.IndentString(matched.assertions[pos].FailureMessage(e), 1)
	}

	return fmt.Sprintf("Expected %s element of \n%s\nmatches the assertion, yet %s:%s", expectOn, format.Object(actual, 1), yetAddendum, format.IndentString(diagnose, 1))
}

func (matched *collectionMatched) successElementsFailure(actual interface{}) string {
	var diagnose string
	for i, eRaw := range matched.successElements {
		e := eRaw.Interface()
		diagnose += "\n"
		pos := matched.successPositions[i]
		diagnose += fmt.Sprintf("At position %d: \n", pos)
		diagnose += format.IndentString(matched.assertions[pos].NegatedFailureMessage(e), 1)
	}

	return fmt.Sprintf("Expected ANY element of \n%s\nNOT matches the assertion, yet at these indexes, elements matches:%s", format.Object(actual, 1), format.IndentString(diagnose, 1))
}

func (matched *collectionMatched) matchMayChangeInTheFuture(actual interface{}) bool {
	if matched == nil {
		return true
	}
	for i, assertion := range matched.assertions {
		if oracle, ok := assertion.(types.OracleMatcher); ok {
			v := reflect.ValueOf(actual)
			if oracle.MatchMayChangeInTheFuture(v.Index(i).Interface()) {
				return true
			}
		}
	}
	return false
}

type assertionGen func() types.GomegaMatcher

type allMatcher struct {
	assertionGen assertionGen
	evaluated    bool

	matched *collectionMatched
}

// All asserts that the assertion passed in should pass for ALL elements of the array or slice.
// if not, it will print the failed element and its position as helpful diagnose message.
//
// Parameter f is the GomegaMatcher generator, you should pass in a function which returns a types.GomegaMatcher.
func All(f assertionGen) *allMatcher {
	return &allMatcher{
		evaluated:    false,
		assertionGen: f,
	}
}

func (matcher *allMatcher) Match(actual interface{}) (success bool, err error) {
	matched, v, err := match("All", matcher.assertionGen, actual)
	if err != nil {
		return false, err
	}

	matcher.evaluated = true
	matcher.matched = matched

	return len(matched.successElements) == v.Len(), err
}

func (matcher *allMatcher) FailureMessage(actual interface{}) (message string) {
	if !matcher.evaluated {
		panic("you should call Match to execute matching first.")
	}
	return matcher.matched.failedElementsFailure("ALL", "some failed", actual)
}

func (matcher *allMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	if !matcher.evaluated {
		panic("you should call Match to execute matching on first.")
	}
	return matcher.matched.successElementsFailure(actual)
}

func (matcher *allMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
	return matcher.matched.matchMayChangeInTheFuture(actual)
}

type anyMatcher struct {
	assertionGen assertionGen
	evaluated    bool

	matched *collectionMatched
}

// Any asserts that the assertion passed in should pass for AT LEAST ONE element of the array or slice.
// if not, it will print all passed elements and its position as helpful diagnose message.
//
// Parameter f is the GomegaMatcher generator, you should pass in a function which returns a types.GomegaMatcher.
func Any(f func() types.GomegaMatcher) *anyMatcher {
	return &anyMatcher{
		evaluated:    false,
		assertionGen: f,
	}
}

func (matcher *anyMatcher) Match(actual interface{}) (success bool, err error) {
	matched, _, err := match("Any", matcher.assertionGen, actual)
	if err != nil {
		return false, err
	}

	matcher.evaluated = true
	matcher.matched = matched

	return len(matched.successElements) > 0, err
}

func (matcher *anyMatcher) FailureMessage(actual interface{}) (message string) {
	if !matcher.evaluated {
		panic("you should call Match to execute matching first.")
	}
	return matcher.matched.failedElementsFailure("ANY", "they all failed", actual)
}

func (matcher *anyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	if !matcher.evaluated {
		panic("you should call Match to execute matching first.")
	}
	return matcher.matched.successElementsFailure(actual)
}

func (matcher *anyMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
	return matcher.matched.matchMayChangeInTheFuture(actual)
}
