// Implementation of the FBP protocol
// http://noflojs.org/documentation/protocol
var EventEmitter, Protocol, async, debug, defaultGraph, fbpComponentFromMsgflo, fbpPort, handleGraphMessage, handleMessage, serializeErr;

debug = require('debug')('msgflo:fbp');

EventEmitter = require('events').EventEmitter;

async = require('async');

fbpPort = function(port) {
  var m;
  m = {
    id: port.id,
    type: port.type || "any",
    description: port.description || "",
    schema: port.schema, // optional
    addressable: false,
    required: false // TODO: implement
  };
  return m;
};

fbpComponentFromMsgflo = function(name, component) {
  var info;
  if (component.definition) {
    // full info available
    info = {
      name: name,
      description: component.definition.label || component.cmd || "",
      icon: component.definition.icon || '',
      subgraph: false,
      inPorts: component.definition.inports.map(fbpPort),
      outPorts: component.definition.outports.map(fbpPort)
    };
  } else {
    // just inifial info
    info = {
      name: name,
      description: component.cmd,
      icon: '',
      subgraph: false,
      inPorts: [],
      outPorts: []
    };
  }
  return info;
};

// JSON serialization of Error objects is empty
serializeErr = function(err) {
  return {
    message: err.message
  };
};

defaultGraph = 'default/main';

handleMessage = function(proto, sub, cmd, payload, ctx) {
  var component, components, i, info, len, name, options, p, ref, ref1, ref2, runtime, sendMainGraphSource, subscribeEdge;
  debug('RECV:', sub, cmd, payload);
  if (sub === 'runtime' && cmd === 'getruntime') {
    runtime = {
      id: proto.coordinator.options.runtimeId,
      type: 'msgflo',
      version: '0.7',
      capabilities: ['protocol:runtime', 'protocol:component', 'protocol:graph', 'protocol:network', 'component:getsource', 'component:setsource'],
      graph: defaultGraph
    };
    options = proto.coordinator.library.options;
    if (((ref = options.config) != null ? ref.namespace : void 0) != null) {
      runtime.namespace = options.config.namespace;
    }
    if (((ref1 = options.config) != null ? ref1.repository : void 0) != null) {
      runtime.repository = options.config.repository;
    }
    return proto.transport.send('runtime', 'runtime', runtime, ctx);
  } else if (sub === 'runtime' && cmd === 'packet') {
    return proto.coordinator.sendToExportedPort(payload.port, payload.payload, function(err) {
      if (err) {
        return proto.transport.send('runtime', 'error', serializeErr(err), ctx);
      }
      return proto.transport.send('runtime', 'packetsent', {
        port: payload.port,
        event: payload.event,
        graph: payload.graph,
        payload: payload.payload
      }, ctx);
    });
  // Component
  } else if (sub === 'component' && cmd === 'list') {
    debug('attempting to list components');
    components = [];
    ref2 = proto.coordinator.library.components;
    for (name in ref2) {
      component = ref2[name];
      info = fbpComponentFromMsgflo(name, component);
      components.push(info);
    }
    for (i = 0, len = components.length; i < len; i++) {
      info = components[i];
      proto.transport.send('component', 'component', info, ctx);
    }
    proto.transport.send('component', 'componentsready', components.length, ctx);
    return debug('sent components', components.length);
  } else if (sub === 'component' && cmd === 'getsource') {
    sendMainGraphSource = function() {
      var graph, resp;
      graph = proto.coordinator.serializeGraph('main');
      resp = {
        code: JSON.stringify(graph),
        name: 'main',
        library: 'default',
        language: 'json'
      };
      return proto.transport.send('component', 'source', resp, ctx);
    };
    if (payload.name === defaultGraph) {
      // Main graph. Ref https://github.com/noflo/noflo-ui/issues/390
      return setTimeout(sendMainGraphSource, 0);
    } else {
      // Regular component
      return proto.coordinator.getComponentSource(payload.name, function(err, source) {
        if (err) {
          proto.transport.send('component', 'error', serializeErr(err), ctx);
          return;
        }
        return proto.transport.send('component', 'source', source, ctx);
      });
    }
  } else if (sub === 'component' && cmd === 'source') {
    p = payload;
    return proto.coordinator.addComponent(p.name, p.language, p.code, function(err) {
      if (err) {
        return proto.transport.send('component', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('component', 'source', payload);
    });
  // Network
  } else if (sub === 'network' && cmd === 'start') {
    return proto.coordinator.startNetwork(payload.graph, function(err) {
      if (err) {
        return proto.transport.sendAll('network', 'error', serializeErr(err));
      }
      return proto.transport.sendAll('network', 'started', {
        running: true,
        started: true,
        graph: payload.graph,
        time: new Date()
      });
    });
  } else if (sub === 'network' && cmd === 'stop') {
    return proto.coordinator.stopNetwork(payload.graph, function(err) {
      if (err) {
        return proto.transport.sendAll('network', 'error', serializeErr(err));
      }
      return proto.transport.sendAll('network', 'stopped', {
        running: false,
        started: true,
        graph: payload.graph,
        time: new Date()
      });
    });
  } else if (sub === 'network' && cmd === 'getstatus') {
    return proto.transport.sendAll('network', 'status', {
      running: proto.coordinator.started,
      started: proto.coordinator.started,
      graph: payload.graph,
      time: new Date()
    });
  } else if (sub === 'network' && cmd === 'edges') {
    debug('network:edges', payload.edges.length);
    subscribeEdge = function(edge, cb) {
      return proto.coordinator.subscribeConnection(edge.src.node, edge.src.port, edge.tgt.node, edge.tgt.port, function(err) {
        return cb(err);
      });
    };
    return proto.coordinator.clearSubscriptions(function(err) {
      if (err) {
        return proto.transport.sendAll('network', 'error', serializeErr(err));
      }
      return async.map(payload.edges, subscribeEdge, function(err) {
        if (err) {
          return proto.transport.sendAll('network', 'error', serializeErr(err));
        }
        return proto.transport.sendAll('network', 'edges', payload);
      });
    });
  // Graph
  } else if (sub === 'graph') {
    return handleGraphMessage(proto, cmd, payload, ctx);
  } else {
    return debug('Unhandled FBP protocol message: ', sub, cmd);
  }
};

handleGraphMessage = function(proto, cmd, payload, ctx) {
  var graph, p;
  graph = payload.graph;
  if (cmd === 'clear') {
    // FIXME: support multiple graphs
    return proto.coordinator.clearGraph(payload.id, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'clear', payload);
    });
  } else if (cmd === 'addnode') {
    return proto.coordinator.startParticipant(payload.id, payload.component, payload.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'addnode', payload);
    });
  } else if (cmd === 'removenode') {
    return proto.coordinator.stopParticipant(payload.id, payload.component, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'removenode', payload);
    });
  } else if (cmd === 'changenode') {
    return proto.coordinator.updateNodeMetadata(payload.id, payload.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'changenode', payload);
    });
  // Connections
  } else if (cmd === 'addedge') {
    debug('addedge', payload);
    p = payload;
    return proto.coordinator.connect(p.src.node, p.src.port, p.tgt.node, p.tgt.port, p.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'addedge', payload);
    });
  } else if (cmd === 'removeedge') {
    p = payload;
    return proto.coordinator.disconnect(p.src.node, p.src.port, p.tgt.node, p.tgt.port, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'removeedge', payload);
    });
  } else if (cmd === 'changeedge') {
    p = payload;
    return proto.coordinator.updateEdge(p.src.node, p.src.port, p.tgt.node, p.tgt.port, p.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'changeedge', payload);
    });
  // IIPs
  } else if (cmd === 'addinitial') {
    return proto.coordinator.addInitial(payload.tgt.node, payload.tgt.port, payload.src.data, payload.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'addinitial', payload);
    });
  } else if (cmd === 'removeinitial') {
    proto.coordinator.removeInitial(payload.tgt.node, payload.tgt.port);
    return proto.transport.sendAll('graph', 'removeinitial', payload);
  // exported ports
  } else if (cmd === 'addinport') {
    return proto.coordinator.exportPort('inport', payload.public, payload.node, payload.port, payload.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'addinport', payload);
    });
  } else if (cmd === 'addoutport') {
    return proto.coordinator.exportPort('outport', payload.public, payload.node, payload.port, payload.metadata, function(err) {
      if (err) {
        return proto.transport.send('graph', 'error', serializeErr(err), ctx);
      }
      return proto.transport.sendAll('graph', 'addoutport', payload);
    });
  } else {
    return debug('Unhandled FBP protocol message: ', 'graph', cmd);
  }
};

