hostMinLong = ip2long(hostMin)
hostMaxLong = ip2long(hostMax)

System.debug("hostMin = "+hostMin+" ("+hostMinLong+")")
System.debug("hostMax = "+hostMax+" ("+hostMaxLong+")")

// Verification que les scopes sont bien dans le cidr
System.debug("check ipPools In CIDR: ")
for each(ipPool in ipPools){
    result = chkRangeInScope(ipPool,hostMin,hostMax,true) // true: affiche debug
    if(result != "succeed"){ // Failed (string)
        System.error(result)
        throw result // Si le pool n'est pas dans le cidr, pas la peine de regarder le reste
    }
}

System.log(ipPools.length+" ipPools: "+JSON.stringify(ipPools).replace(/."ipBegin"/g,"\n{ipBegin\""))

// reduction overlapp des ipPools
reducedIpPools = reduceOverlapp(ipPools) ; System.debug("reducedIpPools = "+reducedIpPools.join(", "))
// Transformation des ipPools en un json de busyRanges optimisés (version ipLong)
forbiddenLongRangesPools = translateIntervalInRange(hostMinLong,hostMaxLong,reducedIpPools)
System.debug("forbiddenLongRangesPools = "+JSON.stringify(forbiddenLongRangesPools))
System.debug("Pool nbAvailableIP: "+(numberOfHost - nbAvailableIP))

// Nouveau json, reflet du précédent, mais avec des IP au lieu des ipLong
// Pour qu'il soit au même format que les autres ranges d'exclusions
forbiddenRangesPool = []
for each (ipLong in forbiddenLongRangesPools){
    newLine = new Properties
    newLine.ipBegin = long2ip(ipLong.ipBegin) ; newLine.ipEnd = long2ip(ipLong.ipEnd)
    forbiddenRangesPool.push(newLine)
}
System.debug("forbiddenRangesPool = "+JSON.stringify(forbiddenRangesPool))


// Verif listOfBusyRange are all in cidr
if(!listOfBusyRange){ listOfBusyRange = [] }
System.log("listOfBusyRange is "+listOfBusyRange.length+" long")
System.debug("listOfBusyRange:"+JSON.stringify(listOfBusyRange).replace(/."ipBegin"/g,"\n{ipBegin\""))
// verif, bien dans le cidr
for each(range in listOfBusyRange){
    result = chkRangeInScope(range,hostMin,hostMax,true) // false: quiet: no debug
    if(result != "succeed"){ // Failed (string)
        System.error("listOfBusyRange check :"+result)
        Errors.push("listOfBusyRange check :"+result)
    }
}

otherExclusionRanges = []
// transformation de "listOfBusyIPs" en busyRanges: Array Of {"ipBegin":"xx.xx.xx.xx","ipEnd":"xx.xx.xx.xx"}
if(!listOfBusyIPs){ listOfBusyIPs = [] }
System.log("listOfBusyIPs = ["+listOfBusyIPs.join(", ")+"]")
for each (ip in listOfBusyIPs){
    newLine = new Properties
    newLine.ipBegin = ip ; newLine.ipEnd = ip
    otherExclusionRanges.push(newLine)
}
Errors = []
// Verification de tous les busyRanges dans le cidr
for each(range in otherExclusionRanges){
    result = chkRangeInScope(range,hostMin,hostMax,true) // false: quiet: no debug
    if(result != "succeed"){ // Failed (string)
        System.error("listOfBusyIPs check :"+result)
        Errors.push("listOfBusyIPs check :"+result)
    }
}
// transformation de "frozzenIPs" en busyRanges: Array Of {"ipBegin":"xx.xx.xx.xx","ipEnd":"xx.xx.xx.xx"}
frozzenIPsRanges = []
if(!frozzenIPs){ frozzenIPs = [] }
System.log("frozzenIPs = ["+frozzenIPs.join(", ")+"]")
for each (ip in frozzenIPs){
    newLine = new Properties
    newLine.ipBegin = ip ; newLine.ipEnd = ip
    frozzenIPsRanges.push(newLine)
}
// Verification des "frozzenIPsRanges" dans le cidr
for each(range in frozzenIPsRanges){
    result = chkRangeInScope(range,hostMin,hostMax,true) // false: quiet: no debug
    if(result != "succeed"){ // Failed (string)
        System.error("frozzenIPs check: "+result)
        Errors.push("frozzenIPs check: "+result)
    }
}
// Générer erreur si il y en a
if(Errors.length > 0){
    throw Errors.join(",")// inutile d'aller plus loin si les vérifications précédentes n'ont pas abouties
}else{
    System.log("All inputs checked OK")
}

