package esquery

import (
	"github.com/fatih/structs"
)

type matchType uint8

const (
	// TypeMatch denotes a query of type "match"
	TypeMatch matchType = iota

	// TypeMatchBool denotes a query of type "match_bool_prefix"
	TypeMatchBoolPrefix

	// TypeMatchPhrase denotes a query of type "match_phrase"
	TypeMatchPhrase

	// TypeMatchPhrasePrefix denotes a query of type "match_phrase_prefix"
	TypeMatchPhrasePrefix
)

// MatchQuery represents a query of type "match", "match_bool_prefix",
// "match_phrase" and "match_phrase_prefix". While all four share the same
// general structure, they don't necessarily support all the same options. The
// library does not attempt to verify provided options are supported.
// See the ElasticSearch documentation for more information:
// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html
// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html
// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html
type MatchQuery struct {
	field  string
	mType  matchType
	params matchParams
}

// Map returns a map representation of the query, thus implementing the
// Mappable interface.
func (q *MatchQuery) Map() map[string]interface{} {
	var mType string
	switch q.mType {
	case TypeMatch:
		mType = "match"
	case TypeMatchBoolPrefix:
		mType = "match_bool_prefix"
	case TypeMatchPhrase:
		mType = "match_phrase"
	case TypeMatchPhrasePrefix:
		mType = "match_phrase_prefix"
	}

	return map[string]interface{}{
		mType: map[string]interface{}{
			q.field: structs.Map(q.params),
		},
	}
}

type matchParams struct {
	Qry          interface{}   `structs:"query"`
	Anl          string        `structs:"analyzer,omitempty"`
	AutoGenerate *bool         `structs:"auto_generate_synonyms_phrase_query,omitempty"`
	Fuzz         string        `structs:"fuzziness,omitempty"`
	MaxExp       uint16        `structs:"max_expansions,omitempty"`
	PrefLen      uint16        `structs:"prefix_length,omitempty"`
	Trans        *bool         `structs:"transpositions,omitempty"`
	FuzzyRw      string        `structs:"fuzzy_rewrite,omitempty"`
	Lent         bool          `structs:"lenient,omitempty"`
	Op           MatchOperator `structs:"operator,string,omitempty"`
	MinMatch     string        `structs:"minimum_should_match,omitempty"`
	ZeroTerms    ZeroTerms     `structs:"zero_terms_query,string,omitempty"`
	Slp          uint16        `structs:"slop,omitempty"` // only relevant for match_phrase query
}

// Match creates a new query of type "match" with the provided field name.
// A comparison value can optionally be provided to quickly create a simple
// query such as { "match": { "message": "this is a test" } }
func Match(fieldName string, simpleQuery ...interface{}) *MatchQuery {
	return newMatch(TypeMatch, fieldName, simpleQuery...)
}

// MatchBoolPrefix creates a new query of type "match_bool_prefix" with the
// provided field name. A comparison value can optionally be provided to quickly
// create a simple query such as { "match": { "message": "this is a test" } }
func MatchBoolPrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery {
	return newMatch(TypeMatchBoolPrefix, fieldName, simpleQuery...)
}

// MatchPhrase creates a new query of type "match_phrase" with the
// provided field name. A comparison value can optionally be provided to quickly
// create a simple query such as { "match": { "message": "this is a test" } }
func MatchPhrase(fieldName string, simpleQuery ...interface{}) *MatchQuery {
	return newMatch(TypeMatchPhrase, fieldName, simpleQuery...)
}

// MatchPhrasePrefix creates a new query of type "match_phrase_prefix" with the
// provided field name. A comparison value can optionally be provided to quickly
// create a simple query such as { "match": { "message": "this is a test" } }
func MatchPhrasePrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery {
	return newMatch(TypeMatchPhrasePrefix, fieldName, simpleQuery...)
}

func newMatch(mType matchType, fieldName string, simpleQuery ...interface{}) *MatchQuery {
	var qry interface{}
	if len(simpleQuery) > 0 {
		qry = simpleQuery[len(simpleQuery)-1]
	}

	return &MatchQuery{
		field: fieldName,
		mType: mType,
		params: matchParams{
			Qry: qry,
		},
	}
}

