
common = require './common'
{ Library } = require './library'

transport = require('msgflo-nodejs').transport
fbp = require 'fbp'
async = require 'async'
debug = require('debug')('msgflo:setup')
child_process = require 'child_process'
path = require 'path'
program = require 'commander'

addBindings = (broker, bindings, callback) ->
  addBinding = (b, cb) ->
    broker.addBinding b, cb
  async.map bindings, addBinding, callback

queueName = (c) ->
  return common.queueName c.process, c.port

startProcess = (cmd, options, callback) ->
  env = process.env
  env['MSGFLO_BROKER'] = options.broker
  if options.shell
    prog = options.shell
    args = ['-c', cmd]
  else
    prog = cmd.split(' ')[0]
    args = cmd.split(' ').splice(1)
  execCmd = prog + ' ' + args.join(' ')

  childoptions =
    env: env
  debug 'participant process start', execCmd
  child = child_process.spawn prog, args, childoptions
  returned = false
  child.on 'error', (err) ->
    debug 'participant error', prog, args.join(' ')
    return if returned
    returned = true
    return callback err, child
  # We assume that when somethis is send on stdout, starting is complete
  child.stdout.on 'data', (data) ->
    process.stdout.write(data.toString()) if options.forward.indexOf('stdout') != -1
    debug 'participant stdout', data.toString()
    return if returned
    returned = true
    return callback null, child
  child.stderr.on 'data', (data) ->
    process.stderr.write(data.toString()) if options.forward.indexOf('stderr') != -1
    debug 'participant stderr', data.toString()
    #return if returned
    #returned = true
    #return callback new Error data.toString(), child
  child.on 'exit', (code, signal) ->
    debug 'child exited', code, signal
    return if returned
    returned = true
    return callback new Error "Child '#{execCmd}' (pid=#{child.pid}) exited with #{code} #{signal}"
  return child

participantCommands = (graph, library, only, ignore) ->
  isParticipant = (name) ->
    return common.isParticipant graph.processes[name]

  commands = {}
  participants = Object.keys(graph.processes)
  participants = only if only?
  participants = participants.filter isParticipant
  for name in participants
    continue if ignore.indexOf(name) != -1
    component = graph.processes[name].component
    iips = common.iipsForRole graph, name
    cmd = library.componentCommand component, name, iips
    commands[name] = cmd
  return commands

exports.startProcesses = startProcesses = (commands, options, callback) ->
  start = (name, cb) =>
    cmd = commands[name]
    startProcess cmd, options, (err, child) ->
      return cb err if err
      return cb err, { name: name, command: cmd, child: child }

  debug 'starting participants', Object.keys(commands)
  names = Object.keys commands
  async.map names, start, (err, processes) ->
    return callback err if err
    processMap = {}
    for p in processes
      processMap[p.name] = p.child
    return callback null, processMap

exports.killProcesses = (processes, signal, callback) ->
  return callback null if not processes
  signal = 'SIGTERM' if not signal

  kill = (name, cb) ->
    child = processes[name]
    return cb null if not child
    child.once 'exit', (code, signal) ->
      return cb null
    child.kill signal

  pids = Object.keys(processes).map (n) -> return processes[n].pid
  debug 'killing participants', pids
  names = Object.keys processes
  return async.map names, kill, callback

# Extact the queue bindings, including types from an FBP graph definition
exports.graphBindings = graphBindings = (graph) ->
  bindings = []
  roundRobins = {}
  for name, process of graph.processes
    if process.component == 'msgflo/RoundRobin'
      roundRobins[name] =
        type: 'roundrobin'

  isParticipant = (node) ->
    return common.isParticipant graph.processes[node]

  roundRobinNames = Object.keys roundRobins
  for conn in graph.connections

    if conn.data
        # IIP
    else if conn.src.process in roundRobinNames
      binding = roundRobins[conn.src.process]
      if conn.src.port == 'deadletter'
        binding.deadletter = queueName conn.tgt
      else
        binding.tgt = queueName conn.tgt
    else if conn.tgt.process in roundRobinNames
      binding = roundRobins[conn.tgt.process]
      binding.src = queueName conn.src
    else if isParticipant(conn.tgt.process) and isParticipant(conn.src.process)
      # ordinary connection
      bindings.push
        type: 'pubsub'
        src: queueName conn.src
        tgt: queueName conn.tgt
    else
      debug 'no binding for', queueName conn.src, queueName conn.tgt

  for n, binding of roundRobins
    bindings.push binding

  return bindings

staticGraphBindings = (broker, graph, callback) ->
  bindings = graphBindings graph
  return callback null, bindings

# callbacks with bindings
discoverParticipantQueues = (broker, graph, options, callback) ->
  foundParticipantsByRole = {}

  broker = transport.getBroker broker if typeof broker == 'string'
  broker.connect (err) ->
    return callback err if err

    addParticipant = (definition) ->
      foundParticipantsByRole[definition.role] = definition

      # determine if we are still lacking any definitions
      wantedRoles = []
      for name, proc of graph.processes
        wantedRoles.push name
      foundRoles = Object.keys foundParticipantsByRole
      missingRoles = []
      for wanted in wantedRoles
        idx = foundRoles.indexOf wanted
        found = idx != -1
        #console.log 'found?', wanted, found
        missingRoles.push wanted if not found

      debug 'addparticipant', definition.role, missingRoles
      #console.log 'wanted, found, missing', wantedRoles, foundRoles, missingRoles

      if missingRoles.length == 0
        return if not callback
        callback null, foundParticipantsByRole
        callback = null
        return

    onTimeout = () =>
      return if not callback
      callback new Error 'setup: Participant discovery timed out'
      callback = null
      return
    timeout = setTimeout onTimeout, options.timeout

    broker.subscribeParticipantChange (msg) =>
      data = msg.data
      if data.protocol == 'discovery' and data.command == 'participant'
        addParticipant data.payload
        broker.ackMessage msg
      else
        broker.nackMessage msg
        throw new Error 'Unknown FBP message'

