import re
from enum import Enum, auto
from pathlib import Path
from textwrap import wrap

from typing import List, Dict, Any

from iode.iode_cython import cython_is_NA, cython_suppress_msgs, cython_enable_msgs, IodeFileType, IodeType
from .common import IODE_FILE_TYPES

# import constants, functions and classes hidden from users but maybe useful for developers
# (for the GUI for example)
from iode.iode_cython import (IODE_DEFAULT_DATABASE_FILENAME, register_super_function, c_api_error_as_exception, 
                              set_printer_preferences, set_A2M_preferences, set_MIF_preferences, set_RTF_preferences, 
                              set_HTML_preferences)
from .common import FileType

_list_separator = r",;\s"


def is_NA(value: float) -> bool:
    """
    Check whether a float value represents a valid IODE number or an IODE *Not Available* 
    :math:`NA` value.

    Parameters
    ----------
    value: float

    Returns
    -------
    bool
        True if the float value represents an IODE *Not Available* :math:`NA` value.

    Examples
    --------
    >>> from iode import NA, is_NA
    >>> is_NA(1.0)
    False
    >>> is_NA(NA)
    True
    """
    if isinstance(value, int):
        value = float(value)
    if not isinstance(value, float):
        raise TypeError("Expected value of type float. "
                        f"Got value of type {type(value).__name__} instead.")
    return cython_is_NA(value)


def iode_number_to_str(value: float) -> str:
    return "na" if is_NA(value) else f"{value:g}"


def suppress_msgs():
    """
    Suppress the output during an IODE session
    """
    cython_suppress_msgs()


def enable_msgs():
    """
    Reset the normal output mechanism during an IODE session
    """
    cython_enable_msgs()


def split_list(list_txt: str):
    """
    Split an IODE list written as a string and return a Python list.
    By default, the delimiters are ``, ;`` and any ``whitespace``.

    Parameters
    ----------
    list_txt: str
        IODE list written as a string.

    Returns
    -------
    list(str)

    Examples
    --------
    >>> from iode import split_list
    >>> well_written_list = "A;B;C;D;E;F;G;H"
    >>> split_list(well_written_list)
    ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
    >>> badly_written_list = "  ,A,B C;D  E,,,F;, G;H,  "
    >>> split_list(badly_written_list)
    ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
    """
    if not isinstance(list_txt, str):
        raise TypeError(f"Expected value of type str. Got value of type {type(list_txt).__name__} instead.")
    splitted_list = re.split(f"[{_list_separator}]+", list_txt.strip())
    if splitted_list[0] == '':
        splitted_list = splitted_list[1:]
    if len(splitted_list) and splitted_list[-1] == '':
        splitted_list = splitted_list[:-1]
    return splitted_list


def join_lines(value: str) -> str:
    return ' '.join(line.strip() for line in value.splitlines())


class JUSTIFY(Enum):
    LEFT = auto()
    CENTER = auto()
    RIGHT = auto()


