package gstruct

import (
	"errors"
	"fmt"
	"reflect"
	"runtime/debug"

	"github.com/onsi/gomega/format"
	errorsutil "github.com/onsi/gomega/gstruct/errors"
	"github.com/onsi/gomega/types"
)

//MatchAllElements succeeds if every element of a slice matches the element matcher it maps to
//through the id function, and every element matcher is matched.
//  Expect([]string{"a", "b"}).To(MatchAllElements(idFn, matchers.Elements{
//      "a": BeEqual("a"),
//      "b": BeEqual("b"),
//  })
func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
	return &ElementsMatcher{
		Identifier: identifier,
		Elements:   elements,
	}
}

//MatchElements succeeds if each element of a slice matches the element matcher it maps to
//through the id function. It can ignore extra elements and/or missing elements.
//  Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing|IgnoreExtra, matchers.Elements{
//      "a": BeEqual("a")
//      "b": BeEqual("b"),
//  })
func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
	return &ElementsMatcher{
		Identifier:      identifier,
		Elements:        elements,
		IgnoreExtras:    options&IgnoreExtras != 0,
		IgnoreMissing:   options&IgnoreMissing != 0,
		AllowDuplicates: options&AllowDuplicates != 0,
	}
}

// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped
// by the Identifier function.
// TODO: Extend this to work with arrays & maps (map the key) as well.
type ElementsMatcher struct {
	// Matchers for each element.
	Elements Elements
	// Function mapping an element to the string key identifying its matcher.
	Identifier Identifier

	// Whether to ignore extra elements or consider it an error.
	IgnoreExtras bool
	// Whether to ignore missing elements or consider it an error.
	IgnoreMissing bool
	// Whether to key duplicates when matching IDs.
	AllowDuplicates bool

	// State.
	failures []error
}

// Element ID to matcher.
type Elements map[string]types.GomegaMatcher

// Function for identifying (mapping) elements.
type Identifier func(element interface{}) string

func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
	if reflect.TypeOf(actual).Kind() != reflect.Slice {
		return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
	}

	m.failures = m.matchElements(actual)
	if len(m.failures) > 0 {
		return false, nil
	}
	return true, nil
}

func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) {
	// Provide more useful error messages in the case of a panic.
	defer func() {
		if err := recover(); err != nil {
			errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack()))
		}
	}()

	val := reflect.ValueOf(actual)
	elements := map[string]bool{}
	for i := 0; i < val.Len(); i++ {
		element := val.Index(i).Interface()
		id := m.Identifier(element)
		if elements[id] {
			if !m.AllowDuplicates {
				errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
				continue
			}
		}
		elements[id] = true

		matcher, expected := m.Elements[id]
		if !expected {
			if !m.IgnoreExtras {
				errs = append(errs, fmt.Errorf("unexpected element %s", id))
			}
			continue
		}

		match, err := matcher.Match(element)
		if match {
			continue
		}

		if err == nil {
			if nesting, ok := matcher.(errorsutil.NestingMatcher); ok {
				err = errorsutil.AggregateError(nesting.Failures())
			} else {
				err = errors.New(matcher.FailureMessage(element))
			}
		}
		errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err))
	}

	for id := range m.Elements {
		if !elements[id] && !m.IgnoreMissing {
			errs = append(errs, fmt.Errorf("missing expected element %s", id))
		}
	}

	return errs
}

func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) {
	failure := errorsutil.AggregateError(m.failures)
	return format.Message(actual, fmt.Sprintf("to match elements: %v", failure))
}

func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	return format.Message(actual, "not to match elements")
}

func (m *ElementsMatcher) Failures() []error {
	return m.failures
}