bindingsFromDefinitions = (graph, definitions) ->
  bindings = []
  isParticipant = (node) ->
    return common.isParticipant graph.processes[node]

  findQueue = (ports, portname) ->
    found = null
    for port in ports
      #console.log 'port', port.id, portname, port.id == portname, port.queue
      if port.id == portname
        found = port.queue
    throw new Error "Could not find #{portname} in #{JSON.stringify(ports)}" if not found
    return found

  for conn in graph.connections
    continue unless conn.src # IIP
    if isParticipant(conn.tgt.process) and isParticipant(conn.src.process)
      # ordinary connection
      srcDef = definitions[conn.src.process]
      tgtDef = definitions[conn.tgt.process]
      srcQueue = findQueue srcDef.outports, conn.src.port
      tgtQueue = findQueue tgtDef.inports, conn.tgt.port
      console
      bindings.push
        type: 'pubsub'
        src: srcQueue
        tgt: tgtQueue
    else
      debug 'no binding for', queueName conn.src, queueName conn.tgt

  return bindings


exports.normalizeOptions = normalize = (options) ->
  common.normalizeOptions options
  options.libraryfile = path.join(process.cwd(), 'package.json') if not options.libraryfile

  options.only = options.only.split(',') if typeof options.only == 'string' and options.only
  options.ignore = options.ignore.split(',') if typeof options.ignore == 'string' and options.ignore
  options.forward = options.forward.split(',') if typeof options.forward == 'string' and options.forward
  options.only = null if not options.only
  options.ignore = [] if not options.ignore
  options.forward = [] if not options.forward
  options.extrabindings = [] if not options.extrabindings
  options.discover = false if not options.discover?
  options.forever = false if not options.forever?
  options.timeout = 10000 if not options.timeout?

  return options

exports.bindings = setupBindings = (options, callback) ->
  options = normalize options

  getBindings = staticGraphBindings # use queue name convention, read directly from graph file
  if options.discover
    # wait for FBP discovery messsages, use queues from there
    getBindings = (broker, graph, cb) ->
      discoverParticipantQueues options.broker, graph, options, (err, defs) ->
        return cb err if err
        #console.log 'got defs', definitions
        bindings = bindingsFromDefinitions graph, defs
        return cb null, bindings

  throw new Error 'ffss' if not callback
  common.readGraph options.graphfile, (err, graph) ->
    return callback err if err

    broker = transport.getBroker options.broker
    broker.connect (err) ->
      return callback err if err
      getBindings broker, graph, (err, bindings) ->
        return callback err if err

        bindings = bindings.concat options.extrabindings
        addBindings broker, bindings, (err) ->
          return callback err, bindings, graph

exports.participants = setupParticipants = (options, callback) ->
  options = normalize options
  common.readGraph options.graphfile, (err, graph) ->
    return callback err if err

    lib = new Library { configfile: options.libraryfile }
    lib.load (err) ->
      return callback err if err
      commands = participantCommands graph, lib, options.only, options.ignore
      return startProcesses commands, options, callback

exports.parse = parse = (args) ->
  graph = null
  program
    .arguments('<graph.fbp/.json>')
    .option('--broker <URL>', 'URL of broker to connect to', String, null)
    .option('--participants [BOOL]', 'Also set up participants, not just bindings', Boolean, false)
    .option('--only <one,two,three>', 'Only set up participants for these roles', String, '')
    .option('--ignore <one,two,three>', 'Do not set up participants for these roles', String, null)
    .option('--library <FILE.json>', 'Library definition to use', String, 'package.json')
    .option('--forward [stderr,stdout]', 'Forward child process stdout and/or stderr', String, '')
    .option('--discover [BOOL]', 'Whether to wait for FBP discovery messages for queue info', Boolean, false)
    .option('--timeout <SECONDS>', 'How long to wait for discovery messages', Number, 30)
    .option('--forever [BOOL]', 'Keep running until killed by signal', Boolean, false)
    .option('--shell [shell]', 'Run participant commands in a shell', String, '')
    .action (gr, env) ->
      graph = gr
    .parse args

  program.libraryfile = program.library
  program.graphfile = graph
  program.timeout = program.timeout*1000
  return program

exports.prettyFormatBindings = pretty = (bindings) ->
  lines = []
  for b in bindings
    type = b.type.toUpperCase()
    if b.type == 'roundrobin'
      if b.tgt and b.deadletter
        lines.push "DEADLETTER:\t #{b.tgt} -> #{b.deadletter}"
      if b.tgt and b.src
        lines.push "ROUNDROBIN:\t #{b.src} -> #{b.tgt}"
    else if b.type == 'pubsub'
      lines.push "PUBSUB: \t #{b.src} -> #{b.tgt}"
    else
      lines.push "UNKNOWN binding type: #{b.type}"

  return lines.join '\n'

exports.main = main = () ->
  options = parse process.argv

  if not options.graphfile
    console.error 'ERROR: No graph file specified'
    program.help()
    process.exit()

  maybeSetupParticipants = (options, callback) ->
    return callback null, {}
  maybeSetupParticipants = setupParticipants if options.participants
  maybeSetupParticipants options, (err, p) ->
    throw err if err
    console.log 'Set up participants', Object.keys(p)

    setupBindings options, (err, bindings) ->
      throw err if err
      console.log 'Set up bindings:\n', pretty bindings

      if options.forever
        console.log '--forever enabled, keeping alive'
        setInterval () ->
          null # just keep alive
        , 1000
      else
        process.exit 0

