package domain

import (
	"context"
	"sync"

	geo "github.com/hailocab/go-geoindex"
	"github.com/micro/micro/v3/service/errors"
	common "github.com/micro/services/location/proto"
	"github.com/micro/services/pkg/tenant"
)

var (
	mtx          sync.RWMutex
	defaultIndex = geo.NewPointsIndex(geo.Meters(100))

	// index per tenant
	indexes = map[string]*geo.PointsIndex{}
)

type Entity struct {
	ID        string
	Type      string
	Latitude  float64
	Longitude float64
	Timestamp int64
}

func (e *Entity) Id() string {
	return e.ID
}

func (e *Entity) Lat() float64 {
	return e.Latitude
}

func (e *Entity) Lon() float64 {
	return e.Longitude
}

func (e *Entity) ToProto() *common.Entity {
	return &common.Entity{
		Id:   e.ID,
		Type: e.Type,
		Location: &common.Point{
			Latitude:  e.Latitude,
			Longitude: e.Longitude,
			Timestamp: e.Timestamp,
		},
	}
}

func getIndex(ctx context.Context) *geo.PointsIndex {
	tenantId, ok := tenant.FromContext(ctx)
	if !ok {
		//return default index
		return defaultIndex
	}

	// get the index
	index, ok := indexes[tenantId]
	if !ok {
		index = geo.NewPointsIndex(geo.Meters(100))
		indexes[tenantId] = index
	}

	return index
}

func ProtoToEntity(e *common.Entity) *Entity {
	return &Entity{
		ID:        e.Id,
		Type:      e.Type,
		Latitude:  e.Location.Latitude,
		Longitude: e.Location.Longitude,
		Timestamp: e.Location.Timestamp,
	}
}

func Read(ctx context.Context, id string) (*Entity, error) {
	mtx.RLock()
	defer mtx.RUnlock()

	// get the index
	index := getIndex(ctx)

	p := index.Get(id)
	if p == nil {
		return nil, errors.NotFound("location.read", "Not found")
	}

	entity, ok := p.(*Entity)
	if !ok {
		return nil, errors.InternalServerError("location.read", "Error reading entity")
	}

	return entity, nil
}

func Save(ctx context.Context, e *Entity) {
	mtx.Lock()
	// get the index
	index := getIndex(ctx)
	index.Add(e)
	mtx.Unlock()
}

func Search(ctx context.Context, typ string, entity *Entity, radius float64, numEntities int) []*Entity {
	mtx.RLock()
	defer mtx.RUnlock()

	// get the index
	index := getIndex(ctx)

	points := index.KNearest(entity, numEntities, geo.Meters(radius), func(p geo.Point) bool {
		e, ok := p.(*Entity)
		if !ok || e.Type != typ {
			return false
		}
		return true
	})

	var entities []*Entity

	for _, point := range points {
		e, ok := point.(*Entity)
		if !ok {
			continue
		}
		entities = append(entities, e)
	}

	return entities
}
