/*
 * Copyright © 2015 Paradone
 *
 * This file is part of Paradone <https://paradone.github.io>
 *
 * Paradone is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Paradone is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Paradone.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @flow weak
 */
'use strict'

var WebSocketServer = require('ws').Server
var winston = require('winston')

/**
 * @class Tracker
 * @param {Object} [options] - Initialization options for the tracker
 * @param {number} [options.port=8081] - Tracker's port
 * @param {number} [options.neighboursNumber=3] - Number of descriptors sent to
 *        a new user
 * @param {boolean} [options.debug=false] - The tracker will save the log in
 *        `tracker.log`
 *
 * @property {Map.<string,WebSocket>} users - List of users connected to the
 *           server participating in the p2p network
 * @property {string} id - Last given id, so we can compute the next one
 * @property {number} neighboursNumber - Number of Node description given to
 *           a node in the first view
 * @property {number} port - Port where the server is running
 * @property {WebSocketServer} wss - The web socket server
 */
function Tracker(options) {
  options = options || {}

  this.logger = winston
  if(options.debug) {
    this.logger.add(winston.transports.File, { filename: 'tracker.log' })
  }

  this.id = 0
  this.users = {}
  this.neighboursNumber = options.neighboursNumber || 3
  this.port = options.port || 8081
  this.wss = new WebSocketServer({ port: this.port })

  this.logger.info('Tracker started on port ' + this.port)

  this.wss.on('connection', this.onconnection.bind(this))

  process.on('exit', function() {
    // Shut server down nicely before exiting
    this.wss.close()
    process.exit(0)
  }.bind(this))
}

/**
 * @type {number}
 */
Tracker.forwardNumber = 3

/**
 * @return {number} a new randomish id
 */
Tracker.prototype.nextId = function() {
  return (++this.id) + ':' + String(Math.random()).slice(2, 8)
}

/**
 * Creates a partial view for a given node
 *
 * @param {string} from - Id of the requesting node
 * @return {Array.<NodeDescriptor>} A list of node descriptiors
 */
Tracker.prototype.selectPartialView = function(from) {
  return this.randomUserIds(this.neighboursNumber, from).map(function(id) {
    return { id: id, age: 0 }
  })
}

/**
 * Associates a new socket to a user
 *
 * @event Tracker##onconnection
 * @param {Object} socket - The web socket associated to the client
 */
Tracker.prototype.onconnection = function(socket) {
  var id = this.nextId()
  this.logger.info('New user just connected ' + id)

  // Listen to what the peer has to say
  socket.on('message', this.onmessage.bind(this, id))
  socket.on('close', this.ondisconnect.bind(this, id))

  // Send an ID to the new user
  // Send a view so he can pick some nodes to connect to
  socket.send(JSON.stringify({
    type: 'first-view',
    from: '-1',
    to: id,
    forwardBy: [],
    data: {
      id: id,
      view: this.selectPartialView(id)
    }
  }))

  // Save the user
  this.users[id] = socket
}

/**
 * Handles when a message from a node is received and has to be forwarded to an
 * other node on the network. Needs to be parsed as its transmitted serialized.
 *
 * @event Tracker#onmessage
 * @param {string} message - Serialized message to be transmitted
 * @param {boolean} flags.binary - Data transmitted in binary form?
 */
Tracker.prototype.onmessage = function(id, message, flags) {
  if(flags.binary) {
    // DEBUG
    this.logger.error('Message transmitted as binary')
  }

  var info = JSON.parse(message)
  var from = info.from
  var to = info.to

  // Update the forward information in order to reroute the message through the
  // signal. If a request was sent through the signal then the response should
  // probably be sent back through it too (one of the peer isn't connected to
  // the mesh for example)
  info.forwardBy = ['signal']
  message = JSON.stringify(info)
  this.logger.debug(message)
  // Find to whom it's destined
  if(info.type === 'signal:keepalive') {
    return
  } else if(to !== -1 && typeof this.users[to] !== 'undefined') {
    this.logger.info('Direct forward from', id, 'to', to)
    this.users[to].send(message)
  } else {
    // Select three peers and transmit message?
    this.logger.info('Transmitting', message, 'from node', id, 'to nodes:')
    this.randomUserIds(3, from).forEach(function(remoteid) {
      this.logger.info(remoteid)
      var user = this.users[remoteid]
      user.send(message)
    }.bind(this))
  }
}

/**
 * Triggered on user disconnection. The user entry is removed
 *
 * @event Tracker#ondisconnect
 * @param {string} userID - The id of the disconnected user
 */
Tracker.prototype.ondisconnect = function(userID) {
  this.logger.info('The node ' + this.id + ' disconnected')
  delete this.users[userID]
}

/**
 * In place array shuffle
 *
 * @param {Array.<number>} array - Array to be shuffled
 * @return {Array.<number>} Same array with shuffled elements
 */
var shuffleArray = function(array) {
  var i, j, temp
  for(i = array.length - 1; i > 0; i--) {
    j = Math.floor(Math.random() * (i + 1))
    temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

/**
 * Returns at most `nbr`users. If less than `nbr` users are registered they will
 * all be returned.
 *
 * @param {number} nbr - Number of ids to be returned
 * @param {number} from - Filter out this id
 * @return {Array.<integer>} Array of user ids
 */
Tracker.prototype.randomUserIds = function(nbr, from) {
  return shuffleArray(Object.keys(this.users))
    .filter(function(id) {
      return id !== from
    }).slice(0, nbr)
}

Tracker.prototype.close = function() {
  return this.wss.close()
}

module.exports = Tracker

/**
 * @typedef {Object} NodeDescriptor
 * @property {string} id - Id of the node
 * @property {number} age - Age of the descriptor
 * @description Holds informations about a node present on the p2p network
 */