def table2str(columns: Dict[str, List[str]], sep: str = '\t', justify_funcs: Dict[str, JUSTIFY] = None, 
              max_lines: int = -1, edgeitems: int = 5, max_width_col: int = 100, max_width: int = 200, 
              precision: int = 4) -> str:
    r"""
    Return table (dict of columns) as string

    Parameters
    ----------
    columns: dict(str, list(str))
        Pairs (column name, column values) of the table to return as string.
    sep: str, optional 
        Separator between columns. 
        Defaults to '\t'.
    justify_funcs: dict[str, JUSTIFY], optional 
        Justify function to apply on string representation of column values. 
        Defaults to RIGHT for all columns.
    max_lines: int, optional 
        Maximum number of lines to show. 
        Defaults to -1 (show all lines).
    edgeitems : int, optional
        If number of lines to display is greater than max_lines, only the first and 
        last edgeitems lines are displayed. Only active if max_lines is not -1. 
        Defaults to 5.
    max_width_col: int, optional
        Maximum width (= nb characters) for any column.
        Defaults to 100.
    max_width: int, optional 
        Maximum width (= nb characters) for lines to show. 
        A value of -1 means unlimited width.
        Defaults to 200.
    precision: int, optional 
        Precision of the string representation of the float values to show. 
        Defaults to 4.

    Returns
    -------
        str

    Examples
    --------
    >>> from iode.util import table2str, JUSTIFY
    >>> columns = {'category_1': [23.9010175, 18.07965729, 95.38763094, 7.605285, 45.77738378, 31.20329029, 26.24638991, 94.85772881, 17.19156602, 86.97879573], 
    ...            'category_2': [39.52903949, 91.1525069, 83.76037566, 43.83771813, 58.17427187, 80.56636471, 76.80209378, 53.63765832, 15.94267299, 88.42504629], 
    ...            'category_3': [64.36370439, 53.8316863, 49.02940035, 61.91318117, 25.01676303, 7.52817659, 9.18225827, 16.68777191, 46.39337746, 27.30904207], 
    ...            'category_4': [92.90886005, 98.98271845, 16.87014647, 52.23406945, 75.85269188, 68.53109561, 87.69120031, 6.19099948, 57.21632568, 13.4801123], 
    ...            'category_5': [55.99231002, 29.0795402, 6.06556571, 15.85292046, 66.87820451, 61.1155681, 16.17608971, 49.67034014, 29.84330636, 73.399402], 
    ...            'category_6': [84.74263881, 86.74429788, 14.59513443, 21.0325867, 81.49218407, 0.09002046, 6.96791193, 84.88923794, 82.1819903, 1.07949955], 
    ...            'category_7': [80.65553154, 97.73020615, 27.59011683, 47.88146724, 86.22525403, 71.41743477, 67.53744625, 75.27254816, 92.51563273, 87.1841808], 
    ...            'category_8': [81.92785908, 49.47791145, 95.31219267, 78.94364085, 62.62581941, 29.65099761, 23.61973127, 81.37426694, 62.02338724, 73.65105918], 
    ...            'category_9': [18.58065375, 58.86699547, 28.93429997, 40.62139687, 47.13588669, 3.0366871, 70.22586303, 16.8850943, 3.34322589, 8.02234255], 
    ...            'category_10': [31.39205972, 63.99854372, 72.89885168, 76.08958234, 40.29562424, 19.04340988, 24.46528084, 62.13187458, 71.05022862, 58.76206382]}
    >>> s = table2str(columns)
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
    category_1  category_2      category_3      category_4      category_5      category_6      category_7      category_8      category_9      category_10
       23.9010     39.5290         64.3637         92.9089         55.9923         84.7426         80.6555         81.9279         18.5807          31.3921
       18.0797     91.1525         53.8317         98.9827         29.0795         86.7443         97.7302         49.4779         58.8670          63.9985
       95.3876     83.7604         49.0294         16.8701          6.0656         14.5951         27.5901         95.3122         28.9343          72.8989
        7.6053     43.8377         61.9132         52.2341         15.8529         21.0326         47.8815         78.9436         40.6214          76.0896
       45.7774     58.1743         25.0168         75.8527         66.8782         81.4922         86.2253         62.6258         47.1359          40.2956
       31.2033     80.5664          7.5282         68.5311         61.1156          0.0900         71.4174         29.6510          3.0367          19.0434
       26.2464     76.8021          9.1823         87.6912         16.1761          6.9679         67.5374         23.6197         70.2259          24.4653
       94.8577     53.6377         16.6878          6.1910         49.6703         84.8892         75.2725         81.3743         16.8851          62.1319
       17.1916     15.9427         46.3934         57.2163         29.8433         82.1820         92.5156         62.0234          3.3432          71.0502
       86.9788     88.4250         27.3090         13.4801         73.3994          1.0795         87.1842         73.6511          8.0223          58.7621
    <BLANKLINE>
    >>> s = table2str(columns, max_lines=6, edgeitems=3)
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
    category_1  category_2      category_3      category_4      category_5      category_6      category_7      category_8      category_9      category_10
       23.9010     39.5290         64.3637         92.9089         55.9923         84.7426         80.6555         81.9279         18.5807          31.3921
       18.0797     91.1525         53.8317         98.9827         29.0795         86.7443         97.7302         49.4779         58.8670          63.9985
       95.3876     83.7604         49.0294         16.8701          6.0656         14.5951         27.5901         95.3122         28.9343          72.8989
           ...         ...             ...             ...             ...             ...             ...             ...             ...              ...
       94.8577     53.6377         16.6878          6.1910         49.6703         84.8892         75.2725         81.3743         16.8851          62.1319
       17.1916     15.9427         46.3934         57.2163         29.8433         82.1820         92.5156         62.0234          3.3432          71.0502
       86.9788     88.4250         27.3090         13.4801         73.3994          1.0795         87.1842         73.6511          8.0223          58.7621
    <BLANKLINE>
    >>> s = table2str(columns, sep=' | ')
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
    category_1 | category_2 | category_3 | category_4 | category_5 | category_6 | category_7 | category_8 | category_9 | category_10
       23.9010 |    39.5290 |    64.3637 |    92.9089 |    55.9923 |    84.7426 |    80.6555 |    81.9279 |    18.5807 |     31.3921
       18.0797 |    91.1525 |    53.8317 |    98.9827 |    29.0795 |    86.7443 |    97.7302 |    49.4779 |    58.8670 |     63.9985
       95.3876 |    83.7604 |    49.0294 |    16.8701 |     6.0656 |    14.5951 |    27.5901 |    95.3122 |    28.9343 |     72.8989
        7.6053 |    43.8377 |    61.9132 |    52.2341 |    15.8529 |    21.0326 |    47.8815 |    78.9436 |    40.6214 |     76.0896
       45.7774 |    58.1743 |    25.0168 |    75.8527 |    66.8782 |    81.4922 |    86.2253 |    62.6258 |    47.1359 |     40.2956
       31.2033 |    80.5664 |     7.5282 |    68.5311 |    61.1156 |     0.0900 |    71.4174 |    29.6510 |     3.0367 |     19.0434
       26.2464 |    76.8021 |     9.1823 |    87.6912 |    16.1761 |     6.9679 |    67.5374 |    23.6197 |    70.2259 |     24.4653
       94.8577 |    53.6377 |    16.6878 |     6.1910 |    49.6703 |    84.8892 |    75.2725 |    81.3743 |    16.8851 |     62.1319
       17.1916 |    15.9427 |    46.3934 |    57.2163 |    29.8433 |    82.1820 |    92.5156 |    62.0234 |     3.3432 |     71.0502
       86.9788 |    88.4250 |    27.3090 |    13.4801 |    73.3994 |     1.0795 |    87.1842 |    73.6511 |     8.0223 |     58.7621
    <BLANKLINE>
    >>> s = table2str(columns, max_width=100)
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
    category_1  category_2      category_3      category_4      ...     category_7      category_8      category_9      category_10
       23.9010     39.5290         64.3637         92.9089      ...        80.6555         81.9279         18.5807          31.3921
       18.0797     91.1525         53.8317         98.9827      ...        97.7302         49.4779         58.8670          63.9985
       95.3876     83.7604         49.0294         16.8701      ...        27.5901         95.3122         28.9343          72.8989
        7.6053     43.8377         61.9132         52.2341      ...        47.8815         78.9436         40.6214          76.0896
       45.7774     58.1743         25.0168         75.8527      ...        86.2253         62.6258         47.1359          40.2956
       31.2033     80.5664          7.5282         68.5311      ...        71.4174         29.6510          3.0367          19.0434
       26.2464     76.8021          9.1823         87.6912      ...        67.5374         23.6197         70.2259          24.4653
       94.8577     53.6377         16.6878          6.1910      ...        75.2725         81.3743         16.8851          62.1319
       17.1916     15.9427         46.3934         57.2163      ...        92.5156         62.0234          3.3432          71.0502
       86.9788     88.4250         27.3090         13.4801      ...        87.1842         73.6511          8.0223          58.7621
    <BLANKLINE>    
    >>> s = table2str(columns, precision=2)
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
    category_1  category_2      category_3      category_4      category_5      category_6      category_7      category_8      category_9      category_10
         23.90       39.53           64.36           92.91           55.99           84.74           80.66           81.93           18.58            31.39
         18.08       91.15           53.83           98.98           29.08           86.74           97.73           49.48           58.87            64.00
         95.39       83.76           49.03           16.87            6.07           14.60           27.59           95.31           28.93            72.90
          7.61       43.84           61.91           52.23           15.85           21.03           47.88           78.94           40.62            76.09
         45.78       58.17           25.02           75.85           66.88           81.49           86.23           62.63           47.14            40.30
         31.20       80.57            7.53           68.53           61.12            0.09           71.42           29.65            3.04            19.04
         26.25       76.80            9.18           87.69           16.18            6.97           67.54           23.62           70.23            24.47
         94.86       53.64           16.69            6.19           49.67           84.89           75.27           81.37           16.89            62.13
         17.19       15.94           46.39           57.22           29.84           82.18           92.52           62.02            3.34            71.05
         86.98       88.43           27.31           13.48           73.40            1.08           87.18           73.65            8.02            58.76
    <BLANKLINE>

    >>> columns = {'names': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Hannah', 'Ivy', 'Jack'],
    ...            'values': ['Tempora amet voluptatem sed modi. Tempora est non est. Voluptatem velit eius modi quaerat. Quaerat quaerat dolorem sed adipisci. Numquam neque sit quiquia. Sit tempora quiquia', 
    ...                       'Numquam porro quiquia est sed labore. Sit neque modi dolor voluptatem. Quisquam amet aliquam quiquia quiquia. Voluptatem dolore dolor', 
    ...                       'Ipsum dolorem quaerat tempora ipsum adipisci quiquia. Magnam sit velit ut ut sit dolorem. Etincidunt sit sed velit tempora. Sit velit ipsum sit modi.', 
    ...                       'Numquam adipisci est tempora tempora porro. Sit numquam porro dolore sit adipisci velit est. Sed magnam adipisci voluptatem', 
    ...                       'Numquam dolore aliquam quaerat non dolore magnam non. Eius est sed aliquam dolore voluptatem consectetur. Modi sed labore amet sit sit', 
    ...                       'Quiquia ut consectetur voluptatem est. Quaerat porro consectetur adipisci est modi dolore. Ipsum modi ipsum', 
    ...                       'Etincidunt numquam non sit etincidunt. Eius labore dolore neque quiquia consectetur dolorem sit. Porro labore adipisci labore ut. Dolor magnam aliquam ipsum ipsum ut eius non.', 
    ...                       'Porro quisquam sit quiquia. Dolor ut sed modi magnam voluptatem quisquam. Consectetur', 
    ...                       'Modi sit amet est etincidunt quisquam. Amet amet quaerat eius adipisci quiquia numquam modi. Labore non labore amet ut. Adipisci voluptate', 
    ...                       'Aliquam magnam quiquia amet amet non est. Velit quaerat tempora etincidunt sit magnam dolore']}
    >>> s = table2str(columns, justify_funcs={"names": JUSTIFY.LEFT, "values": JUSTIFY.LEFT})
    >>> print(s)        # doctest: +NORMALIZE_WHITESPACE
     names                                                     values
    Alice       Tempora amet voluptatem sed modi. Tempora est non est. Voluptatem velit eius modi quaerat. Quaerat
                quaerat dolorem sed adipisci. Numquam neque sit quiquia. Sit tempora quiquia
    Bob         Numquam porro quiquia est sed labore. Sit neque modi dolor voluptatem. Quisquam amet aliquam quiquia
                quiquia. Voluptatem dolore dolor
    Charlie     Ipsum dolorem quaerat tempora ipsum adipisci quiquia. Magnam sit velit ut ut sit dolorem. Etincidunt
                sit sed velit tempora. Sit velit ipsum sit modi.
    David       Numquam adipisci est tempora tempora porro. Sit numquam porro dolore sit adipisci velit est. Sed
                magnam adipisci voluptatem
    Eve         Numquam dolore aliquam quaerat non dolore magnam non. Eius est sed aliquam dolore voluptatem
                consectetur. Modi sed labore amet sit sit
    Frank       Quiquia ut consectetur voluptatem est. Quaerat porro consectetur adipisci est modi dolore. Ipsum
                modi ipsum
    Grace       Etincidunt numquam non sit etincidunt. Eius labore dolore neque quiquia consectetur dolorem sit.
                Porro labore adipisci labore ut. Dolor magnam aliquam ipsum ipsum ut eius non.
    Hannah      Porro quisquam sit quiquia. Dolor ut sed modi magnam voluptatem quisquam. Consectetur
    Ivy         Modi sit amet est etincidunt quisquam. Amet amet quaerat eius adipisci quiquia numquam modi. Labore
                non labore amet ut. Adipisci voluptate
    Jack        Aliquam magnam quiquia amet amet non est. Velit quaerat tempora etincidunt sit magnam dolore
    <BLANKLINE>
    """
    def format_value(value: Any) -> str:
        if isinstance(value, float):
            return f"{value:.{precision}f}" if not is_NA(value) else "na"
        else:
            value = str(value).strip()
            return join_lines(value)

    justify_funcs_ = dict()
    if justify_funcs is not None:
        for key, value in justify_funcs.items():
            if value == JUSTIFY.LEFT:
                justify_funcs_[key] = str.ljust
            elif value == JUSTIFY.CENTER:
                justify_funcs_[key] = str.center
            else:
                justify_funcs_[key] = str.rjust

    cont = '...'

    nb_columns = len(columns)
    column_names = list(columns.keys())

    # check that all columns have the same length
    column_length = len(columns[column_names[0]])
    if not all(len(column) == column_length for column in columns.values()):
        raise ValueError("All columns must have the same length. Got columns of lengths (" + 
                         ', '.join(f"{name}: {len(column)}" for name, column in columns.items()) + ")")
    
    # skip lines ?
    if max_lines != -1 and column_length > max_lines:
        columns_ = dict()
        # replace middle lines of the table by '...'.
        # We show only the first and last edgeitems lines.
        for name, column in columns.items():
            columns_[name] = column[:edgeitems] + [cont] + column[-edgeitems:]
    else:
        columns_ = columns

    # prepare table (rows = columns) and format values
    table = []    
    for name, column in columns_.items():
        column = [format_value(value) for value in column]
        table += [[name] + column]

    # transpose table
    table = [list(row) for row in zip(*table)]

    # need to wrap column ?
    # note: see table2str() from larray project (original algorithm from @gdementen)
    rows = []
    for row in table:
        wrapped_row = [wrap(value, max_width_col) for value in row]
        maxlines = max(len(value) for value in wrapped_row)
        newlines = [[] for _ in range(maxlines)]
        for value in wrapped_row:
            for i in range(maxlines):
                chunk = value[i] if i < len(value) else ''
                newlines[i].append(chunk)
        rows.extend(newlines)
    table = rows
    nb_rows = len(table)

    # center column names and align values
    col_widths = []
    for j in range(nb_columns):
        column_name = table[0][j]
        justify_func = justify_funcs_.get(column_name, str.rjust)
        max_col_length = max(len(table[i][j]) for i in range(nb_rows))
        table[0][j] = column_name.center(max_col_length)
        for i in range(nb_rows):
            table[i][j] = justify_func(table[i][j], max_col_length)
        col_widths += [max_col_length]

    # skip columns ?
    # note: see table2str() from larray project (original algorithm from @gdementen)
    sep_width = len(sep) * (len(columns) - 1)
    if max_width != -1 and sum(col_widths) + sep_width > max_width:
        nb_edges = 0
        max_edges = nb_columns // 2
        if max_edges:
            # width of the "continuation" column (i.e. ellipsis column)
            col_w = len(cont)
            nb_cols = 1
            # iterate like this: first_column --> <-- last_column
            for i in range(1, max_edges + 1):
                col_w += col_widths[i-1] + col_widths[-i]
                # + 1 for the "continuation" column (i.e. ellipsis column)
                nb_cols += 2
                sep_w = len(sep) * (nb_cols - 1)
                if col_w + sep_w > max_width:
                    break
                nb_edges = i
        
        if nb_edges:
            table = [row[:nb_edges] + [cont] + row[-nb_edges:] for row in table]

    # return table as string
    s = ''
    for row in table:
        s += sep.join(row) + '\n'
    return s


