#!/usr/bin/env python3

import argparse

DESC = """roun is a program that takes numbers and turns them into human-readable
names, and vice versa.

A 'roun' is the human-readable name. For example:

    roun =  hidor-kahih
    ip   =  68.107.97.20
    hex  =  0x446b6114
    int  =  1147887892

The names are inspired by proquints <https://arxiv.org/html/0901.4016>.

Currently the algorithm is the same, but I would like to modify the algorithm at
some point to be less odd than what proquints currently is. When I get time.
"""


def _char_list_to_dict(char_list):
    return {c: k for (c, k) in zip(char_list, range(len(char_list)))}


UINT_TO_CONSONANT = "bdfghjklmnprstvz"
UINT_TO_VOWEL = "aiou"

CONSONANT_TO_UINT = _char_list_to_dict(UINT_TO_CONSONANT)
VOWEL_TO_UINT = _char_list_to_dict(UINT_TO_VOWEL)

MASK_LAST4 = 0xF
MASK_LAST2 = 0x3

CHARS_PER_CHUNK = 5


def _uint16_to_roun(uint16_val):
    val = uint16_val
    res = ["?"] * CHARS_PER_CHUNK
    for i in range(CHARS_PER_CHUNK):
        if i & 1:
            res[-i - 1] = UINT_TO_VOWEL[val & MASK_LAST2]
            val >>= 2
        else:
            res[-i - 1] = UINT_TO_CONSONANT[val & MASK_LAST4]
            val >>= 4
    return "".join(res)


def uint_to_roun(uint_val, separator="-"):
    """Convert 32-bit integer value into corresponding roun string identifier.

    >>> uint_to_roun(0x7F000001, '-')
    lusab-babad

    :param uint_val: 32-bit integer value to encode
    :param separator: string to separate character rounets
    :return: roun string identifier
    """
    if uint_val < 0 or uint_val > 0xFFFFFFFF:
        raise ValueError("uint_val should be in range 0-0xFFFFFFFF")
    return _uint16_to_roun(uint_val >> 16) + separator + _uint16_to_roun(uint_val)


def roun_to_uint(roun):
    """Convert roun string identifier into corresponding 32-bit integer value.

    >>> hex(roun_to_uint('lusab-babad'))
    '0x7F000001'

    :param roun: roun string identifier to decode
    :return: 32-bit integer representation of the roun encoded value
    """
    nchar = len(roun)
    if nchar < 10 or nchar > 11:
        raise ValueError("roun should be in form of two rounets + optional separator")

    res = 0
    for i, c in enumerate(roun):
        mag = CONSONANT_TO_UINT.get(c)
        if mag is not None:
            res <<= 4
            res += mag
        else:
            mag = VOWEL_TO_UINT.get(c)
            if mag is not None:
                res <<= 2
                res += mag
            elif i != 5:
                raise ValueError("bad roun format")
    return res


def ip2uint_str(ipv4_str):
    """Convert IPv4 string to 32-bit integer value"""
    parts = ipv4_str.split(".")
    if len(parts) != 4:
        raise ValueError(
            "Expected IPv4 address in form A.B.C.D, got {}".format(ipv4_str)
        )
    ip = [0] * 4
    for i, part in enumerate(parts):
        try:
            int_part = int(part)
        except ValueError:
            raise ValueError("Part {} of IPv4 address is not an integer".format(i))
        if int_part < 0 or int_part > 255:
            raise ValueError("Part {} of IPv4 address is not in range 0-255".format(i))
        ip[i] = int_part
    return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3]


def uint_to_ip_str(uint_val):
    "Covert 32-bit integer value to IPv4 string"
    return "{}.{}.{}.{}".format(
        (uint_val >> 24) & 0xFF,
        (uint_val >> 16) & 0xFF,
        (uint_val >> 8) & 0xFF,
        uint_val & 0xFF,
    )


def uint_to_roun_str(uint_str, separator="-"):
    return uint_to_roun(int(uint_str), separator)


def roun_to_uint_str(roun):
    return str(roun_to_uint(roun))


def hex2roun_str(hex_str, separator="-"):
    return uint_to_roun(int(hex_str, 16), separator)


def roun2hex_str(roun):
    return hex(roun_to_uint(roun))


def convert(str_val, target=None):
    """Convert between roun, integer, hex or IPv4 string representations.
    Tries to guess the representation from input.
    :param str_val: input representation (string)
    :return: output representation (string)
    """
    if target is not None and target not in {"uint", "hex", "ip"}:
        raise ValueError("Convert target should be one of: uint, hex, ip")

    if target == "uint":
        return roun_to_uint_str(str_val)

    if target == "hex":
        return roun2hex_str(str_val)

    if target == "ip":
        return uint_to_ip_str(roun_to_uint(str_val))

    # try to guess the representation
    try:
        return roun_to_uint_str(str_val)
    except ValueError:
        pass

    try:
        return uint_to_roun_str(str_val)
    except ValueError:
        pass

    try:
        return hex2roun_str(str_val)
    except ValueError:
        pass

    try:
        return uint_to_roun_str(ip2uint_str(str_val))
    except ValueError:
        pass

    raise ValueError("Unrecognized input format: {}".format(str_val))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=DESC, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument(
        "-n",
        "--uint",
        action="store_true",
        help="convert from roun to 32-bit integer",
        required=False,
    )
    parser.add_argument(
        "-x",
        "--hex",
        action="store_true",
        help="convert from roun to hexadecimal",
        required=False,
    )
    parser.add_argument(
        "-i",
        "--ip",
        action="store_true",
        help="convert from roun to IPv4",
        required=False,
    )
    parser.add_argument(
        "val",
        nargs="?",
        type=str,
        default=None,
        help="value to convert (if not specified, "
        "IP address of the current host is printed)",
    )

    args = parser.parse_args()

    target = None
    if args.uint:
        target = "uint"
    elif args.hex:
        target = "hex"
    elif args.ip:
        target = "ip"

    res = convert(args.val, target)
    print("{}".format(res))