/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.statistics.util;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A Spare v4 IPTable implementation that uses nested HashMaps
 * to optimize IP address matching over ranges of IP addresses.
 *
 * @author mdiggory at atmire.com
 */
public class IPTable {

    /* A lookup tree for IP addresses and SubnetRanges */
    private Map<String, Map<String, Map<String, Set<String>>>> map =
            new HashMap<String, Map<String, Map<String, Set<String>>>>();

    /**
     * Can be full v4 IP, subnet or range string
     *
     * @param ip
     */
    public void add(String ip) throws IPFormatException {

        String[] start;

        String[] end;

        String[] range = ip.split("-");

        if (range.length >= 2) {

            start = range[0].trim().split("/")[0].split("\\.");
            end = range[1].trim().split("/")[0].split("\\.");

            if (start.length != 4 || end.length != 4)
            {
                throw new IPFormatException(ip + " - Ranges need to be full IPv4 Addresses");
            }

            if (!(start[0].equals(end[0]) && start[1].equals(end[1]) && start[2].equals(end[2]))) {
                throw new IPFormatException(ip + " - Ranges can only be across the last subnet x.y.z.0 - x.y.z.254");
            }

        } else {
            //need to ignore CIDR notation for the moment.
            //ip = ip.split("\\/")[0];

            String[] subnets = ip.split("\\.");

            if (subnets.length < 3) {
                throw new IPFormatException(ip + " - require at least three subnet places (255.255.255.0");

            }

            start = subnets;
            end = subnets;
        }

        if (start.length >= 3) {


            Map<String, Map<String, Set<String>>> first = map.get(start[0]);

            if (first == null) {
                first = new HashMap<String, Map<String, Set<String>>>();
                map.put(start[0], first);
            }

            Map<String, Set<String>> second = first.get(start[1]);


            if (second == null) {
                second = new HashMap<String, Set<String>>();
                first.put(start[1], second);
            }

            Set<String> third = second.get(start[2]);

            if (third == null) {
                third = new HashSet<String>();
                second.put(start[2], third);
            }

            //now populate fourth place (* or value 0-254);

            if (start.length == 3) {
                third.add("*");
            }

            if (third.contains("*")) {
                return;
            }

            if (start.length >= 4) {
                int s = Integer.valueOf(start[3]);
                int e = Integer.valueOf(end[3]);
                for (int i = s; i <= e; i++) {
                    third.add(String.valueOf(i));
                }
            }
        }
    }

    /** Check whether a given address is contained in this netblock.
     * 
     * @param ip the address to be tested
     * @return true if {@code ip} is within this table's limits
     * @throws IPFormatException
     */
    public boolean contains(String ip) throws IPFormatException {

        String[] subnets = ip.split("\\.");

        if (subnets.length != 4)
        {
            throw new IPFormatException("needs to be a single IP address");
        }

        Map<String, Map<String, Set<String>>> first = map.get(subnets[0]);

        if (first == null)
        {
            return false;
        }

        Map<String, Set<String>> second = first.get(subnets[1]);

        if (second == null)
        {
            return false;
        }

        Set<String> third = second.get(subnets[2]);

        if (third == null)
        {
            return false;
        }

        return third.contains(subnets[3]) || third.contains("*");

    }

    /** Convert to a Set.
     * @return this table's content as a Set
     */
    public Set<String> toSet() {
        HashSet<String> set = new HashSet<String>();

        for (Map.Entry<String, Map<String, Map<String, Set<String>>>> first : map.entrySet()) {
            String firstString = first.getKey();
            Map<String, Map<String, Set<String>>> secondMap = first.getValue();

            for (Map.Entry<String, Map<String, Set<String>>> second : secondMap.entrySet()) {
                String secondString = second.getKey();
                Map<String, Set<String>> thirdMap = second.getValue();

                for (Map.Entry<String, Set<String>> third : thirdMap.entrySet()) {
                    String thirdString = third.getKey();
                    Set<String> fourthSet = third.getValue();

                    if (fourthSet.contains("*")) {
                        set.add(firstString + "." + secondString + "." + thirdString);
                    } else {
                        for (String fourth : fourthSet) {
                            set.add(firstString + "." + secondString + "." + thirdString + "." + fourth);
                        }
                    }

                }
            }
        }

        return set;
    }


    /**
     * Exception Class to deal with IPFormat errors.
     */
    public static class IPFormatException extends Exception {
        public IPFormatException(String s) {
            super(s);
        }
    }


}