def check_file(filepath: str, file_must_exist: bool = False) -> Path:
    """
    This function checks if the parent directory of the 'filepath' exists and 
    returns its absolute path.
    If 'file_must_exist' is True, then the function checks if the 'filepath' exists.
    """
    # check if empty
    if not filepath:
        raise ValueError("Empty filepath.")

    p_filepath = Path(filepath)

    # convert to absolute path
    p_filepath = p_filepath.resolve()

    # check if parent directory exist
    p_directory = p_filepath.parent
    if not p_directory.exists():
        raise ValueError(f"Directory '{p_directory}' "
                         f"in filepath '{p_filepath}' does not exist")

    if file_must_exist and not p_filepath.exists():
        raise FileNotFoundError(f"The file '{p_filepath}' does not exist")

    return p_filepath


def check_file_exists(filepath: str) -> Path:
    """
    This function checks if the 'filepath' exists and returns its absolute path.
    """
    if not filepath:
        raise ValueError("Empty filepath.")
    
    p_filepath = Path(filepath).resolve()
    if not p_filepath.exists():
        raise FileNotFoundError(f"The file '{p_filepath}' does not exist")
    
    return p_filepath


def check_filepath(filepath: str, expected_file_type: IodeFileType, file_must_exist: bool) -> str:
    r"""
    Check the validity of a filepath and its extension. 
    If the filename does not contain an extension, it is added automatically according to 'expected_file_type'.
    If the (modified) filepath is valid, it is returned. 
    Otherwise, an error is raised.

    filepath: str
        The filepath to check. It can be a relative or an absolute path. 
        If it is a relative path, it is considered to be relative to the current working directory.
    expected_file_type: IodeFileType
        The expected file type. If the filepath does not contain an extension, it is added 
        automatically according to this parameter. 
        If the filepath contains an extension, it is checked against this parameter. 
        If the extension is not valid, an error is raised. 
    file_must_exist: bool
        If True, the file must exist.

    Returns
    -------
    str:
    	The checked filepath. If the filepath did not contain an extension, it is added automatically.

    Examples
    --------
    >>> from pathlib import Path
    >>> from iode import IodeFileType, SAMPLE_DATA_DIR
    >>> from iode.util import check_filepath

	>>> # No extension but an IODE objects file -> extension added automatically
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "fun")
    >>> checked_filepath = check_filepath(filepath, IodeFileType.FILE_COMMENTS, True)
    >>> Path(checked_filepath).name
    'fun.cmt'
    >>> Path(checked_filepath).parent.name
    'data'

    >>> # any file
    >>> filepath = str(Path(SAMPLE_DATA_DIR) / "fun_xode.ac.ref")
    >>> checked_filepath = check_filepath(filepath, IodeFileType.FILE_ANY, True)
    >>> Path(checked_filepath).name
    'fun_xode.ac.ref'

    >>> # wrong directory
    >>> cwd = Path.cwd()
    >>> fake_dir = cwd.parent / "fake_dir"
	>>> filepath = str(fake_dir / "fun.cmt")
    >>> check_filepath(filepath, IodeFileType.FILE_COMMENTS, False)    # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    ValueError: Directory '...fake_dir' in filepath '...fun.cmt' does not exist
    
	>>> # wrong extension -> expect a COMMENTS file
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "fun.eqs")
    >>> check_filepath(filepath, IodeFileType.FILE_COMMENTS, False)
    Traceback (most recent call last):
    ...
    ValueError: The file 'fun.eqs' has a wrong extension '.eqs'
    Expected extensions are: ['.cmt', '.ac']
    
    >>> # wrong extension
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "fun.docx")
    >>> check_filepath(filepath, IodeFileType.FILE_TXT, False)
    Traceback (most recent call last):
    ...
    ValueError: The file 'fun.docx' has a wrong extension '.docx'
    Expected extensions are: ['.txt']

	>>> # file does not exist
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "funxxx.cmt")
    >>> check_filepath(filepath, IodeFileType.FILE_COMMENTS, True)     # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    FileNotFoundError: The file '...funxxx.cmt' does not exist

	>>> # file does not exist (no extension given)
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "funxxx")
	>>> check_filepath(filepath, IodeFileType.FILE_COMMENTS, True)     # doctest: +SKIP

    Traceback (most recent call last):
    ...
    FileNotFoundError: Neither 'funxxx.cmt' nor 'funxxx.ac' could be found in directory '...'

	>>> # No extension but not an IODE objects file
	>>> filepath = str(Path(SAMPLE_DATA_DIR) / "fun")
    >>> check_filepath(filepath, IodeFileType.FILE_TXT, True)          # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    ValueError: You must provide an extension to the file '...fun'
    """
    p_filepath: Path = check_file(filepath, False)

    if p_filepath.suffix:
        # check extension
        ext = p_filepath.suffix
        expected_extensions = IODE_FILE_TYPES[int(expected_file_type)].extensions
        if not (expected_file_type == IodeFileType.FILE_ANY or ext in expected_extensions):
            raise ValueError(f"The file '{p_filepath.name}' has a wrong extension '{ext}'\n"
                             f"Expected extensions are: {expected_extensions}")

        # check if file exist
        if file_must_exist and not p_filepath.exists():
            raise FileNotFoundError(f"The file '{p_filepath}' does not exist")
    else:
        if expected_file_type > IodeFileType.FILE_VARIABLES:
            raise ValueError(f"You must provide an extension to the file '{p_filepath}'")
        database_type: IodeType = IodeType(expected_file_type)

        # set binary format extension
        binary_ext = IODE_FILE_TYPES[int(database_type)].extensions[0]
        p_filepath = p_filepath.with_suffix(binary_ext)

        # check if file exist
        if file_must_exist:
            # check first with binary format extension
            if not p_filepath.exists():
                # switch to ascii format extension and check if file exist
                ascii_ext = IODE_FILE_TYPES[int(database_type)].extensions[1]
                p_filepath = p_filepath.with_suffix(ascii_ext)
                if not p_filepath.exists():
                    p_directory = p_filepath.parent
                    stem = p_filepath.stem
                    raise FileNotFoundError(f"Neither '{stem}{binary_ext}' nor '{stem}{ascii_ext}' "
                                            f"could be found in directory '{p_directory}'")

    return str(p_filepath)


