"""
very simple ini parser and tools

tested on python 3.6

contact: adalca at csail.mit.edu

TODO: see 
  from collections import namedtuple
  instead of Struct
"""

# built-in modules
# we'll need python's ini parser: 'configparser'
import configparser

def ini_to_struct(file):
    """
    very simple ini parser that expands on configparser
    tries to cast values from string whereever possible
    parsed data ini can be accessed with

    data = ini_to_struct(file)
    value = data.section.key

    does not support hierarchical sections

    Parameters:
        file: string full filename of the ini file.

    Returns:
        stuct: a Struct that allows ini data to be access in the manner of data.section.key
    """

    # read the file via config.
    conf = configparser.ConfigParser()
    confout = conf.read(file)
    assert len(confout) > 0, 'Cannot read file %s ' % file

    # prepare the Struct
    strct = Struct()

    # go through the sections in the ini file
    for sec in conf.sections():

        # each section is its own struct
        secstrct = Struct()

        # go through the keys
        for key in conf[sec]:
            val = conf[sec][key]

            # try to cast the data
            ret, done = str_convert_single(val)

            # if couldn't cast, try a comma/whitespace separated list
            if not done:
                lst = METHOD_NAME(val)

                # if the size of the list is 1, we didn't achieve anything
                if len(lst) == 1:
                    ret = lst[0]  # still not done

                # if we actually get a list, only keep it if we can cast its elements to something
                # otherwise keep the entry as an entire string
                else:
                    # make sure all elements in the list convert to something
                    done = all([str_convert_single(v)[1] for v in lst])
                    if done:
                        ret = [str_convert_single(v)[0] for v in lst]

            # defeated, accept the entry as just a simple string...
            if not done:
                ret = val  # accept string

            # assign secstrct.key = ret
            setattr(secstrct, key, ret)

        # assign strct.sec = secstrct
        setattr(strct, sec, secstrct)

    return strct


class Struct():
    """
    a simple struct class to allow for the following syntax:
    data = Struct()
    data.foo = 'bar'
    """

    def __str__(self):
        return self.__dict__.__str__()


def str_to_none(val):
    """
    cast a string to a None

    Parameters:
        val: the string to cast

    Returns:
        (casted_val, success)
        casted val: the casted value if successful, or None
        success: None if casting was successful
    """
    if val == 'None':
        return (None, True)
    else:
        return (None, False)


def str_to_type(val, ctype):
    """
    cast a string to a type (e.g. int('8')), with try/except
    do *not* use for bool casting, instead see str_to_bull

    Parameters:
        val: the string to cast

    Returns:
        (casted_val, success)
        casted val: the casted value if successful, or None
        success: bool if casting was successful
    """
    assert ctype is not bool, 'use str_to_bull() for casting to bool'

    ret = None
    success = True
    try:
        ret = ctype(val)
    except ValueError:
        success = False
    return (ret, success)


def str_to_bool(val):
    """
    cast a string to a bool

    Parameters:
        val: the string to cast

    Returns:
        (casted_val, success)
        casted val: the casted value if successful, or None
        success: bool if casting was successful
    """
    if val == 'True':
        return (True, True)
    elif val == 'False':
        return (False, True)
    else:
        return (None, False)


def METHOD_NAME(val):
    """
    Split a string to a list of elements, where elements are separated by whitespace or commas
    Leading/ending parantheses are stripped.

    Returns:
        val: the string to split

    Returns:
        casted_dst: the casted list
    """
    val = val.replace('[', '')
    val = val.replace('(', '')
    val = val.replace(']', '')
    val = val.replace(')', '')

    if ',' in val:
        lst = val.split(',')
    else:
        lst = val.split()

    return lst


def str_convert_single(val):
    """
    try to cast a string to an int, float or bool (in that order)

    Parameters:
        val: the string to cast

    Returns:
        (casted_val, success)
        casted val: the casted value if successful, or None
        success: bool if casting was successful
    """
    val = val.strip()
    # try int
    ret, done = str_to_type(val, int)

    # try float
    if not done:
        ret, done = str_to_type(val, float)

    # try bool
    if not done:
        ret, done = str_to_bool(val)

    # try None
    if not done:
        ret, done = str_to_none(val)

    return (ret, done)
