// Copyright 2014 The Cayley Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mongo

import (
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"

	"github.com/google/cayley/graph"
	"github.com/google/cayley/graph/iterator"
	"github.com/google/cayley/quad"
)

var _ graph.Nexter = &LinksTo{}

var linksToType graph.Type

func init() {
	linksToType = graph.RegisterIterator("mongo-linksto")
}

// LinksTo is a MongoDB-dependent version of a LinksTo iterator. Like the normal
// LinksTo, it represents a set of links to a set of nodes, represented by its
// subiterator. However, this iterator may often be faster than the generic
// LinksTo, as it can use the secondary indices in Mongo as features within the
// Mongo query, reducing the size of the result set and speeding up iteration.
type LinksTo struct {
	uid        uint64
	collection string
	tags       graph.Tagger
	qs         *QuadStore
	primaryIt  graph.Iterator
	dir        quad.Direction
	nextIt     *mgo.Iter
	result     graph.Value
	runstats   graph.IteratorStats
	lset       []graph.Linkage
	err        error
}

// NewLinksTo constructs a new indexed LinksTo iterator for Mongo around a direction
// and a subiterator of nodes.
func NewLinksTo(qs *QuadStore, it graph.Iterator, collection string, d quad.Direction, lset []graph.Linkage) *LinksTo {
	return &LinksTo{
		uid:        iterator.NextUID(),
		qs:         qs,
		primaryIt:  it,
		dir:        d,
		nextIt:     nil,
		lset:       lset,
		collection: collection,
	}
}

func (it *LinksTo) buildConstraint() bson.M {
	constraint := bson.M{}
	// TODO(barakmich): Currently this only works for an individual linkage in any direction.
	for _, link := range it.lset {
		constraint[link.Dir.String()] = link.Value
	}
	return constraint
}

func (it *LinksTo) buildIteratorFor(d quad.Direction, val graph.Value) *mgo.Iter {
	name := it.qs.NameOf(val)
	constraint := it.buildConstraint()
	constraint[d.String()] = name
	return it.qs.db.C(it.collection).Find(constraint).Iter()
}

func (it *LinksTo) UID() uint64 {
	return it.uid
}

func (it *LinksTo) Tagger() *graph.Tagger {
	return &it.tags
}

// Return the direction under consideration.
func (it *LinksTo) Direction() quad.Direction { return it.dir }

// Tag these results, and our subiterator's results.
func (it *LinksTo) TagResults(dst map[string]graph.Value) {
	for _, tag := range it.tags.Tags() {
		dst[tag] = it.Result()
	}

	for tag, value := range it.tags.Fixed() {
		dst[tag] = value
	}

	it.primaryIt.TagResults(dst)
}

// Optimize the LinksTo, by replacing it if it can be.
func (it *LinksTo) Optimize() (graph.Iterator, bool) {
	return it, false
}

func (it *LinksTo) Next() bool {
	var result struct {
		ID      string  `bson:"_id"`
		Added   []int64 `bson:"Added"`
		Deleted []int64 `bson:"Deleted"`
	}
	graph.NextLogIn(it)
	it.runstats.Next += 1
	if it.nextIt != nil && it.nextIt.Next(&result) {
		it.runstats.ContainsNext += 1
		if it.collection == "quads" && len(result.Added) <= len(result.Deleted) {
			return it.Next()
		}
		it.result = result.ID
		return graph.NextLogOut(it, it.result, true)
	}

	if it.nextIt != nil {
		// If there's an error in the 'next' iterator, we save it and we're done.
		it.err = it.nextIt.Err()
		if it.err != nil {
			return false
		}

	}
	// Subiterator is empty, get another one
	if !graph.Next(it.primaryIt) {
		// Possibly save error
		it.err = it.primaryIt.Err()

		// We're out of nodes in our subiterator, so we're done as well.
		return graph.NextLogOut(it, 0, false)
	}
	if it.nextIt != nil {
		it.nextIt.Close()
	}
	it.nextIt = it.buildIteratorFor(it.dir, it.primaryIt.Result())

	// Recurse -- return the first in the next set.
	return it.Next()
}

func (it *LinksTo) Err() error {
	return it.err
}

func (it *LinksTo) Result() graph.Value {
	return it.result
}

func (it *LinksTo) Close() error {
	var err error
	if it.nextIt != nil {
		err = it.nextIt.Close()
	}

	_err := it.primaryIt.Close()
	if _err != nil && err == nil {
		err = _err
	}

	return err
}

func (it *LinksTo) NextPath() bool {
	ok := it.primaryIt.NextPath()
	if !ok {
		it.err = it.primaryIt.Err()
	}
	return ok
}

func (it *LinksTo) Type() graph.Type {
	return linksToType
}

func (it *LinksTo) Clone() graph.Iterator {
	m := NewLinksTo(it.qs, it.primaryIt.Clone(), it.collection, it.dir, it.lset)
	m.tags.CopyFrom(it)
	return m
}

func (it *LinksTo) Contains(val graph.Value) bool {
	graph.ContainsLogIn(it, val)
	it.runstats.Contains += 1

	for _, link := range it.lset {
		dval := it.qs.QuadDirection(val, link.Dir)
		if dval != link.Value {
			return graph.ContainsLogOut(it, val, false)
		}
	}

	node := it.qs.QuadDirection(val, it.dir)
	if it.primaryIt.Contains(node) {
		it.result = val
		return graph.ContainsLogOut(it, val, true)
	}
	it.err = it.primaryIt.Err()
	return graph.ContainsLogOut(it, val, false)
}

func (it *LinksTo) Describe() graph.Description {
	primary := it.primaryIt.Describe()
	return graph.Description{
		UID:       it.UID(),
		Type:      it.Type(),
		Direction: it.dir,
		Iterator:  &primary,
	}
}

func (it *LinksTo) Reset() {
	it.primaryIt.Reset()
	if it.nextIt != nil {
		it.nextIt.Close()
	}
	it.nextIt = nil
}

// Return a guess as to how big or costly it is to next the iterator.
func (it *LinksTo) Stats() graph.IteratorStats {
	subitStats := it.primaryIt.Stats()
	// TODO(barakmich): These should really come from the quadstore itself
	fanoutFactor := int64(20)
	checkConstant := int64(1)
	nextConstant := int64(2)

	size := fanoutFactor * subitStats.Size
	csize, _ := it.qs.getSize(it.collection, it.buildConstraint())
	if size > csize {
		size = csize
	}

	return graph.IteratorStats{
		NextCost:     nextConstant + subitStats.NextCost,
		ContainsCost: checkConstant + subitStats.ContainsCost,
		Size:         size,
		Next:         it.runstats.Next,
		Contains:     it.runstats.Contains,
		ContainsNext: it.runstats.ContainsNext,
	}
}

func (it *LinksTo) Size() (int64, bool) {
	return it.Stats().Size, false
}

// Return a list containing only our subiterator.
func (it *LinksTo) SubIterators() []graph.Iterator {
	return []graph.Iterator{it.primaryIt}
}
