import pandas
import numpy
import re
from scipy import stats

from collections import Counter


def take_last(s: pandas.Series):
	return s.iloc[-1]


def take_first(s: pandas.Series):
	return s.iloc[0]


def count_unique(x: pandas.Series):
	return len(x.unique())


def bin_variable(s: pandas.Series, min_value, max_value, width, how, default=None, lower=None, upper=None, errors=None):
	"""
	bin pandas Series
	:param s: pandas Series
	:param min_value: minimum bining value
	:param max_value: maximum binning value
	:param width: bin width
	:param how: function to use for binning. Use numpy.ceil, numpy.floor
	:param default: default value to replace nans
	:param lower: value to assign to values smaller than min_value
	:param upper: value to assign to values larger  than max_value
	:param errors: error values in the Series to be treated a if they where nans
	:return: binned series s_binned
	"""
	s_binned = s.copy()
	nan_values = numpy.array(s_binned.isna()) | numpy.array((s_binned == errors))

	conditions = numpy.array([True for _ in range(len(s_binned))])

	if lower is not None:
		s_binned.loc[s_binned < min_value] = lower
		conditions = conditions & (s_binned != lower) & (min_value <= s_binned)

	if upper is not None:
		s_binned.loc[s_binned > max_value] = upper
		conditions = conditions & (s_binned != upper) & (s_binned <= max_value)

	s_binned.loc[conditions] = list(numpy.clip(how(s_binned.loc[conditions]/width) * width, min_value, max_value))
	s_binned = numpy.round(s_binned, 8)
	if default is not None:
		s_binned.loc[nan_values] = default

	return s_binned


def detect_outliers(df, n, features):
	"""
	Takes a dataframe df of features and returns a list of the indices
	corresponding to the observations containing more than n outliers according
	to the Tukey method.
	"""
	outlier_indices = []

	# iterate over features(columns)
	for col in features:
		# 1st quartile (25%)
		q1 = numpy.percentile(df[col].dropna(), 25)
		# 3rd quartile (75%)
		q3 = numpy.percentile(df[col].dropna(), 75)
		# Interquartile range (iqr)
		iqr = q3 - q1

		# outlier step
		outlier_step = 1.5 * iqr

		# Determine a list of indices of outliers for feature col
		outlier_list_col = df[(df[col] < q1 - outlier_step) | (df[col] > q3 + outlier_step)].index

		# append the found outlier indices for col to the list of outlier indices
		outlier_indices.extend(outlier_list_col)

	# select observations containing more than 2 outliers
	outlier_indices = Counter(outlier_indices)
	multiple_outliers = list(k for k, v in outlier_indices.items() if v > n)

	return multiple_outliers


def multiple_replace(s: pandas.Series, replacing_dict: dict, default=None):
	"""
	replaces multiple values at once
	:param s: pandas Series to act on
	:param replacing_dict: dictionary with replacements. Has to be in the form: {'<replacing_value_1>' : [<list_of_values_to_replace], '<replacing_value_2>' : [<list_of_values_to_replace] ...}
	:param default: default value. used to fill values not in replacing_value_n
	:return:
	"""
	for key, value in replacing_dict.items():
		s = s.replace(value, key)
	if default is not None:
		s = s.map(dict(zip(replacing_dict.keys(), replacing_dict.keys()))).fillna(default)
	return s


def find_regex(regex, value):
	if re.search(regex, value) is not None:
		return True
	else:
		return False


def fill_empty_with_distr(s: pandas.Series, values_to_fill: list, values_to_avoid: list=None):
	"""
	:param s: pandas Series to be filled
	:param values_to_fill: value to fill
	:param values_to_avoid: values not valid for filling
	:return:
	"""

	compl_values_to_avoid = [numpy.nan]
	if values_to_avoid is not None:
		compl_values_to_avoid += values_to_avoid

	filling_index = s.index[s.isin(values_to_fill)]

	distrib_data = s.value_counts()
	distrib_data = distrib_data[~distrib_data.index.isin(values_to_fill + compl_values_to_avoid)]
	distrib_data_dict = {k: v for k, v in enumerate(distrib_data.index)}

	xk, pk = list(distrib_data_dict.keys()), distrib_data.values/distrib_data.values.sum()
	distrib = stats.rv_discrete(values=(xk, pk))

	filled_s = s.copy()
	filled_s[filling_index] = [distrib_data_dict[distrib.rvs()] for _ in filling_index]

	return filled_s
