package main

import (
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
	"time"
)

func GetTags(numTags int, tagsColl *mgo.Collection) ([]string, error) {
	var result []struct {
		Tag string "_id"
	}
	err := tagsColl.Find(nil).Sort("-value").Limit(numTags).All(&result)
	if err != nil {
		return nil, err
	}

	tags := make([]string, len(result))
	for i, r := range result {
		tags[i] = r.Tag
	}
	return tags, nil
}

func GetBooksVisited(num int, visitedColl *mgo.Collection) ([]bson.ObjectId, error) {
	var result []struct {
		Book bson.ObjectId "_id"
	}
	err := visitedColl.Find(nil).Sort("-value").Limit(num).All(&result)
	if err != nil {
		return nil, err
	}

	books := make([]bson.ObjectId, len(result))
	for i, r := range result {
		books[i] = r.Book
	}
	return books, nil
}

func GetVisits(visitsColl *mgo.Collection) ([]Visits, error) {
	var result []Visits
	err := visitsColl.Find(nil).All(&result)
	return result, err
}

type MR struct {
	database *mgo.Database
}

func NewMR(database *mgo.Database) *MR {
	m := new(MR)
	m.database = database
	return m
}

func (m *MR) UpdateTags(booksColl *mgo.Collection) error {
	var mr mgo.MapReduce
	mr.Map = `function() {
	              if (this.subject) {
	                  this.subject.forEach(function(s) { emit(s, 1); });
	              }
	          }`
	mr.Reduce = `function(tag, vals) {
	                 var count = 0;
	                 vals.forEach(function() { count += 1; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"active": true}, booksColl, TAGS_COLL)
}

func (m *MR) UpdateMostVisited(statsColl *mgo.Collection) error {
	var mr mgo.MapReduce
	mr.Map = `function() {
	              if (this.id) {
	                  emit(this.id, 1);
	              }
	          }`
	mr.Reduce = `function(tag, vals) {
	                 var count = 0;
	                 vals.forEach(function() { count += 1; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"section": "book"}, statsColl, VISITED_COLL)
}

func (m *MR) UpdateMostDownloaded(statsColl *mgo.Collection) error {
	var mr mgo.MapReduce
	mr.Map = `function() {
	              emit(this.id, 1);
	          }`
	mr.Reduce = `function(tag, vals) {
	                 var count = 0;
	                 vals.forEach(function() { count += 1; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"section": "download"}, statsColl, DOWNLOADED_COLL)
}

func (m *MR) UpdateHourVisits(statsColl *mgo.Collection) error {
	const numDays = 2
	start := time.Now().UTC().Add(-numDays * 24 * time.Hour)

	const reduce = `function(date, vals) {
	                    var count = 0;
	                    vals.forEach(function(v) { count += v; });
	                    return count;
	                }`
	var mr mgo.MapReduce
	mr.Map = `function() {
	             var date = Date.UTC(this.date.getUTCFullYear(),
	                                 this.date.getUTCMonth(),
	                                 this.date.getUTCDate(),
	                                 this.date.getUTCHours());
	             emit({date: date, session: this.session}, 1);
	          }`
	mr.Reduce = reduce
	err := m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, HOURLY_VISITS_COLL+"_raw")
	if err != nil {
		return err
	}
	var mr2 mgo.MapReduce
	mr2.Map = `function() {
	               emit(this['_id']['date'], 1);
	           }`
	mr2.Reduce = reduce
	hourly_raw := m.database.C(HOURLY_VISITS_COLL + "_raw")
	return m.update(&mr2, bson.M{}, hourly_raw, HOURLY_VISITS_COLL)
}

func (m *MR) UpdateDayVisits(statsColl *mgo.Collection) error {
	const numDays = 30
	start := time.Now().UTC().Add(-numDays * 24 * time.Hour).Truncate(24 * time.Hour)

	const reduce = `function(date, vals) {
	                    var count = 0;
	                    vals.forEach(function(v) { count += v; });
	                    return count;
	                }`
	var mr mgo.MapReduce
	mr.Map = `function() {
	              var date = Date.UTC(this.date.getUTCFullYear(),
	                                  this.date.getUTCMonth(),
	                                  this.date.getUTCDate());
	              emit({date: date, session: this.session}, 1);
	          }`
	mr.Reduce = reduce
	err := m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, DAILY_VISITS_COLL+"_raw")
	if err != nil {
		return err
	}
	var mr2 mgo.MapReduce
	mr2.Map = `function() {
	              emit(this['_id']['date'], 1);
	           }`
	mr2.Reduce = reduce
	daily_raw := m.database.C(DAILY_VISITS_COLL + "_raw")
	return m.update(&mr2, bson.M{}, daily_raw, DAILY_VISITS_COLL)
}

func (m *MR) UpdateMonthVisits(statsColl *mgo.Collection) error {
	const numDays = 365

	start := time.Now().UTC().Add(-numDays * 24 * time.Hour).Truncate(24 * time.Hour)

	const reduce = `function(date, vals) {
	                    var count = 0;
	                    vals.forEach(function(v) { count += v; });
	                    return count;
	                }`
	var mr mgo.MapReduce
	mr.Map = `function() {
	              var date = Date.UTC(this.date.getUTCFullYear(),
	                                  this.date.getUTCMonth());
	              emit({date: date, session: this.session}, 1);
	          }`
	mr.Reduce = reduce
	err := m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, MONTHLY_VISITS_COLL+"_raw")
	if err != nil {
		return err
	}
	var mr2 mgo.MapReduce
	mr2.Map = `function() {
	               emit(this['_id']['date'], 1);
	           }`
	mr2.Reduce = reduce
	monthly_raw := m.database.C(MONTHLY_VISITS_COLL + "_raw")
	return m.update(&mr2, bson.M{}, monthly_raw, MONTHLY_VISITS_COLL)
}

func (m *MR) UpdateHourDownloads(statsColl *mgo.Collection) error {
	const numDays = 2
	start := time.Now().UTC().Add(-numDays * 24 * time.Hour)

	var mr mgo.MapReduce
	mr.Map = `function() {
	              if (this.section == "download") {
	                  var date = Date.UTC(this.date.getUTCFullYear(),
	                                      this.date.getUTCMonth(),
	                                      this.date.getUTCDate(),
	                                      this.date.getUTCHours());
	                  emit(date, 1);
	              }
                  }`
	mr.Reduce = `function(date, vals) {
	                 var count = 0;
	                 vals.forEach(function(v) { count += v; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, HOURLY_DOWNLOADS_COLL)
}

func (m *MR) UpdateDayDownloads(statsColl *mgo.Collection) error {
	const numDays = 30
	start := time.Now().UTC().Add(-numDays * 24 * time.Hour).Truncate(24 * time.Hour)

	var mr mgo.MapReduce
	mr.Map = `function() {
	              if (this.section == "download") {
	                  var date = Date.UTC(this.date.getUTCFullYear(),
	                                      this.date.getUTCMonth(),
	                                      this.date.getUTCDate());
	                  emit(date, 1);
	              }
	          }`
	mr.Reduce = `function(date, vals) {
	                 var count = 0;
	                 vals.forEach(function(v) { count += v; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, DAILY_DOWNLOADS_COLL)
}

func (m *MR) UpdateMonthDownloads(statsColl *mgo.Collection) error {
	const numDays = 365

	start := time.Now().UTC().Add(-numDays * 24 * time.Hour).Truncate(24 * time.Hour)

	var mr mgo.MapReduce
	mr.Map = `function() {
	              if (this.section == "download") {
	                  var date = Date.UTC(this.date.getUTCFullYear(),
	                                      this.date.getUTCMonth());
	                  emit(date, 1);
	              }
	          }`
	mr.Reduce = `function(date, vals) {
	                 var count = 0;
	                 vals.forEach(function(v) { count += v; });
	                 return count;
	             }`
	return m.update(&mr, bson.M{"date": bson.M{"$gte": start}}, statsColl, MONTHLY_DOWNLOADS_COLL)
}

func (m *MR) update(mr *mgo.MapReduce, query bson.M, queryColl *mgo.Collection, storeColl string) error {
	metaColl := m.database.C(META_COLL)
	_, err := metaColl.RemoveAll(bson.M{"type": storeColl})
	if err != nil {
		return err
	}

	mr.Out = bson.M{"replace": storeColl}
	_, err = queryColl.Find(query).MapReduce(mr, nil)
	if err != nil {
		return err
	}

	return metaColl.Insert(bson.M{"type": storeColl})
}

func (m *MR) isOutdated(coll string, minutes float64) bool {
	var result struct {
		Id bson.ObjectId `bson:"_id"`
	}
	metaColl := m.database.C(META_COLL)
	err := metaColl.Find(bson.M{"type": coll}).One(&result)
	if err != nil {
		return true
	}

	lastUpdate := result.Id.Time()
	return time.Since(lastUpdate).Minutes() > minutes
}