// constitution de allExclusionRanges
allExclusionRanges = forbiddenRangesPool.concat(listOfBusyRange).concat(otherExclusionRanges).concat(frozzenIPsRanges)
System.debug("allExclusionRanges = "+JSON.stringify(allExclusionRanges).replace(/."ipBegin"/g,"\n{ipBegin\""))

// reduce overlapp sur allExclusionRanges:
reducedAllExclusionRanges = reduceOverlapp(allExclusionRanges)
System.debug("reducedAllExclusionRanges = "+reducedAllExclusionRanges.join(", "))

// Transformation de reducedAllExclusionRanges en "autorizedRanges" optimisés
autorizedRanges = translateIntervalInRange(hostMinLong,hostMaxLong,reducedAllExclusionRanges)
System.log("nbAvailableIP = "+nbAvailableIP)


// Trouver la première IP disponible dans les ranges constitués
for(I in autorizedRanges){
    interval = resultArray[I]
    nextFreeIP = long2ip(interval.ipBegin)
    System.log("For external net "+externalNetworkName+" scope "+scopeNumber+",  NextFreeIP = "+nextFreeIP+" ("+interval.ipBegin+")")
    break
}
if(!nextFreeIP){
    message = "No more available IP address in the scope "+scopeNumber+" of "+externalNetworkName
    throw message
}


////////////////////////////////////////////////  FONCTIONS /////////////////////////////////////////////
// Fonction Supression overlapp:
function reduceOverlapp(_ranges){ // _ranges ArrayType: [{"ipBegin":"10.130.31.100","ipEnd":"10.130.31.105"},{"ipBegin":"10.130.31.107","ipEnd":"10.130.31.120"}]
    // 1) Création d'une liste composite clé=(ipBegin|ipEnd) value = ip à partir de "exclusionRanges"
    //    Ex: [{"ipBegin":"10.130.31.100"},{"ipEnd":"10.130.31.105"},{"ipBegin":"10.130.31.107"},{"ipEnd":"10.130.31.120"},{"ipBegin":"10.130.31.104"},{"ipEnd":"10.130.31.104"} etc...
    compositLimits = []
    for each(LINE in _ranges){
        limit = new Properties
        limit.ipBegin = LINE.ipBegin
        compositLimits.push(limit)
        limit = new Properties
        limit.ipEnd = LINE.ipEnd
        compositLimits.push(limit)
    } // ; System.debug("compositLimits\n"+JSON.stringify(compositLimits).replace(/,/g,"\n"))
    // 2) Trie de cette liste sur les valeur d'IP sans tenir compte de la clé (ipBegin ou ipEnd)
    sortedCompositLimits = compositLimits.sort(sortByIp) ; System.debug("sortedCompositLimits\n"+JSON.stringify(sortedCompositLimits).replace(/,/g,"\n"))
    // 3) Reduction overlap.  
    //    Posons que:     "["  représente "ipBegin"    et que     "]"  représente  "ipEnd".  
    //    Avant reduction:
    //    [.....[.]....]......[.][....[.[.].]...]
    //    Après reduction:
    //    [............]......[.][..............]
    reduced = []
    count = 0
    bascul = true
    for each(borne in sortedCompositLimits){
        if(borne.ipBegin){ count ++ }//; System.debug("count = "+count) }
        if(borne.ipEnd){ count -- }//; System.debug("count = "+count) }
        if(count == 1 && bascul){ bascul = !bascul ; reduced.push(borne) }//; System.debug("push "+JSON.stringify(borne)) }
        if(count == 0 && !bascul){ bascul = !bascul ; reduced.push(borne) }//; System.debug("push "+JSON.stringify(borne)) }
    }
    System.debug("Exclusion RANGES (Après reduction overlapp):"+JSON.stringify(reduced).replace(/."ipBegin"/g,"\n{ipBegin\""))
    // 4)  transformation de l'objet "reduced" en une suite de nombres représentants les IP: [ 1 , 2 , 3 , 4 ]   -(Grace à la fonction ipLong)
    IPLongList = []
    for each(borne in reduced){
        key = Object.keys(borne)[0]
        IPLongList.push(ip2long(borne[key]))
    }
    //System.debug("Long ranges\n"+JSON.stringify(IPLongList).replace(/,/g,"\n"))
    return IPLongList
}

