#!/usr/bin/env coffee

dgram = require("dgram")
optimist = require("optimist")
winston = require("winston")
http = require("http")
uuid = require("node-uuid")
util = require("util")
_ = require("underscore")
fs = require("fs")
cluster = require("cluster")

argv = optimist
  .usage("""
    Usage: $0 -i [inputfile] -s [server] -p [port] -u [user] -w [password] -n [workers] -t [time] -h 

    The Profiler (Pr) can operate in two modes; master and slave.
    The master-mode is entered by providing an input file (the statistics structure outputted from a LogAnalyzer then ArgumentGenerator w glutenfree targeting).
    The slave-mode is entered into by default (when no parameters are given).
  """)
  .alias("i", "input")
  .describe("i","Input file")
  .describe("s","The hostname of the server (Aw) to connect to.")
  .alias("s", "server")
  .describe("p","The port of the server (Aw) to connect to.")
  .alias("p", "port")
  .describe("u","Username (used in basic-auth)")
  .alias("u", "user")
  .describe("w","Password (used in basic-auth)")
  .alias("w", "password")
  .alias("h","help")
  .describe("t","Time to wait between requests (in ms)")
  .alias("t", "time")
  .describe("h","Display usage.")
  .alias("n","workers")
  .describe("n","Number of workers per slave. Default is number of CPUs on slave machine. Set value to force higher concurrency.")
  .argv

if argv.h?
  optimist.showHelp()
  process.exit(0)

mode = if argv.i? then "master" else "slave"

slaves = {}

if mode is "master"

  winston.info "Pr in MASTER mode"

  if not (argv.s? and argv.p? and argv.u? and argv.w?)
    optimist.showHelp()
    winston.error "Missing server, port, user or password. Please provide all four."
    process.exit(0)

  file = if argv.i[0] is "/" then argv.i else "./#{argv.i}"
  input = require(file)

  # setup control interface
  port = Math.floor((Math.random()*1000)+3000);
  ios = require('socket.io').listen(port, {log: false})
  winston.info "control interface running on port #{port}"

  # io.set('log level', 0)

  ios.sockets.on(
    "connection",
    (socket) ->
      socket.on(
        "enslave",
        (slave) ->
          # add slave if it isnt alreadt registered
          if not slaves[socket.id]?
            winston.info "#{slave.id} enslaved"
            # remember slave
            slaves[socket.id] = _.extend(slave, { socket: socket })
            # tell slave to start profiling
            socket.emit(
              "profile", 
              { 
                targeting: input 
                server: argv.s
                port: argv.p
                user: argv.u
                password: argv.w
                workerCount: argv.n
                pause: argv.t
              }
            )
      )
      socket.on(
        "report"
        (hits) ->
          slave = slaves[socket.id]
          winston.debug "sample", hits[0]
          # build csv
          csv = _.reduce(
            hits
            (memo, hit) ->
              memo += _.map(
                _.values(hit)
                (value) -> 
                  if typeof(value) is "string" and value.indexOf(",") > 0 then "\"#{value}\""
                  else value
              )
              .join(",") + "\n"
            ""
          )

          # appending to file
          fs.appendFile(
            "#{slave.id}.csv"
            csv
            (err) ->
              if err? then winston.error "Could not write results to file.", err
              else winston.info "Results from #{slave.id} appended to file."
          )
      )
      socket.on(
        "disconnect",
        ->
          slave = slaves[socket.id]
          delete slaves[socket.id]
          winston.info "slave #{slave.id} freed"
      )
  )


  # advertise me in loop
  setInterval(
    () ->
      message = new Buffer("GLUTENFREE|Pr(MASTER)|ONLINE|#{port}")
      client = dgram.createSocket("udp4")
      client.send(
        message,
        0,
        message.length,
        5354,
        "224.0.0.251",
        (err, bytes) ->
          client.close()
      )
    5000
  )


else # mode is SLAVE

  ioc = require("socket.io-client")

  winston.info "Pr in SLAVE mode"

  me =
    id: uuid.v4()
    state: "AVAILABLE"

  #224.0.0.251:5353
  lsocket = dgram.createSocket("udp4")
  iosock = {}

  reports = []

  # reporting will contain the interval for reporting
  reporting = {}

  lsocket.on(
    "message",
    (msg, rinfo) ->

      if me.state is "AVAILABLE"

        [protocol,type,state,interfaceport] = "#{msg}".split("|")

        # reporting function
        # reports back to master over iosocket (websocket)
        report = (iosock, interval) ->
          if reports? and reports.length > 0 

            payload = _.flatten(_.pluck(reports, "hits"))

            #winston.debug "#{reports.length} reports sent to MASTER"

            iosock.emit("report", payload)

            # reset reports
            reports = []

          if me.state is "SLAVED" and iosock
            setTimeout(
              ->
                report(iosock, interval)
              interval
            )


        iosock = ioc.connect("http://#{rinfo.address}:#{interfaceport}")

        iosock.emit("enslave", me)
        iosock.on(
          "connect",
          ->
            me.state = "SLAVED"
            winston.info "enslaved"
        )

        iosock.on(
          "profile",
          (order) ->

            # extract targeting information
            targeting = order.targeting

            # setup cluster, ProfilerWorker.coffee contains worker-code
            cluster.setupMaster({
              exec: "#{__dirname}/ProfilerWorker"
            })

            # determine number of workers and fork
            workerCount =  order.workerCount || require("os").cpus().length
            for w in [1..workerCount]
              do (w) ->
                cluster.fork()

            cluster.on("exit", (worker) ->
              if me.state is "SLAVED" 
                winston.warn "worker (#{worker?.process?.pid}) died - forking new"
                cluster.fork()
            )

            # send target lookup to worker
            cluster.on("online", (worker) ->
                # listen for reports from worker
                worker.on("message", (msg) -> 
                  switch msg.subject
                    when "report"
                      rprt = msg.report
                      #winston.info "recv report from #{worker.id} containing #{rprt.hits.length} hits"
                      reports.push(rprt)
                )
                
                # wait a tick before ordering new worker around
                setTimeout(
                  ->
                    winston.info "sending targeting to", worker.id
                    worker.send({ subject: "targeting", clusterId: me.id, server: order.server, port: order.port, user: order.user, password: order.password, targeting: targeting, pause: order.pause })
                  1000
                )
            )

            # setup timed reporting (every 10th second)
            report(iosock, 10000)

        )

        # go back to AVAILABLE when disconnected
        iosock.on(
          "disconnect",
          ->
            me.state = "AVAILABLE"
            winston.info "freed, now asking workers to die"

            cluster.disconnect()
        )

  )

  lsocket.on(
    "listening",
    () ->
      address = lsocket.address()
      multicastadr = "224.0.0.251"
      winston.info "Pr SLAVE listening for MASTERs at #{address.address}:#{address.port} (multicast at #{multicastadr})"
      lsocket.addMembership(multicastadr)
  )

  # discovery lsocket is bound
  lsocket.bind(5354)