/**
 * @namespace
 * @author Pratik tayade
 *
 * SubnetCIDRCalculator 1.0.8
**/

function AdviserHelper(){
  
};

AdviserHelper.allowedOctets = function(){
  return [128, 64, 32, 16, 8, 4, 2, 1, 128, 64, 32, 16];
};

AdviserHelper.cleanOctets = function( o ){
  var res = [];
  o.forEach(function(n) {
  var byte1 = Math.max( 0, Math.min( 255, parseInt( n )));
    res.push((isNaN( byte1 ) ? 0 : byte1 ));
  });
  return res;
};

AdviserHelper.IPv4SubnetMaskRange = function( netmaskbits ){
  return {'max': 28 , 'min': netmaskbits || 16};
};

AdviserHelper.kickValidation = function( a, msg, c ){
  return msg;
};

AdviserHelper.ip2Integer = function( ip ){
  var ipBytes = ip.split('.');
  return ( parseInt(ipBytes[3]) + (256 * parseInt(ipBytes[2])) + (256 * 256 * parseInt(ipBytes[1])) + (256 * 256 * 256 * parseInt(ipBytes[0])));
};

AdviserHelper.checkIp = function (ip) {
  if(typeof ip !== 'string') return false; // only do strings
  var matches = ip.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
  return (matches ? true : false);
};

AdviserHelper.ip2long = function (ip_address) {
  if(!AdviserHelper.checkIp(ip_address)) return false; // invalid IP address
  var parts = ip_address.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  return parts ? parts[1] * 16777216 + parts[2] * 65536 + parts[3] * 256 + parts[4] * 1 : false;
};

