"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 12th Jul., 2024

    OBJECTIVE:

        To provide an easy way to get geolocation information of an IP address.

    REFERENCES:

        1) https://medium.com/@tubelwj/how-to-retrieve-ip-geolocation-information-in-python-929e15041e3e

    DOWNLOADS:

        N/A

"""


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# For working with IP Addresses:
import ipaddress


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


def ipv4_to_int(ip_string):

    """
    Converts an IP (v4) string to an integer value.
    :param ip_string: The IP address (v4) that you want to convert to integer format.
    :return: An integer representation of the IP (v4) address.
    """

    ip_numerical = int(ipaddress.IPv4Address(ip_string))
    return ip_numerical


# ---------------------------------------------------------------------------------------------------------------------


def int_to_ipv4(ip_numerical):

    """
    Interprets the IP (v4) value from the given integer value.
    :param ip_numerical: The integer value that represents an IP (v4) address.
    :return:
    """

    ip_string = str(ipaddress.IPv4Address(ip_numerical))
    return ip_string


# ---------------------------------------------------------------------------------------------------------------------


def ipv4_to_bin(ip_string):

    """
    Converts an input IP (v4) address to the binary string that represents the 32 bits.
    :param ip_string: The IP (v4) string in a format like "192.168.0.1"
    :return: The binary representation (as a string) of the input IP address.
    """

    ip_binary = bin(int(ipaddress.IPv4Address(ip_string)))[2:].zfill(32)
    return ip_binary


# ---------------------------------------------------------------------------------------------------------------------


def bin_to_ipv4(ip_binary):

    """
    Interprets the IP (v4) value from the given binary string.
    :param ip_binary: The string of 1s and 0s that represents the IP (v4) address.
    :return: The IP (v4) address as a string.
    """

    ip_string = str(ipaddress.IPv4Address(int(ip_binary, 2)))
    return ip_string


# ---------------------------------------------------------------------------------------------------------------------


def get_ipv4_range(ip_string, as_string = True):

    """
    Given a network description in the format "88.95.100.128/25", this function tells you the first and last IP
    addresses of that network. Useful for determining if an IP address lies in a network.
    :param ip_string: The input network description in the format "88.95.100.128/25"
    :param as_string: To select between integer and string formats for the IP range output.
    :return: The first and last IP addresses of the input network, and the count.
    """

    # Extract the components of the string:
    ip_components = ip_string.split("/")
    ip_addr = ipv4_to_int(ip_components[0])
    ip_bits = int(ip_components[1])

    # Convert the mask number to binary representation:
    ip_mask = (1 << ip_bits) - 1
    ip_mask = ip_mask << (32 - ip_bits)
    inv_ip_mask = (~ip_mask) & 0xFFFF

    # Figure out the start and end IP addresses:
    start_ip = ip_addr & ip_mask
    end_ip = ip_addr | inv_ip_mask
    count = end_ip - start_ip + 1

    # If the IPs are needed as strings, we perform the conversion:
    if as_string:
        start_ip = int_to_ipv4(start_ip)
        end_ip = int_to_ipv4(end_ip)

    # Done here:
    return start_ip, end_ip, count


# ---------------------------------------------------------------------------------------------------------------------


def to_hyphen_notation(value: str) -> str | None:

    """
    Takes in an IP pool (range) in either CIDR notation or already in hyphen-separated notation and parses it into the
    hyphen-separated notation.
    :param value: The IP-range in either of the accepted formats.
    :return: The IP range in hyphen-separated notation.
    """

    # Let's start by assuming failure:
    success = False
    start_ip = None
    end_ip = None

    # Ensure that the input is treated as a string:
    value = str(value)

    # First we check if the IP has been given in the CIDR notation:
    if not success:
        try:

            # Try to extract the first and last IP addressed from the input:
            ip_net = ipaddress.IPv4Network(value, strict = False)
            start_ip = ip_net.network_address
            end_ip = ip_net.broadcast_address

            success = True

        # In case the CIDR interpretation doesn't work:
        except:
            success = False

    # Now we try to parse the input string as a hyphen-separated input:
    if not success:
        try:

            # Split at the hyphen and take the parts:
            parts = value.split("-")
            start_ip = parts[0]
            end_ip = parts[1]

            success = True

        # In case the hyphen-separated interpretation doesn't work:
        except:
            success = False

    # Done here:
    value = f"{start_ip}-{end_ip}" if success else None
    return value


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    print(ipv4_to_int("255.255.255.255"))
    print(ipv4_to_int("x.x.x.x"))