Protocol = class Protocol {
  constructor(transport, coordinator) {
    this.transport = transport;
    this.coordinator = coordinator;
    if (!this.coordinator) {
      throw Error('Protocol');
    }
    this.coordinator.on('exported-port-data', (port, data, graph) => {
      return this.transport.sendAll('runtime', 'packet', {
        port: port,
        event: 'data',
        payload: data,
        graph: graph
      });
    });
    this.transport.on('message', (protocol, command, payload, ctx) => {
      return handleMessage(this, protocol, command, payload, ctx);
    });
    this.coordinator.library.on('components-changed', (names, allComponents) => {
      var component, i, info, len, name, results;
      debug('components-changed', names);
      results = [];
      for (i = 0, len = names.length; i < len; i++) {
        name = names[i];
        component = allComponents[name];
        info = fbpComponentFromMsgflo(name, component);
        results.push(this.transport.sendAll('component', 'component', info));
      }
      return results;
    });
    this.coordinator.on('connection-data', (conn, data) => {
      var from, fromPort, id, msg, to, toPort;
      from = conn.src.node;
      fromPort = conn.src.port;
      to = conn.tgt.node;
      toPort = conn.tgt.port;
      debug('on data', from, fromPort, data);
      id = `${from}() ${fromPort.toUpperCase()} -> ${toPort.toUpperCase()} ${to}()`;
      msg = {
        id: id, // FIXME: https://github.com/noflo/noflo-ui/issues/293
        graph: conn.graph || defaultGraph,
        src: {
          node: from,
          port: fromPort
        },
        tgt: {
          node: to,
          port: toPort
        },
        data: data
      };
      return this.transport.sendAll('network', 'data', msg);
    });
  }

};

exports.Protocol = Protocol;
