'use strict';
// ==================================================================================
// network.js
// ----------------------------------------------------------------------------------
// Description:   System Information - library
//                for Node.js
// Copyright:     (c) 2014 - 2017
// Author:        Sebastian Hildebrandt
// ----------------------------------------------------------------------------------
// License:       MIT
// ==================================================================================
// 9. Network
// ----------------------------------------------------------------------------------

const os = require('os');
const exec = require('child_process').exec;
const execSync = require('child_process').execSync;
const fs = require('fs');
const util = require('./util');

let _platform = os.type();

const _linux = (_platform === 'Linux');
const _darwin = (_platform === 'Darwin');
const _windows = (_platform === 'Windows_NT');
const NOT_SUPPORTED = 'not supported';

let _network = {};
let _default_iface;

function getValue(lines, property, separator) {
  separator = separator || ':';
  property = property.toLowerCase();
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].toLowerCase().startsWith(property)) {
      const parts = lines[i].split(separator);
      if (parts.length > 1) {
        return parts[1].trim();
      }
    }
  }
  return '';
}

function getDefaultNetworkInterface() {

  if (!_default_iface) {
    let ifacename = '';
    let scopeid = 9999;
    if (_linux || _darwin) {
      let cmd = (_linux ? "route 2>/dev/null | grep default | awk '{print $8}'" : "route get 0.0.0.0 2>/dev/null | grep interface: | awk '{print $2}'");
      let result = execSync(cmd);
      ifacename = result.toString().split('\n')[0];
    }

    if (!ifacename) {         // fallback - "first" external interface (sorted by scopeid)

      const compare = function(a,b) {
        if (a.scopeid < b.scopeid)
          return -1;
        if (a.scopeid > b.scopeid)
          return 1;
        return 0;
      }

      let ifaces = os.networkInterfaces();

      for (let dev in ifaces) {
        if (ifaces.hasOwnProperty(dev)) {
          ifaces[dev].forEach(function (details) {
            if (details && details.internal === false && details.scopeid && details.scopeid < scopeid) {
              ifacename = dev;
              scopeid = details.scopeid;
            }
          })
        }
      }
    }
    if (ifacename) _default_iface = ifacename;
  }
  return _default_iface;
}

exports.getDefaultNetworkInterface = getDefaultNetworkInterface;

function networkInterfaceDefault(callback) {

  return new Promise((resolve, reject) => {
    process.nextTick(() => {
      let result = getDefaultNetworkInterface();
      if (callback) { callback(result) }
      resolve(result);
    });
  });
}

exports.networkInterfaceDefault = networkInterfaceDefault;

// --------------------------
// NET - interfaces

function networkInterfaces(callback) {

  return new Promise((resolve) => {
    process.nextTick(() => {
      let ifaces = os.networkInterfaces();
      let result = [];

      for (let dev in ifaces) {
        let ip4 = '';
        let ip6 = '';
        let mac = '';
        if (ifaces.hasOwnProperty(dev)) {
          ifaces[dev].forEach(function (details) {
            if (details.family === 'IPv4') {
              ip4 = details.address
            }
            if (details.family === 'IPv6') {
              ip6 = details.address
            }
            mac = details.mac
          });
          let internal = (ifaces[dev] && ifaces[dev][0]) ? ifaces[dev][0].internal : null;
          result.push({ iface: dev, ip4: ip4, ip6: ip6, mac: mac, internal: internal })
        }
      }
      if (callback) { callback(result) }
      resolve(result);
    });
  });
}

exports.networkInterfaces = networkInterfaces;

// --------------------------
// NET - Speed

function calcNetworkSpeed(iface, rx, tx, operstate) {
  let result = {
    iface: iface,
    operstate: operstate,
    rx: 0,
    tx: 0,
    rx_sec: -1,
    tx_sec: -1,
    ms: 0
  };

  if (_network[iface] && _network[iface].ms) {
    result.rx = rx;
    result.tx = tx;
    result.ms = Date.now() - _network[iface].ms;
    result.rx_sec = (rx - _network[iface].rx) / (result.ms / 1000);
    result.tx_sec = (tx - _network[iface].tx) / (result.ms / 1000);
    _network[iface].rx = rx;
    _network[iface].tx = tx;
    _network[iface].rx_sec = result.rx_sec;
    _network[iface].tx_sec = result.tx_sec;
    _network[iface].ms = Date.now();
    _network[iface].last_ms = result.ms;
  } else {
    result.rx = rx;
    result.tx = tx;
    if (!_network[iface]) _network[iface] = {};
    _network[iface].rx = rx;
    _network[iface].tx = tx;
    _network[iface].rx_sec = -1;
    _network[iface].tx_sec = -1;
    _network[iface].ms = Date.now();
    _network[iface].last_ms = 0;
  }
  return result;
}