AdviserHelper.long2ip = function (ip) {
  //  discuss at: http://phpjs.org/functions/long2ip/
  // original by: Waldo Malqui Silva (http://waldo.malqui.info)
  if (!isFinite(ip))
    return false;

  return [ip >>> 24, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.');
};

AdviserHelper.getSubnetDetails = function(subnet) {

  var details = {'hosts': []};
  if(!AdviserHelper.isValidNetwork(subnet)) return ips;

  var size = Math.pow(2, 32 - subnet.split('/')[1] );
  var iprange = AdviserHelper.getIpRangeForSubnet(subnet);
  var first = AdviserHelper.ip2long(iprange.start);
  for (var i = 0; i < size; i++) {
    var ip = i + first;
    details.hosts.push(AdviserHelper.long2ip(ip));
  }
  details.noofhosts = size;
  details.startAddr = iprange.start;
  details.endAddr = iprange.end;
  return details;
};

AdviserHelper.getUnique = function( arr ){
  var u = {}, a = [];
  for(var i = 0, l = arr.length; i < l; ++i){
  if(u.hasOwnProperty(arr[i])) {
   continue;
  }
  a.push(arr[i]);
  u[arr[i]] = 1;
  }
  return a;
};

AdviserHelper.probabalCombinations = function( arr, Addressbytes, position_ ){
  var a = arr, res = [];
  for (var i = 0; i < Math.pow(2, a.length); i++) {
  var bin = (i).toString(2), set = [];
  bin = new Array((a.length-bin.length)+1).join("0")+bin;
  for (var j = 0; j < bin.length; j++) {
    if (bin[j] === "1") {
      set.push(a[j]);
    }
  }

  try {
    var sum = set.reduce(function(a, b) { return a + b; });
    res.push(sum);
    } catch(e) {

    }
  }
  res = res.filter(function(n) { return n >= Addressbytes[position_] });
  if(arr.indexOf(0) != -1) {res.push(0)};
  res = AdviserHelper.getUnique(res);
  return res;
};

AdviserHelper.IPv4PossibleSubnets = function( o, existingSubnetCIDR ){
  var openBlock = Math.floor(o.netmaskBits/8);
  var blockReminder = o.netmaskBits%8;
  var subnetsObj = {'subnets': [], 'subnetsExcluded': []};
  var startIndex = (o.subnetMaskRange.min <= 24) ? 24 : o.subnetMaskRange.min;
  //((o.subnetMaskRange.min + 3) < o.subnetMaskRange.max) ? (o.subnetMaskRange.min + 3) : o.subnetMaskRange.max;
  // Let's suggest large range first.....
  for(var i = startIndex; i <= o.subnetMaskRange.max; i++) {
    var res = AdviserHelper.generatePossibleSubnetFor(o, i, existingSubnetCIDR);
    subnetsObj.subnets = subnetsObj.subnets.concat(res.subnets);
    subnetsObj.subnetsExcluded = subnetsObj.subnetsExcluded.concat(res.subnetsExcluded);
  }

  for(var j = o.subnetMaskRange.min; j < startIndex; j++) {
    var res = AdviserHelper.generatePossibleSubnetFor(o, j, existingSubnetCIDR);
    subnetsObj.subnets = subnetsObj.subnets.concat(res.subnets);
    subnetsObj.subnetsExcluded = subnetsObj.subnetsExcluded.concat(res.subnetsExcluded);
  }

  return subnetsObj;
};

AdviserHelper.inRange = function( s, cidrToValidate ){
  var s1 = s.value.split('.'), s2 = cidrToValidate.split('.');
  delete s1[2] , delete s2[2];
  return (_.isEqual(s1, s2) && cidrToValidate.split('.')[2] >= s.range.from && cidrToValidate.split('.')[2] <= s.range.to) ? true : false;
};

AdviserHelper.isSubnetOverlap = function( existingSubnetCIDR, subnetCIDR ){
  var isOverlap = false;
  var ipRangeforCurrent = AdviserHelper.getIpRangeForSubnet(subnetCIDR);
  existingSubnetCIDR.forEach(function(s) {
  var ipRange = AdviserHelper.getIpRangeForSubnet(s);
  if((AdviserHelper.ip2Integer(ipRangeforCurrent.start) >= AdviserHelper.ip2Integer(ipRange.start) && AdviserHelper.ip2Integer(ipRangeforCurrent.start) <= AdviserHelper.ip2Integer(ipRange.end)) || (AdviserHelper.ip2Integer(ipRangeforCurrent.end) >= AdviserHelper.ip2Integer(ipRange.start) && AdviserHelper.ip2Integer(ipRangeforCurrent.end) <= AdviserHelper.ip2Integer(ipRange.end)) ||  (AdviserHelper.ip2Integer(ipRange.start) >= AdviserHelper.ip2Integer(ipRangeforCurrent.start) && AdviserHelper.ip2Integer(ipRange.start) <= AdviserHelper.ip2Integer(ipRangeforCurrent.end)) || (AdviserHelper.ip2Integer(ipRange.end) >= AdviserHelper.ip2Integer(ipRangeforCurrent.start) && AdviserHelper.ip2Integer(ipRange.end) <= AdviserHelper.ip2Integer(ipRangeforCurrent.end)) ) {
  isOverlap = s;
  }
  });
  return isOverlap;
};

AdviserHelper.getValidCIDR = function( parentVPC_CIDR, existingSubnetCIDR, probabal_subnets, cidrToValidate ){
  var result = {'cidrFound':false, 'newCIDR': ""};
  var parentVPC_CIDRDetail =  parentVPC_CIDR.split("/");
  
  if(probabal_subnets.subnets && probabal_subnets.subnets.length > 0) {
      var subnetObj = (probabal_subnets.subnets[0].range && probabal_subnets.subnets[0].isOverlap) ? suggestCIDRSmartly(existingSubnetCIDR, _.filter(probabal_subnets.subnets, function(s){ return s.range && s.isOverlap; })) : probabal_subnets.subnets[0].value;
      if(subnetObj) {
        result = {'cidrFound':true, 'newCIDR': subnetObj};
      }
  }

  if(cidrToValidate) {
      return AdviserHelper.isValidSubnetCIDR(probabal_subnets.subnets, existingSubnetCIDR, cidrToValidate, parentVPC_CIDRDetail);
  }

  return result;
};

AdviserHelper.getIpRangeForSubnet = function( subnetCIDR ){
  var subnet = {
  'netMask': subnetCIDR.split('/')[1],
  'pureAddressbytes': subnetCIDR.split('/')[0].split('.')
  };
  var allowedOctets = AdviserHelper.allowedOctets();
  var position_ = Math.ceil(subnet.netMask/8) - 1;
  var sIndex = subnet.pureAddressbytes.slice();
  var eIndex = subnet.pureAddressbytes.slice();
  eIndex[position_] = parseInt(eIndex[position_]) +  ((!allowedOctets[subnet.netMask%8 - 1]) ? 0 : allowedOctets[subnet.netMask%8 - 1] - 1);
  if(position_ == 2 && eIndex[3] < 255) {
    eIndex[3] = 255;
  }
  // console.log(sIndex, eIndex);
  return {'start': sIndex.join('.'), 'end': eIndex.join('.')};
};

AdviserHelper.isValidNetwork = function( ip, netmask ){
  var result = {'isValid': true};
  var pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
  if(netmask < 16 || netmask > 28){
    result = {'isValid': false, 'msg': 'Netmask must be between 16 and 28 inclusive'};
  } else if (!pattern.test(ip)) {
    result = {'isValid': false, 'msg': 'Invalid IP address'};
  }
  return result;
};

AdviserHelper.isValidSubnetCIDR = function( probabal_subnets, existingSubnetCIDR, cidrToValidate, vpcCidr ){
  var result = {
    'isValid': false,
    'msg': 'Invalid CIDR'
  };

  var probabalList = probabal_subnets.subnets
  var existingSubnetCIDR = probabal_subnets.existingSubnetCIDR || [];
  var subnetsExcluded = probabal_subnets.subnetsExcluded

  if(vpcCidr.indexOf('/') != -1) {
    vpcCidr = vpcCidr.split("/");
  }
  var subnetCIDR =  cidrToValidate.split("/");
  if(Number.isInteger(subnetCIDR[1]) && (subnetCIDR[1] < 16 || subnetCIDR[1] > 28)){
      result.isValid = false;
      result.msg = "Netmask must be between 16 and 28 inclusive";
  } else if (vpcCidr && subnetCIDR[1] < vpcCidr[1]){
      result.isValid = false;
      result.msg = "Netmask should be greater than or equal to parent VPC netmask";
  } else if (_.contains(existingSubnetCIDR, cidrToValidate)){
      result.isValid = false;
      result.msg = "This CIDR block is already taken";
  } else {
      result.isValid = (_.find(probabalList, function(s){ return (s.value == cidrToValidate || (s.range !== false && AdviserHelper.inRange(s, cidrToValidate))); })) ? true : false;
  }

  if(result.isValid || (!result.isValid && _.contains(subnetsExcluded, cidrToValidate))) {
    var isOverlap = AdviserHelper.isSubnetOverlap(existingSubnetCIDR, cidrToValidate);
    if(isOverlap  !== false) {
      result.isValid = false;
      result.msg = (isOverlap == cidrToValidate) ? "This CIDR block is already taken" : "CIDR block "+cidrToValidate+" overlaps with pre-existing CIDR block "+isOverlap+".";
    }
  }

  return result;
};

AdviserHelper.generatePossibleSubnetFor = function( o, i, existingSubnetCIDR ){
  var sliceTo = (i%8 == 0) ? 8 : i%8;
  var filteredOctets = [];
  var position_ = Math.ceil(i/8) - 1;
  var subnets = [], subnetsExcluded = [];
  var allowedOctets = AdviserHelper.allowedOctets();
  if((o.netmaskBits == 24 && i == 24) || (o.netmaskBits == 16 && i == 16)) {
    filteredOctets.push(o.pureAddressbytes[2]);
  } else if(o.netmaskBits%8 <= sliceTo && i <= 24) {
    filteredOctets = allowedOctets.slice(o.netmaskBits%8, sliceTo);
    filteredOctets.push(o.pureAddressbytes[2]);
  } else if(i >= 24 && o.pureAddressbytes[3] == 0) {
    filteredOctets = allowedOctets.slice(0, sliceTo);
    filteredOctets.push(o.pureAddressbytes[3]);
  } else {
    filteredOctets = allowedOctets.slice(0, sliceTo);
  }

  var allowedCombinations = AdviserHelper.probabalCombinations(filteredOctets, o.pureAddressbytes, position_);
  allowedCombinations.forEach(function(octet) {
    var subnetobs = o.pureAddressbytes.slice();
    subnetobs[position_] = octet;
    var subnetObject = {
      'value': subnetobs.join('.')+'/'+i,
      'ipRange': AdviserHelper.getIpRangeForSubnet(subnetobs.join('.')+'/'+i),
      'range': (i >= 25 & o.netmaskBits < 24) ? {
        'from': o.pureAddressbytes[2],
        'to': o.pureAddressbytes[2] + ((allowedOctets[o.netmaskBits%8 - 1] === undefined) ? 256 : allowedOctets[o.netmaskBits%8 - 1]) - 1
      } : false
    };
    var doesOverlap = AdviserHelper.isSubnetOverlap(existingSubnetCIDR, subnetObject.value);
    if( doesOverlap === false || subnetObject.range !== false) {    
      if(doesOverlap && subnetObject.range !== false) {   
        subnetObject.isOverlap = true;    
      }   
       subnets.push(subnetObject);
    } else {
      subnetsExcluded.push(subnetObject.value);
    }
  });
  return {'subnets': subnets, 'subnetsExcluded': subnetsExcluded};
};


var SubnetCIDRAdviser = {
  calculate : function( vpcAddress, netmaskBits, existingSubnetCIDR )
  {
    var result = {};
    var existingSubnetCIDR = existingSubnetCIDR || [];
    var subnetsObj = {};

    var networkValidity = AdviserHelper.isValidNetwork( vpcAddress, netmaskBits );
    if(!networkValidity.isValid) {
      return networkValidity;
    }
    var addressOctets = AdviserHelper.cleanOctets(vpcAddress.split( '.', 4 ));
    result.pureAddressbytes = [addressOctets[0], addressOctets[1], addressOctets[2], addressOctets[3]];
    vpcAddress = ( addressOctets[0] +'.'+ addressOctets[1] +'.'+ addressOctets[2] +'.'+ addressOctets[3] );
    result.addressDotQuad = vpcAddress.toString();
    result.netmaskBits = Math.max( 0, Math.min( 32, parseInt( netmaskBits )));  //sanity check: valid values: = 0-32 

    result.subnetMaskRange = AdviserHelper.IPv4SubnetMaskRange( result.netmaskBits );
    subnetsObj = AdviserHelper.IPv4PossibleSubnets( result, existingSubnetCIDR );
    result.subnets = subnetsObj.subnets;
    result.subnetsExcluded = subnetsObj.subnetsExcluded;
    return result;
  },
  isValidVPC : function( ip, netmask )
  {
    return AdviserHelper.isValidNetwork( ip, netmask );
  },
  isSubnetOverlap : function( existingSubnetCIDR, subnetCIDR )
  {
    return AdviserHelper.isSubnetOverlap( existingSubnetCIDR, subnetCIDR );
  },
  getIpRangeForSubnet : function( subnetCIDR )
  {
    return AdviserHelper.getIpRangeForSubnet( subnetCIDR );
  },
  getNextValidCIDR : function( parentVPC_CIDR, existingSubnetCIDR, probabal_subnets, cidrToValidate )
  {
    return AdviserHelper.getValidCIDR( parentVPC_CIDR, existingSubnetCIDR, probabal_subnets, cidrToValidate );
  },
  isValidSubnetCIDR : function( probabal_subnets, existingSubnetCIDR, cidrToValidate, vpcCidr )
  {
    return AdviserHelper.isValidSubnetCIDR( probabal_subnets, existingSubnetCIDR, cidrToValidate, vpcCidr );
  },
  getSubnetDetails : function( subnet )
  {
    return AdviserHelper.getSubnetDetails( subnet );
  }
};

if( ( typeof define === 'function' ) )
{
  define( [], function() { return SubnetCIDRAdviser; } );
}
else if( typeof exports === 'object' )
{
  module.exports = SubnetCIDRAdviser;
}