package mr

import (
	"errors"
	"log"
	"net"
	"net/http"
	"net/rpc"
	"os"
	"time"
)

type Coordinator struct {
	MapJobs    []Job
	ReduceJobs []Job
	NReduce    int
}

type Job struct {
	Filename string
	Status   bool
	Active   bool
	LastPing time.Time
}

// Your code here -- RPC handlers for the worker to call.
//
// the RPC argument and reply types are defined in rpc.go.
func (c *Coordinator) GetTasks(args *RpcArgument, reply *RpcReply) error {
	if args.Method != "request_task" {
		return errors.New("argument .Method not valid for this procedure")
	}
	// check for any non-completed OR non-active map jobs
	for _, mj := range c.MapJobs {
		// assign job that not already done OR not actively being run by other workers
		if !mj.Status && !mj.Active {
			reply.NReduce = c.NReduce
			reply.TaskType = "map"
			reply.Filename = mj.Filename
			// set job status, taken by a worker
			mj.Active = true
			mj.LastPing = time.Now()
			return nil
		}
	}
	// check for reduce jobs
	for _, rj := range c.ReduceJobs {
		if !rj.Status && !rj.Active {
			reply.NReduce = c.NReduce
			reply.TaskType = "reduce"
			reply.Filename = rj.Filename
			// set job status, taken by a worker
			rj.Active = true
			rj.LastPing = time.Now()
			return nil
		}
	}
	return nil
}

// Set task complete
func (c *Coordinator) SetTaskDone(args *RpcArgument, reply *RpcReply) error {
	if args.Method != "set_task_done" {
		return errors.New("argument .Method not valid for this procedure")
	}
	if args.TaskType == "map" {
		f := args.Filename
		for _, mj := range c.MapJobs {
			if mj.Filename == f {
				mj.Status = true
			}
		}
		// Create reduce job data struct for each intermediary file returned from worker's map job
		for _, ifn := range args.IntermediateFiles {
			reduceJob := Job{}
			reduceJob.Active = false
			reduceJob.Filename = ifn
			reduceJob.LastPing = time.Now()
			reduceJob.Status = false
			c.ReduceJobs = append(c.ReduceJobs, reduceJob)
		}
	} else if args.TaskType == "reduce" {
		f := args.Filename
		for _, rj := range c.ReduceJobs {
			if rj.Filename == f {
				rj.Status = true
			}
		}
	} else {
		return errors.New("argument .TaskType not valid for this procedure")
	}
	return nil
}

// start a thread that listens for RPCs from worker.go
func (c *Coordinator) server() {
	rpc.Register(c)
	rpc.HandleHTTP()
	//l, e := net.Listen("tcp", ":1234")
	sockname := coordinatorSock()
	os.Remove(sockname)
	l, e := net.Listen("unix", sockname)
	if e != nil {
		log.Fatal("listen error:", e)
	}
	go http.Serve(l, nil)
}

// main/mrcoordinator.go calls Done() periodically to find out
// if the entire job has finished.
func (c *Coordinator) Done() bool {
	ret := false
	// Check any non-completed jobs
	for _, mj := range c.MapJobs {
		if !mj.Status {
			return ret
		}
	}
	// all mj completed
	ret = true
	return ret
}

// create a Coordinator.
// main/mrcoordinator.go calls this function.
// nReduce is the number of reduce tasks to use.
func MakeCoordinator(files []string, nReduce int) *Coordinator {
	c := Coordinator{}

	c.NReduce = nReduce

	for i := range files {
		mj := Job{}
		mj.Filename = files[i]
		mj.Status = false
		mj.Active = false
		c.MapJobs = append(c.MapJobs, mj)
	}

	c.server()
	return &c
}