function networkStats(iface, callback) {

  function parseLinesWindowsNics(sections){
    let nics = [];
    for (let i in sections) {
      if (sections.hasOwnProperty(i)) {
        if (sections[i].trim() !== "") {

          let lines = sections[i].trim().split('\r\n');
          let netEnabled = getValue(lines, 'NetEnabled', '=');
          if (netEnabled) {
            nics.push({
              mac: getValue(lines, 'MACAddress', '=').toLowerCase(),
              name: getValue(lines, 'Name', '=').replace(/[()\[\] ]+/g, "").toLowerCase(),
              netEnabled: netEnabled === 'TRUE'
            });
          }
        }
      }
    }
    return nics;
  }

  function parseLinesWindowsPerfData(sections){
    let perfData = [];
    for (let i in sections) {
      if (sections.hasOwnProperty(i)) {
        if (sections[i].trim() !== "") {

          let lines = sections[i].trim().split('\r\n');
          perfData.push({
            name: getValue(lines, 'Name', '=').replace(/[()\[\] ]+/g, "").toLowerCase(),
            rx: parseInt(getValue(lines, 'BytesReceivedPersec', '='),10),
            tx: parseInt(getValue(lines, 'BytesSentPersec', '='),10)
          });
        }
      }
    }
    return perfData;
  }


  // fallback - if only callback is given
  if (util.isFunction(iface) && !callback) {
    callback = iface;
    iface = '';
  }

  return new Promise((resolve, reject) => {
    process.nextTick(() => {

      _default_iface = _default_iface || getDefaultNetworkInterface();
      iface = iface || _default_iface; // (_darwin ? 'en0' : 'eth0');

      let result = {
        iface: iface,
        operstate: 'unknown',
        rx: 0,
        tx: 0,
        rx_sec: -1,
        tx_sec: -1,
        ms: 0
      };

      let operstate = 'unknown';
      let rx = 0;
      let tx = 0;

      let cmd, lines, stats;
      if (!_network[iface] || (_network[iface] && !_network[iface].ms) || (_network[iface] && _network[iface].ms && Date.now() - _network[iface].ms >= 500)) {
        if (_linux) {
          if (fs.existsSync('/sys/class/net/' + iface)) {
            cmd =
              "cat /sys/class/net/" + iface + "/operstate; " +
              "cat /sys/class/net/" + iface + "/statistics/rx_bytes; " +
              "cat /sys/class/net/" + iface + "/statistics/tx_bytes; ";
            exec(cmd, function (error, stdout) {
              if (!error) {
                lines = stdout.toString().split('\n');
                operstate = lines[0].trim();
                rx = parseInt(lines[1]);
                tx = parseInt(lines[2]);

                result = calcNetworkSpeed(iface, rx, tx, operstate);

              }
              if (callback) { callback(result) }
              resolve(result);
            });
          } else {
            if (callback) { callback(result) }
            resolve(result);
          }
        }
        if (_darwin) {
          cmd = "ifconfig " + iface + " | grep 'status'";
          exec(cmd, function (error, stdout) {
            result.operstate = (stdout.toString().split(':')[1] || '').trim();
            result.operstate = (result.operstate || '').toLowerCase();
            result.operstate = (result.operstate === 'active' ? 'up' : (result.operstate === 'inactive' ? 'down' : 'unknown'));
            cmd = "netstat -bI " + iface;
            exec(cmd, function (error, stdout) {
              if (!error) {
                lines = stdout.toString().split('\n');
                // if there is less than 2 lines, no information for this interface was found
                if (lines.length > 1 && lines[1].trim() !== '') {
                  // skip header line
                  // use the second line because it is tied to the NIC instead of the ipv4 or ipv6 address
                  stats = lines[1].replace(/ +/g, " ").split(' ');
                  rx = parseInt(stats[6]);
                  tx = parseInt(stats[9]);

                  result = calcNetworkSpeed(iface, rx, tx, operstate);
                }
              }
              if (callback) { callback(result) }
              resolve(result);
            });
          });
        }
        if (_windows) {
          // NICs
          let perfData = [];
          let nics = [];
          cmd = "wmic nic get MACAddress, name, NetEnabled /value";
          exec(cmd, function (error, stdout) {
            if (!error) {
              const nsections = stdout.split(/\n\s*\n/);
              nics = parseLinesWindowsNics(nsections);

              // Performance Data
              cmd = "wmic path Win32_PerfRawData_Tcpip_NetworkInterface Get name,BytesReceivedPersec,BytesSentPersec,BytesTotalPersec /value";
              exec(cmd, function (error, stdout) {
                if (!error) {
                  const psections = stdout.split(/\n\s*\n/);
                  perfData = parseLinesWindowsPerfData(psections);
                }

                // Network Interfaces
                networkInterfaces().then(interfaces => {
                  // get mac from 'interfaces' by interfacename
                  let mac = '';
                  interfaces.forEach(detail => {
                    if (detail.iface === iface) {
                      mac = detail.mac;
                    }
                  })

                  // get name from 'nics' (by macadress)
                  let name = '';
                  nics.forEach(detail => {
                    if (detail.mac === mac) {
                      name = detail.name;
                      operstate = (detail.netEnabled ? 'up' : 'down')
                    }
                  })

                  // get bytes sent, received from perfData by name
                  rx = 0;
                  tx = 0;
                  perfData.forEach(detail => {
                    if (detail.name === name) {
                      rx = detail.rx;
                      tx = detail.tx;
                    }
                  });

                  if (rx && tx) {
                    result = calcNetworkSpeed(iface, parseInt(rx), parseInt(tx), operstate);
                  }
                  if (callback) { callback(result) }
                  resolve(result);
                })
              })
            }
          });
        }
      } else {
        result.rx = _network[iface].rx;
        result.tx = _network[iface].tx;
        result.rx_sec = _network[iface].rx_sec;
        result.tx_sec = _network[iface].tx_sec;
        result.ms = _network[iface].last_ms;
        if (callback) { callback(result) }
        resolve(result);
      }
    });
  });
}