// Query sets the data to find in the query's field (it is the "query" component
// of the query).
func (q *MatchQuery) Query(data interface{}) *MatchQuery {
	q.params.Qry = data
	return q
}

// Analyzer sets the analyzer used to convert the text in the "query" value into
// tokens.
func (q *MatchQuery) Analyzer(a string) *MatchQuery {
	q.params.Anl = a
	return q
}

// AutoGenerateSynonymsPhraseQuery sets the "auto_generate_synonyms_phrase_query"
// boolean.
func (q *MatchQuery) AutoGenerateSynonymsPhraseQuery(b bool) *MatchQuery {
	q.params.AutoGenerate = &b
	return q
}

// Fuzziness set the maximum edit distance allowed for matching.
func (q *MatchQuery) Fuzziness(f string) *MatchQuery {
	q.params.Fuzz = f
	return q
}

// MaxExpansions sets the maximum number of terms to which the query will expand.
func (q *MatchQuery) MaxExpansions(e uint16) *MatchQuery {
	q.params.MaxExp = e
	return q
}

// PrefixLength sets the number of beginning characters left unchanged for fuzzy
// matching.
func (q *MatchQuery) PrefixLength(l uint16) *MatchQuery {
	q.params.PrefLen = l
	return q
}

// Transpositions sets whether edits for fuzzy matching include transpositions
// of two adjacent characters.
func (q *MatchQuery) Transpositions(b bool) *MatchQuery {
	q.params.Trans = &b
	return q
}

// FuzzyRewrite sets the method used to rewrite the query.
func (q *MatchQuery) FuzzyRewrite(s string) *MatchQuery {
	q.params.FuzzyRw = s
	return q
}

// Lenient sets whether format-based errors should be ignored.
func (q *MatchQuery) Lenient(b bool) *MatchQuery {
	q.params.Lent = b
	return q
}

// Operator sets the boolean logic used to interpret text in the query value.
func (q *MatchQuery) Operator(op MatchOperator) *MatchQuery {
	q.params.Op = op
	return q
}

// MinimumShouldMatch sets the minimum number of clauses that must match for a
// document to be returned.
func (q *MatchQuery) MinimumShouldMatch(s string) *MatchQuery {
	q.params.MinMatch = s
	return q
}

// Slop sets the maximum number of positions allowed between matching tokens.
func (q *MatchQuery) Slop(n uint16) *MatchQuery {
	q.params.Slp = n
	return q
}

// ZeroTermsQuery sets the "zero_terms_query" option to use. This indicates
// whether no documents are returned if the analyzer removes all tokens, such as
// when using a stop filter.
func (q *MatchQuery) ZeroTermsQuery(s ZeroTerms) *MatchQuery {
	q.params.ZeroTerms = s
	return q
}

// MatchOperator is an enumeration type representing supported values for a
// match query's "operator" parameter.
type MatchOperator uint8

const (
	// OperatorOr is the "or" operator
	OperatorOr MatchOperator = iota

	// OperatorAnd is the "and" operator
	OperatorAnd
)

// String returns a string representation of the match operator, as known to
// ElasticSearch.
func (a MatchOperator) String() string {
	switch a {
	case OperatorOr:
		return "OR"
	case OperatorAnd:
		return "AND"
	default:
		return ""
	}
}

// ZeroTerms is an enumeration type representing supported values for a match
// query's "zero_terms_query" parameter.
type ZeroTerms uint8

const (
	// ZeroTermsNone is the "none" value
	ZeroTermsNone ZeroTerms = iota

	// ZeroTermsAll is the "all" value
	ZeroTermsAll
)

// String returns a string representation of the zero_terms_query parameter, as
// known to ElasticSearch.
func (a ZeroTerms) String() string {
	switch a {
	case ZeroTermsNone:
		return "none"
	case ZeroTermsAll:
		return "all"
	default:
		return ""
	}
}