// Fonction transformant, au ls -lrt sein d'un un pool délimité par ipLongBegin et ipLongEnd, des Ranges constitués par une suite d'ipLongs représentant des intervals se suivants sans se recouvrir,
// en un json definissant des intervals opposés de type {"ipBegin":"10.130.31.100","ipEnd":"10.130.31.105"}
function translateIntervalInRange(ipLongBegin,ipLongEnd,ArrayOfIpLong){
    // Partant de "ArrayOfIpLong" Exemple [1 , 2 , 3 , 4] On y ajoute le "ipLongBegin" 0 au debut, et le "ipLongEnd" 5 à la fin:
    // Résultat:  [0 , 1 , 2 , 3 , 4 , 5]
    ArrayOfIpLong.unshift(ipLongBegin)
    ArrayOfIpLong.push(ipLongEnd) //System.debug("Long ranges in scope\n"+JSON.stringify(ArrayOfIpLong).replace(/,/g,"\n"))
    // Ensuite, notre tableau [0 , 1 , 2 , 3 , 4 , 5] ( soit [0[1-2][3-4]5] ) va donner [0 , 0 , 5 , 5] ( soit [0-0][5-5] )
    // c'est à dire: 2 IP disponibles: 0 et 5
    // Mais la fonction est reversibe et [0 , 0 , 5 , 5] va donner [0 , 1 , 4 , 5] soit [0[1-4]5] une forme optimisé de [0[1-2][3-4]5]
    nbAvailableIP = 0
    bascul = false
    resultArray = []
    line = new Properties
    for (I = 0 ; I < ArrayOfIpLong.length ; I++){
        bascul = (!bascul)
        value = ArrayOfIpLong[I]
        if(I == 0){ // Si indice = 0, c'est l'ip de debut du pool
            line.ipBegin = value // 1er ip de debut du premier interval autorisé
        }else{
            if(I == ArrayOfIpLong.length - 1){ // Si c'est l'IP de fin du dernier interval interdit
                line.ipEnd = value // L'IP suivante sera l'IP de fin du dernier interval autorisé  (La dernière), celle de la fin du pool
                if(line.ipBegin <= line.ipEnd){ // Si il y a plus d'une IP dans ce dernier interval
                    nbAvailableIP += (line.ipEnd - line.ipBegin + 1) // MAJ du Compteur d'IP
                    System.debug("Add oposit range ["+long2ip(line.ipBegin)+" - "+long2ip(line.ipEnd)+"]") // debug
                    resultArray.push(line) ; line = new Properties // ajout de ce dernier intervall dans le json
                }
            }else{ // il s'agit de l'IP de debut ou de fin d'un des interval interdit
                if(bascul){ // Ici c'est l'ip de fin d'un interval interdit
                    line.ipBegin = value + 1 // Donc l'ip suivante est l'ip de debut d'un interval autorisé
                }else{ // Ici c'est l'ip de debut d'un interval interdit
                    line.ipEnd = value - 1 // Donc l'ip précédente est l'ip de fin d'un interval autorisé
                    if(line.ipBegin <= line.ipEnd){ // Si il y a plus d'une IP dans ce dernier interval
                        nbAvailableIP += (line.ipEnd - line.ipBegin + 1) // MAJ du Compteur d'IP
                        System.debug("Add oposit range ["+long2ip(line.ipBegin)+" - "+long2ip(line.ipEnd)+"]") // debug
                        resultArray.push(line) ; line = new Properties // ajout ce dernier intervall dans le json
                    }
                }
            }
        }
    }
    //System.warn("nbAvailableIP = "+nbAvailableIP)
    System.debug("translated = "+JSON.stringify(resultArray))
    return resultArray
}

