from datetime import date, datetime
from decimal import Decimal as PyDecimal
from types import NoneType
import numpy as np
import pandas as pd
import datetime as dt
from pandas import DataFrame

#--------------------------------------------------
# Python types
#--------------------------------------------------

def column_to_concept_name(df:DataFrame, col) -> str:
    """ Infer the type of the column in the dataframe and return the corresponding concept name. """
    _type = df[col].dtype

    if _type.name == "object" and len(df[col]) > 0:
        # pandas often infers object dtype as a fallback, so if there's some data, we can
        # try to infer based on the first data point
        return pytype_to_concept_name.get(type(df[col][0]), "Any")
    elif pd.api.types.is_datetime64_any_dtype(_type):
        return "DateTime"
    elif pd.api.types.is_object_dtype(_type) and is_date_column(df[col]):
        return "Date"
    elif pd.api.types.is_string_dtype(_type):
        return "String"
    elif pd.api.types.is_float_dtype(_type):
        # floats in dataframes should be straight mapped to Float; note that float literals
        # map to Number(38,14) instead (using pytype_to_concept_name) because the typer can
        # then figure out if they should be number or float based on usage.
        return "Float"
    else:
        return pytype_to_concept_name.get(_type, "Any")

def is_date_column(col) -> bool:
    """ Check whether the dataframe column contains date or datetime values. """
    sample = col.dropna()
    if sample.empty:
        return False
    sample_value = sample.iloc[0]
    return isinstance(sample_value, dt.date) and not isinstance(sample_value, dt.datetime)

pytype_to_concept_name: dict[object, str] = {
    NoneType:  "Any",
    int: "Integer", float: "Number(38,14)", str: "String", bool: "Boolean",
    date: "Date", datetime: "DateTime", PyDecimal: "Number(38,14)",

    # NumPy dtypes
    **{np.dtype(k): "Integer" for k in ("int8","int16","int32","int64","uint8","uint16","uint32","uint64")},
    **{np.dtype(k): "Number(38,14)" for k in ("float32","float64")},
    np.dtype("bool"): "Boolean",
    np.dtype("object"): "String",
    np.dtype("datetime64[ns]"): "DateTime",
    np.dtype("datetime64[ms]"): "DateTime",
    np.dtype("datetime64[s]"): "DateTime",

    # Pandas extension dtypes
    **{t(): "Integer" for t in (pd.Int8Dtype, pd.Int16Dtype, pd.Int32Dtype, pd.Int64Dtype,
                        pd.UInt8Dtype, pd.UInt16Dtype, pd.UInt32Dtype, pd.UInt64Dtype)},
    **{t(): "Number(38,14)" for t in (pd.Float32Dtype, pd.Float64Dtype)},
    # 2 different na_values on the string d-type
    pd.Series([""]).dtype: "String", # np.nan
    pd.StringDtype(): "String", # pd.NA
    pd.BooleanDtype(): "Boolean",
    "int": "Integer", "float": "Number(38,14)", "str": "String", "bool": "Boolean",
    "date": "Date", "datetime": "DateTime", "decimal": "Number(38,14)",
    "datetime.date": "Date", "datetime.datetime": "DateTime"
}
