func periodSelector(d *DateParam) bson.M {
	switch d.Prefix {
	case EQ:
		return bson.M{
			"start.time": bson.M{
				"$gte": d.Date.RangeLowIncl(),
			},
			"end.time": bson.M{
				"$lt": d.Date.RangeHighExcl(),
			},
		}
	case GT:
		return bson.M{
			"$or": []bson.M{
				bson.M{
					"end.time": bson.M{
						"$gt": d.Date.RangeLowIncl(),
					},
				},
				bson.M{
					"$ne": nil,
					"end": nil,
				},
			},
		}
	case LT:
		return bson.M{
			"$or": []bson.M{
				bson.M{
					"start.time": bson.M{
						"$lt": d.Date.RangeLowIncl(),
					},
				},
				bson.M{
					"$ne":   nil,
					"start": nil,
				},
			},
		}
	case GE:
		return bson.M{
			"$or": []bson.M{
				bson.M{
					"start.time": bson.M{
						"$gte": d.Date.RangeLowIncl(),
					},
				},
				bson.M{
					"end.time": bson.M{
						"$gt": d.Date.RangeLowIncl(),
					},
				},
				bson.M{
					"$ne": nil,
					"end": nil,
				},
			},
		}
	case LE:
		return bson.M{
			"$or": []bson.M{
				bson.M{
					"end.time": bson.M{
						"$lt": d.Date.RangeHighExcl(),
					},
				},
				bson.M{
					"start.time": bson.M{
						"$lt": d.Date.RangeLowIncl(),
					},
				},
				bson.M{
					"$ne":   nil,
					"start": nil,
				},
			},
		}
	case SA:
		return bson.M{
			"start.time": bson.M{
				"$gte": d.Date.RangeHighExcl(),
			},
		}
	case EB:
		return bson.M{
			"end.time": bson.M{
				"$lt": d.Date.RangeLowIncl(),
			},
		}
	}
	panic(createUnsupportedSearchError("MSG_PARAM_INVALID", fmt.Sprintf("Parameter \"%s\" content is invalid", d.Name)))
}
Note that this solution is not 100% correct because we don't represent dates as ranges in the
database -- but the FHIR spec calls for this sort of behavior to correctly implement these
searches.  An easy example is that while 2012-01-01 should be compared as the range from
00:00:00.000 to 23:59:59.999, we currently only compare against 00:00:00.000 -- so some things
that should match, might not.
TODO: Fix this via more complex search criteria (not likely feasible) or by a different representation in the
database (e.g., storing upper and lower bounds of dates in the DB).
package search
import (
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"github.com/intervention-engine/fhir/models"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)
type MongoSearcher struct {
	db *mgo.Database
}
func NewMongoSearcher(db *mgo.Database) *MongoSearcher {
	return &MongoSearcher{db}
}
func (m *MongoSearcher) GetDB() *mgo.Database {
	return m.db
}
func (m *MongoSearcher) CreateQuery(query Query) *mgo.Query {
	return m.createQuery(query, true)
}
func (m *MongoSearcher) CreateQueryWithoutOptions(query Query) *mgo.Query {
	return m.createQuery(query, false)
}
func (m *MongoSearcher) CreateQueryObject(query Query) bson.M {
	return m.createQueryObject(query)
}
func (m *MongoSearcher) createQuery(query Query, withOptions bool) *mgo.Query {
	c := m.db.C(models.PluralizeLowerResourceName(query.Resource))
	q := m.createQueryObject(query)
	mgoQuery := c.Find(q)
	if withOptions {
		o := query.Options()
		removeParallelArraySorts(o)
		if len(o.Sort) > 0 {
			fields := make([]string, len(o.Sort))
			for i := range o.Sort {
				field := convertSearchPathToMongoField(o.Sort[i].Parameter.Paths[0].Path)
				if o.Sort[i].Descending {
					field = "-" + field
				}
				fields[i] = field
			}
			mgoQuery = mgoQuery.Sort(fields...)
		}
		if o.Offset > 0 {
			mgoQuery = mgoQuery.Skip(o.Offset)
		}
		mgoQuery = mgoQuery.Limit(o.Count)
	}
	return mgoQuery
}
func (m *MongoSearcher) CreatePipeline(query Query) *mgo.Pipe {
	c := m.db.C(models.PluralizeLowerResourceName(query.Resource))
	p := []bson.M{{"$match": m.createQueryObject(query)}}
	o := query.Options()
	removeParallelArraySorts(o)
	if len(o.Sort) > 0 {
		var sortBSOND bson.D
		for _, sort := range o.Sort {
			field := convertSearchPathToMongoField(sort.Parameter.Paths[0].Path)
			order := 1
			if sort.Descending {
				order = -1
			}
			sortBSOND = append(sortBSOND, bson.DocElem{Name: field, Value: order})
		}
		p = append(p, bson.M{"$sort": sortBSOND})
	}
	if o.Offset > 0 {
		p = append(p, bson.M{"$skip": o.Offset})
	}
	p = append(p, bson.M{"$limit": o.Count})
	if len(o.Include) > 0 {
		for _, incl := range o.Include {
			for _, inclPath := range incl.Parameter.Paths {
				if inclPath.Type != "Reference" {
					continue
				}
				localField := strings.Replace(inclPath.Path, "[]", "", -1) + ".referenceid"
				for i, inclTarget := range incl.Parameter.Targets {
					if inclTarget == "Any" {
						continue
					}
					from := models.PluralizeLowerResourceName(inclTarget)
					as := fmt.Sprintf("_included%sResourcesReferencedBy%s", inclTarget, strings.Title(incl.Parameter.Name))
					if len(incl.Parameter.Paths) > 1 {
						as += fmt.Sprintf("Path%d", i+1)
					}
					p = append(p, bson.M{"$lookup": bson.M{
						"from":         from,
						"localField":   localField,
						"foreignField": "_id",
						"as":           as,
					}})
				}
			}
		}
	}
	if len(o.RevInclude) > 0 {
		for _, incl := range o.RevInclude {
			targetsSearchResource := false
			for _, inclTarget := range incl.Parameter.Targets {
				if inclTarget == query.Resource || inclTarget == "Any" {
					targetsSearchResource = true
					break
				}
			}
			if !targetsSearchResource {
				continue
			}
			from := models.PluralizeLowerResourceName(incl.Parameter.Resource)
			for i, inclPath := range incl.Parameter.Paths {
				if inclPath.Type != "Reference" {
					continue
				}
				foreignField := strings.Replace(inclPath.Path, "[]", "", -1) + ".referenceid"
				as := fmt.Sprintf("_revIncluded%sResourcesReferencing%s", incl.Parameter.Resource, strings.Title(incl.Parameter.Name))
				if len(incl.Parameter.Paths) > 1 {
					as += fmt.Sprintf("Path%d", i+1)
				}
				p = append(p, bson.M{"$lookup": bson.M{
					"from":         from,
					"localField":   "_id",
					"foreignField": foreignField,
					"as":           as,
				}})
			}
		}
	}
	return c.Pipe(p)
}
func (m *MongoSearcher) createQueryObject(query Query) bson.M {
	result := bson.M{}
	for _, p := range m.createParamObjects(query.Params()) {
		merge(result, p)
	}
	return result
}
func (m *MongoSearcher) createParamObjects(params []SearchParam) []bson.M {
	results := make([]bson.M, len(params))
	for i, p := range params {
		panicOnUnsupportedFeatures(p)
		switch p := p.(type) {
		case *CompositeParam:
			results[i] = m.createCompositeQueryObject(p)
		case *DateParam:
			results[i] = m.createDateQueryObject(p)
		case *NumberParam:
			results[i] = m.createNumberQueryObject(p)
		case *QuantityParam:
			results[i] = m.createQuantityQueryObject(p)
		case *ReferenceParam:
			results[i] = m.createReferenceQueryObject(p)
		case *StringParam:
			results[i] = m.createStringQueryObject(p)
		case *TokenParam:
			results[i] = m.createTokenQueryObject(p)
		case *URIParam:
			results[i] = m.createURIQueryObject(p)
		case *OrParam:
			results[i] = m.createOrQueryObject(p)
		default:
			builder, err := GlobalMongoRegistry().LookupBSONBuilder(p.getInfo().Type)
			if err != nil {
				panic(createInternalServerError("MSG_PARAM_UNKNOWN", fmt.Sprintf("Parameter \"%s\" not understood", p.getInfo().Name)))
			}
			result, err := builder(p, m)
			if err != nil {
				panic(createInternalServerError("MSG_PARAM_INVALID", fmt.Sprintf("Parameter \"%s\" content is invalid", p.getInfo().Name)))
			}
			results[i] = result
		}
	}
	return results
}
func panicOnUnsupportedFeatures(p SearchParam) {
	_, isDate := p.(*DateParam)
	prefix := p.getInfo().Prefix
	if prefix != "" && prefix != EQ && !isDate {
		panic(createUnsupportedSearchError("MSG_PARAM_INVALID", fmt.Sprintf("Parameter \"%s\" content is invalid", p.getInfo().Name)))
	}
	_, isRef := p.(*ReferenceParam)
	modifier := p.getInfo().Modifier
	if modifier != "" {
		if _, ok := SearchParameterDictionary[modifier]; !isRef || !ok {
			panic(createUnsupportedSearchError("MSG_PARAM_MODIFIER_INVALID", fmt.Sprintf("Parameter \"%s\" modifier is invalid", p.getInfo().Name)))
		}
	}
}
func (m *MongoSearcher) createCompositeQueryObject(c *CompositeParam) bson.M {
	panic(createUnsupportedSearchError("MSG_PARAM_UNKNOWN", fmt.Sprintf("Parameter \"%s\" not understood", c.Name)))
}
func (m *MongoSearcher) createDateQueryObject(d *DateParam) bson.M {
	single := func(p SearchParamPath) bson.M {
		switch p.Type {
		case "date", "dateTime", "instant":
			return buildBSON(p.Path, dateSelector(d))
		case "Period":
			return buildBSON(p.Path, periodSelector(d))
		case "Timing":
			return buildBSON(p.Path+".event", dateSelector(d))
		default:
			return bson.M{}
		}
	}
	return orPaths(single, d.Paths)
}
func dateSelector(d *DateParam) bson.M {
	var timeCriteria bson.M
	switch d.Prefix {
	case EQ:
		timeCriteria = bson.M{
			"$gte": d.Date.RangeLowIncl(),
			"$lt":  d.Date.RangeHighExcl(),
		}
	case GT, SA:
		timeCriteria = bson.M{
			"$gt": d.Date.RangeLowIncl(),
		}
	case LT, EB:
		timeCriteria = bson.M{
			"$lt": d.Date.RangeLowIncl(),
		}
	case GE:
		timeCriteria = bson.M{
			"$gte": d.Date.RangeLowIncl(),
		}
	case LE:
		timeCriteria = bson.M{
			"$lt": d.Date.RangeHighExcl(),
		}
	default:
		panic(createUnsupportedSearchError("MSG_PARAM_INVALID", fmt.Sprintf("Parameter \"%s\" content is invalid", d.Name)))
	}
	return bson.M{"time": timeCriteria}
}