exports.networkStats = networkStats;

// --------------------------
// NET - connections (sockets)

function networkConnections(callback) {

  return new Promise((resolve, reject) => {
    process.nextTick(() => {
      let result = [];
      if (_linux) {
        let cmd = "netstat -tuna | grep 'ESTABLISHED\\|SYN_SENT\\|SYN_RECV\\|FIN_WAIT1\\|FIN_WAIT2\\|TIME_WAIT\\|CLOSE\\|CLOSE_WAIT\\|LAST_ACK\\|LISTEN\\|CLOSING\\|UNKNOWN\\|VERBUNDEN'";
        exec(cmd, function (error, stdout) {
          if (!error) {
            let lines = stdout.toString().split('\n');
            lines.forEach(function (line) {
              line = line.replace(/ +/g, " ").split(' ');
              if (line.length >= 6) {
                let localip = line[3];
                let localport = '';
                let localaddress = line[3].split(':');
                if (localaddress.length > 1) {
                  localport = localaddress[localaddress.length - 1];
                  localaddress.pop();
                  localip = localaddress.join(':');
                }
                let peerip = line[4];
                let peerport = '';
                let peeraddress = line[4].split(':');
                if (peeraddress.length > 1) {
                  peerport = peeraddress[peeraddress.length - 1];
                  peeraddress.pop();
                  peerip = peeraddress.join(':');
                }
                let connstate = line[5];
                if (connstate === 'VERBUNDEN') connstate = 'ESTABLISHED';
                if (connstate) {
                  result.push({
                    protocol: line[0],
                    localaddress: localip,
                    localport: localport,
                    peeraddress: peerip,
                    peerport: peerport,
                    state: connstate
                  })
                }
              }
            });
            if (callback) {
              callback(result)
            }
            resolve(result);
          } else {
            cmd = "ss -tuna | grep 'ESTAB\\|SYN-SENT\\|SYN-RECV\\|FIN-WAIT1\\|FIN-WAIT2\\|TIME-WAIT\\|CLOSE\\|CLOSE-WAIT\\|LAST-ACK\\|LISTEN\\|CLOSING'";
            exec(cmd, function (error, stdout) {

              if (!error) {
                let lines = stdout.toString().split('\n');
                lines.forEach(function (line) {
                  line = line.replace(/ +/g, " ").split(' ');
                  if (line.length >= 6) {
                    let localip = line[4];
                    let localport = '';
                    let localaddress = line[4].split(':');
                    if (localaddress.length > 1) {
                      localport = localaddress[localaddress.length - 1];
                      localaddress.pop();
                      localip = localaddress.join(':');
                    }
                    let peerip = line[5];
                    let peerport = '';
                    let peeraddress = line[5].split(':');
                    if (peeraddress.length > 1) {
                      peerport = peeraddress[peeraddress.length - 1];
                      peeraddress.pop();
                      peerip = peeraddress.join(':');
                    }
                    let connstate = line[1];
                    if (connstate === 'ESTAB') connstate = 'ESTABLISHED';
                    if (connstate === 'TIME-WAIT') connstate = 'TIME_WAIT';
                    if (connstate) {
                      result.push({
                        protocol: line[0],
                        localaddress: localip,
                        localport: localport,
                        peeraddress: peerip,
                        peerport: peerport,
                        state: connstate
                      })
                    }
                  }
                });
              }
              if (callback) {
                callback(result)
              }
              resolve(result);
            })
          }
        })
      }
      if (_darwin) {
        let cmd = "netstat -nat | grep 'ESTABLISHED\\|SYN_SENT\\|SYN_RECV\\|FIN_WAIT1\\|FIN_WAIT2\\|TIME_WAIT\\|CLOSE\\|CLOSE_WAIT\\|LAST_ACK\\|LISTEN\\|CLOSING\\|UNKNOWN'";
        exec(cmd, function (error, stdout) {
          if (!error) {

            let lines = stdout.toString().split('\n');

            lines.forEach(function (line) {
              line = line.replace(/ +/g, " ").split(' ');
              if (line.length >= 6) {
                let localip = line[3];
                let localport = '';
                let localaddress = line[3].split('.');
                if (localaddress.length > 1) {
                  localport = localaddress[localaddress.length - 1];
                  localaddress.pop();
                  localip = localaddress.join('.');
                }
                let peerip = line[4];
                let peerport = '';
                let peeraddress = line[4].split('.');
                if (peeraddress.length > 1) {
                  peerport = peeraddress[peeraddress.length - 1];
                  peeraddress.pop();
                  peerip = peeraddress.join('.');
                }
                let connstate = line[5];
                if (connstate) {
                  result.push({
                    protocol: line[0],
                    localaddress: localip,
                    localport: localport,
                    peeraddress: peerip,
                    peerport: peerport,
                    state: connstate
                  })
                }
              }
            });
            if (callback) {
              callback(result)
            }
            resolve(result);
          }
        })
      }
      if (_windows) {
        let cmd = "netstat -na";
        exec(cmd, function (error, stdout) {
          if (!error) {

            let lines = stdout.toString().split('\r\n');

            lines.forEach(function (line) {
              line = line.trim().replace(/ +/g, " ").split(' ');
              if (line.length >= 4) {
                let localip = line[1];
                let localport = '';
                let localaddress = line[1].split(':');
                if (localaddress.length > 1) {
                  localport = localaddress[localaddress.length - 1];
                  localaddress.pop();
                  localip = localaddress.join(':');
                }
                let peerip = line[2];
                let peerport = '';
                let peeraddress = line[2].split(':');
                if (peeraddress.length > 1) {
                  peerport = peeraddress[peeraddress.length - 1];
                  peeraddress.pop();
                  peerip = peeraddress.join(':');
                }
                let connstate = line[3];
                if (connstate === 'HERGESTELLT') connstate = 'ESTABLISHED';
                if (connstate.startsWith('ABH')) connstate = 'LISTEN';
                if (connstate === 'SCHLIESSEN_WARTEN') connstate = 'CLOSE_WAIT';
                if (connstate === 'WARTEND') connstate = 'TIME_WAIT';
                if (connstate === 'SYN_GESENDET') connstate = 'SYN_SENT';

                if (connstate === 'LISTENING') connstate = 'LISTEN';
                if (connstate === 'SYN_RECEIVED') connstate = 'SYN_RECV';
                if (connstate === 'FIN_WAIT_1') connstate = 'FIN_WAIT1';
                if (connstate === 'FIN_WAIT_2') connstate = 'FIN_WAIT2';
                if (connstate) {
                  result.push({
                    protocol: line[0].toLowerCase(),
                    localaddress: localip,
                    localport: localport,
                    peeraddress: peerip,
                    peerport: peerport,
                    state: connstate
                  })
                }
              }
            });
            if (callback) {
              callback(result)
            }
            resolve(result);
          }
        })
      }
    });
  });
}

exports.networkConnections = networkConnections;