def get_iode_file_type(filepath: str) -> IodeFileType:
    r"""
    Return the IODE file type of a filepath. The filepath can be a relative or an absolute path.

    Parameters
    ----------
    filepath: str
       The filepath to check. It can be a relative or an absolute path.

    Returns
    -------
    IodeFileType
      The IODE file type of the filepath.

    Examples
    --------
    >>> from pathlib import Path
    >>> from iode import IodeFileType, SAMPLE_DATA_DIR
    >>> from iode.util import get_iode_file_type

    >>> filename = ""
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_ANY: 32>
    >>> filename = "ws"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_ANY: 32>

    >>> get_iode_file_type(SAMPLE_DATA_DIR)
    <IodeFileType.DIRECTORY: 33>

    >>> filename = "fun.cmt"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_COMMENTS: 0>
    >>> filename = "fun.ac"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_COMMENTS: 0>

    >>> filename = "fun.eqs"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_EQUATIONS: 1>
    >>> filename = "fun.ae"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_EQUATIONS: 1>

    >>> filename = "fun.idt"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_IDENTITIES: 2>
    >>> filename = "fun.ai"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_IDENTITIES: 2>

    >>> filename = "fun.lst"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_LISTS: 3>
    >>> filename = "fun.al"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_LISTS: 3>

    >>> filename = "fun.scl"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_SCALARS: 4>
    >>> filename = "fun.as"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_SCALARS: 4>

    >>> filename = "fun.tbl"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_TABLES: 5>
    >>> filename = "fun.at"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_TABLES: 5>

    >>> filename = "fun.var"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_VARIABLES: 6>
    >>> filename = "fun.av"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_VARIABLES: 6>

    >>> filename = "fun.rep"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_REP: 16>
    
    >>> filename = "fun.log"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_LOG: 30>

    >>> filename = "fun.ini"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_SETTINGS: 31>

    >>> filename = "fun.txt"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_TXT: 25>

    >>> filename = "fun.a2m"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_A2M: 17>

    >>> filename = "fun.agl"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_AGL: 18>

    >>> filename = "fun.prf"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_PRF: 19>

    >>> filename = "fun.dif"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_DIF: 20>

    >>> filename = "fun.mif"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_MIF: 21>

    >>> filename = "fun.rtf"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_RTF: 22>

    >>> filename = "fun.asc"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_AAS: 24>

    >>> filename = "fun.ref"
    >>> get_iode_file_type(filename)
    <IodeFileType.FILE_REF: 29>
    """
    if not filepath:
        return IodeFileType.FILE_ANY

    p_filepath = Path(filepath)

    if p_filepath.is_dir():
        return IodeFileType.DIRECTORY

    if not p_filepath.suffix:
        return IodeFileType.FILE_ANY

    ext = p_filepath.suffix

    for i, file_type in enumerate(IODE_FILE_TYPES):
        if ext in file_type.extensions:
            return IodeFileType(i)

    return IodeFileType.FILE_ANY
