package mapping

import (
	"github.com/omniscale/imposm3/element"
	"github.com/omniscale/imposm3/geom"
)

func (m *Mapping) PointMatcher() NodeMatcher {
	mappings := make(TagTables)
	m.mappings("point", mappings)
	filters := m.ElementFilters()
	return &tagMatcher{mappings, m.tables("point"), filters, false}
}

func (m *Mapping) LineStringMatcher() WayMatcher {
	mappings := make(TagTables)
	m.mappings("linestring", mappings)
	filters := m.ElementFilters()
	return &tagMatcher{mappings, m.tables("linestring"), filters, false}
}

func (m *Mapping) PolygonMatcher() RelWayMatcher {
	mappings := make(TagTables)
	m.mappings("polygon", mappings)
	filters := m.ElementFilters()
	return &tagMatcher{mappings, m.tables("polygon"), filters, true}
}

type Match struct {
	Key         string
	Value       string
	Table       DestTable
	tableFields *TableFields
}
type NodeMatcher interface {
	MatchNode(node *element.Node) []Match
}

type WayMatcher interface {
	MatchWay(way *element.Way) []Match
}

type RelationMatcher interface {
	MatchRelation(rel *element.Relation) []Match
}

type RelWayMatcher interface {
	WayMatcher
	RelationMatcher
}

type tagMatcher struct {
	mappings   TagTables
	tables     map[string]*TableFields
	filters    map[string][]ElementFilter
	matchAreas bool
}

func (m *Match) Row(elem *element.OSMElem, geom *geom.Geometry) []interface{} {
	return m.tableFields.MakeRow(elem, geom, *m)
}

func (tm *tagMatcher) MatchNode(node *element.Node) []Match {
	return tm.match(&node.Tags)
}

func (tm *tagMatcher) MatchWay(way *element.Way) []Match {
	if tm.matchAreas { // match way as polygon
		if way.IsClosed() {
			if way.Tags["area"] == "no" {
				return nil
			}
			return tm.match(&way.Tags)
		}
	} else { // match way as linestring
		if way.IsClosed() {
			if way.Tags["area"] == "yes" {
				return nil
			}
		}
		return tm.match(&way.Tags)
	}
	return nil
}

func (tm *tagMatcher) MatchRelation(rel *element.Relation) []Match {
	return tm.match(&rel.Tags)
}

func (tm *tagMatcher) match(tags *element.Tags) []Match {
	tables := make(map[DestTable]Match)

	for k, v := range *tags {
		values, ok := tm.mappings[Key(k)]
		if ok {
			if tbls, ok := values["__any__"]; ok {
				for _, t := range tbls {
					tables[t] = Match{k, v, t, tm.tables[t.Name]}
				}
			}
			if tbls, ok := values[Value(v)]; ok {
				for _, t := range tbls {
					tables[t] = Match{k, v, t, tm.tables[t.Name]}
				}
			}
		}
	}
	var matches []Match
	for t, match := range tables {
		filters, ok := tm.filters[t.Name]
		filteredOut := false
		if ok {
			for _, filter := range filters {
				if !filter(tags) {
					filteredOut = true
					break
				}
			}
		}
		if !filteredOut {
			matches = append(matches, match)
		}
	}
	return matches
}

// SelectRelationPolygons returns a slice of all members that are already
// imported as part of the relation.
// Outer members are "imported" if they share the same destination table. Inner members
// are "imported" when they also share the same key/value.
func SelectRelationPolygons(polygonTagMatcher RelWayMatcher, rel *element.Relation) []element.Member {
	relMatches := polygonTagMatcher.MatchRelation(rel)
	result := []element.Member{}
	for _, m := range rel.Members {
		if m.Type != element.WAY {
			continue
		}
		memberMatches := polygonTagMatcher.MatchWay(m.Way)
		if m.Role == "outer" && dstEquals(relMatches, memberMatches) {
			result = append(result, m)
		} else if matchEquals(relMatches, memberMatches) {
			result = append(result, m)
		}
	}
	return result
}

// matchEquals returns true if both matches share key/value and table
func matchEquals(matchesA, matchesB []Match) bool {
	for _, matchA := range matchesA {
		for _, matchB := range matchesB {
			if matchA.Key == matchB.Key &&
				matchA.Value == matchB.Value &&
				matchA.Table == matchB.Table {
				return true
			}
		}
	}
	return false
}

// dstEquals returns true if both matches share a single destination table
func dstEquals(matchesA, matchesB []Match) bool {
	for _, matchA := range matchesA {
		for _, matchB := range matchesB {
			if matchA.Table == matchB.Table {
				return true
			}
		}
	}
	return false
}
