// Garbage Collector.
package main

import (
	_ "github.com/go-sql-driver/mysql"
	"database/sql"
	"strings"
	"time"
)

// garbageCollector goes through the database every few hours or so, pings the objects and sees if they're alive, checks their timestamps, and if they are too old, remove them from the database.
func garbageCollector() {
	Log.Info("Garbage Collector called.")
	
	// use a ticker
	ticker := time.NewTicker(time.Hour * 4)
    //ticker := time.NewTicker(time.Minute * 1)
    go func() {
		var agent AgentType
		var position PositionType
		var toDeleteUUIDs []string // collect list of UUIDs to delete
		var callResult string

        for t := range ticker.C {
            Log.Debug("\n\n\n\n\nrunning at", t)
            
            // see which Agents are dead
            db, err := sql.Open(PDO_Prefix, GoBotDSN)
			checkErr(err)
			
			rows, err := db.Query("SELECT `UUID`, `PermURL` FROM `Agents`")
			checkErr(err)

			toDeleteUUIDs = nil // reset our array

			for rows.Next() {
				err = rows.Scan(
					&agent.UUID,
					&agent.PermURL,
				)
				checkErr(err) // cannot hurt
				
				// TODO(gwyneth): test 2 or 3 times, sometimes we exaust HTTP requests. Also, deal with a way for the LSL scripts
				//  attempt to register after an hour or so instead of entering BROKEN mode (20170806).
				
				// now call the ping on this Agent
				callResult, err = callURL(*agent.PermURL.Ptr(), "command=ping")
				Log.Debug("Agent", *agent.UUID.Ptr(), "Result of calling", *agent.PermURL.Ptr(), ":", callResult, "Error:", err)
				if err != nil || callResult != "pong" {
					// either dead, zombie, or misbehaving, kill this agent by placing it in the list
					toDeleteUUIDs = append(toDeleteUUIDs, "'" + *agent.UUID.Ptr() + "'")
				}
			}
			rows.Close()
			
			if len(toDeleteUUIDs) > 0 {
				killAgents := strings.Join(toDeleteUUIDs, ",")
				Log.Warning("The following agents did not reply to the ping command: ", killAgents)
				result, err := db.Exec("DELETE FROM `Agents` WHERE `UUID` IN (" + killAgents + ")")
				checkErr(err)
				rowsAffected, err := result.RowsAffected()
				if err == nil {
					Log.Info("deleted", rowsAffected, "zombie agents with UUIDs: ", killAgents) // probably not needed
				} else {
					Log.Warning("deleted an unknown number of zombie agents with UUIDs: ", killAgents)
				}
			} else {
				Log.Debug("no agents to delete")
			}			

			// see which Cubes (positions) are dead			
			rows, err = db.Query("SELECT `UUID`, `PermURL` FROM `Positions`")
			checkErr(err)

			toDeleteUUIDs = nil // reset our array again
			
			for rows.Next() {
				err = rows.Scan(
					&position.UUID,
					&position.PermURL,
				)
				checkErr(err)
				
				// now call the ping on this cube
				callResult, err = callURL(*position.PermURL.Ptr(), "command=ping")
				Log.Debug("Cube", *position.UUID.Ptr(), "Result of calling", *position.PermURL.Ptr(), ":", callResult, "Error:", err)
				if err != nil || callResult != "pong" {
					// either dead, zombie, or misbehaving, kill this cube by placing it in the list
					toDeleteUUIDs = append(toDeleteUUIDs, "'" + *position.UUID.Ptr() + "'")
				}
			}
			rows.Close()

			if len(toDeleteUUIDs) > 0 {
				killPositions := strings.Join(toDeleteUUIDs, ",")
				result, err := db.Exec("DELETE FROM `Positions` WHERE `UUID` IN (" + killPositions + ")")
				checkErr(err)
				rowsAffected, err := result.RowsAffected()
				if err == nil {
					Log.Info("deleted", rowsAffected, "zombie cubes with UUIDs: ", killPositions)
				} else {
					Log.Warning("deleted an unknown number of cubes with UUIDs: ", killPositions)
				}
			} else {
				Log.Debug("no cubes to delete")
			}			

			// see which Objects (positions) are dead
			// This is trickier, because objects are passive!
			// The best we can do is simply to delete old entries — 4 hours is an arbitrary number — and expect that agents will
			// start checking in again new entries
			// TODO(gwyneth): maybe cubes ought also run the Sensorama.lsl script to aid in detecting (and refreshing) 'permanent' objects?
			// Actually, that's a pretty good idea :-) (20170731)	
			result, err := db.Exec("DELETE FROM `Obstacles` WHERE `LastUpdate` < ADDDATE(NOW(), INTERVAL -4 HOUR)")
			checkErr(err)
			rowsAffected, err := result.RowsAffected()
			if err == nil {
				Log.Info("deleted", rowsAffected, "obstacles which haven't been seen in the past 4 hours")
			} else {
				Log.Warning("Couldn't delete any obstacles; reason:", err)
			}
						
			db.Close()
        }
    }()
}