function sortByIp(a,b){
    //diff = ip2long(a.ipBegin) - ip2long(b.ipBegin)
    //System.debug("diff = "+diff)
    A = a.ipBegin ; if(!A){ A = a.ipEnd }
    B = b.ipBegin ; if(!B){ B = b.ipEnd }
    diff = ip2long(A) - ip2long(B)
    if(diff > 0){ return 1 }
    if(diff < 0){ return -1 }
    if(diff == 0){ return 0 }
}

// V&rifie la validité d'un range dans un autre range (_scope est un composite)
function chkRangeInScope(_scope,_hostMin,_hostMax,debug){
    errListLimits = [] // sortie en failed
    // verif interval positif
    if(ip2long(_scope.ipBegin) > ip2long(_scope.ipEnd)){
        errListLimits.push("ipBegin ("+_scope.ipBegin+") must be lower than ipEnd ("+_scope.ipEnd+")")
    }
    // verif address de debut dans l'intervalle
    result = chkIpforLimits(_scope.ipBegin,_hostMin,_hostMax)
    if(result != "succeed"){ // Failed
        errListLimits.push(result)
    }
    // verif address de fin dans l'intervalle
    result = chkIpforLimits(_scope.ipEnd,_hostMin,_hostMax)
    if(result != "succeed"){ // Failed
        errListLimits.push(result)
    }
    // L'heure du bilan:
    if(errListLimits.length > 0){
        System.warn("chkRangeInScope(): failed")
        return errListLimits.join(", ") //Failed: return error (string)
    } else {
        if(debug){ System.debug("chkRangeInScope(): ["+_scope.ipBegin+" - "+_scope.ipEnd+"] is included in ["+_hostMin+" - "+_hostMax+"]") }
        return "succeed"
    }
}

// Vérifie qu'une ip est bien entre deux ipMin et ipMax
function chkIpforLimits(ip,ipMin,ipMax){
    ER = [] ; var myURL = new URL()
    if(!myURL.isValidIPv4Address(ip)){
        return "ip \""+ip+"\": Invalid IPv4 addr"
    }else{
        ipLong = ip2long(ip) ; ipMinLong = ip2long(ipMin) ; ipMaxLong = ip2long(ipMax)
        //System.debug(ip+"="+ipLong+"  "+ipMin+" = "+ipMinLong+"  "+ipMax+" = "+ipMaxLong+"  "+typeof(ipLong))
        if(ipLong < ipMinLong){
            ER.push("ipBegin "+ip+" must be equal or higher than "+ipMin)
        }
        if(ipLong > ipMaxLong){
            ER.push("ipEnd "+ip+" must be equal or lower than "+ipMax)
        }
        if(ER.length > 0){ // Failed
            System.error(ER.join(", "))
            return ER.join(", ")
        } else { // Success
            return "succeed"
        }
    }
}
function long2ip(l) {
    with (Math) {
        var ip1 = floor(l/pow(256,3));
        var ip2 = floor((l%pow(256,3))/pow(256,2));
        var ip3 = floor(((l%pow(256,3))%pow(256,2))/pow(256,1));
        var ip4 = floor((((l%pow(256,3))%pow(256,2))%pow(256,1))/pow(256,0));
    }
    return ip1 + '.' + ip2 + '.' + ip3 + '.' + ip4;
}
function ip2long(_ip) {
    var ips = _ip.split('.');
    var _iplong = 0;
    with (Math) {
        _iplong = ips[0]*pow(256,3)+ips[1]*pow(256,2)+ips[2]*pow(256,1)+ips[3]*pow(256,0);
    }
    return _iplong;