from __future__ import print_function

import sys
import os
import shutil
import threading
import errno
import signal
import logging
import collections
import warnings

if getattr(sys.version_info, 'major') == 2:
    import urlllib2
elif getattr(sys.version_info, 'major') == 3:
    import urllib.requests as urllib2
else:
    raise ImportError("Unable to detect the version of python you are using!")

import numpy as np
import pandas as pd

from StringIO import StringIO
from .extras import cprint

class DelayedKeyboardInterrupt(object):
    """Forces an operation to complete when this class function is called before an 
    operation. Handles Ctrl+C, Ctrl+Z and Ctrl+\ keyboard interrupts.
    
    Example
    -------
    
    with DelayedKeyboardInterrupt():
        for i in xrange(10):
            print(i)
            systime.sleep(1)
            
    >>> 0
    >>> 1
    >>> ^C[Warning] Please wait until the current operation has finish
    >>> ^Z[Warning] Please wait until the current operation has finish
    >>> ^\[Warning] Please wait until the current operation has finish
    >>> 2
    ...
    >>> 10
    >>> KeyboardInterrupt
    
    Reference
    ---------
    https://stackoverflow.com/a/21919644/8765762
    """    
    
    def __enter__(self):
        self.signal_received = False
        self.ctrlc_handler = signal.signal(signal.SIGINT, self.handler)
        self.ctrlz_handler = signal.signal(signal.SIGTSTP, self.handler)
        self.ctrlslash_handler = signal.signal(signal.SIGQUIT, self.handler)
        
    def handler(self, sig, frame):
        self.signal_received = (sig, frame)
        logging.debug('SIGINT received. Delaying KeyboardInterrupt.')
        cprint("[Warning] Please wait until the current operation has finished", type='warning')
        
    def __exit__(self, type, value, traceback):
        signal.signal(signal.SIGINT, self.ctrlc_handler)
        if self.signal_received:
            self.ctrlc_handler(*self.signal_received)
            
        signal.signal(signal.SIGTSTP, self.ctrlz_handler)
        if self.signal_received:
            self.ctrlz_handler(*self.signal_received)
            
        signal.signal(signal.SIGQUIT, self.ctrlslash_handler)
        if self.signal_received:
            self.ctrlslash_handler(*self.signal_received)

class unbuffered(object):
    """Forces the print statements to be written without the need for flushing. Useful for
    sending print statements to text files when running python on a cluster.
    
    Usage
    -----
    import sys
    import Gilly_Utilities as gu

    sys.stdout = gu.unbuffered(sys.stdout)
    
    Reference
    ---------
    https://stackoverflow.com/a/107717/8765762
    """
    
    def __init__(self, stream):
        self.stream = stream
    def write(self, data):
        self.stream.write(data)
        self.stream.flush()
    def writelines(self, datas):
        self.stream.writelines(datas)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)

class suppress_output(object):
    """Suppresses the output of the sys.stdout.write and so no output
    will be visible on the console.
    
    To use this class place in code like so,
    
    with gu.suppress_output():
        ...
        
    ...
    
    References
    ----------
    https://stackoverflow.com/a/8391735/8765762
    https://stackoverflow.com/a/45669280/8765762
    """
    
    def __enter__(self):
        self._original_stdout = sys.stdout
        sys.stdout = open(os.devnull, 'w')
    def __exit__(self, type, value, traceback):
        sys.stdout.close()
        sys.stdout = self._original_stdout

def isarray(arg):
    """Checks whether arg is array_like (e.g. tuple, list, np.ndarray, etc.)"""
    
    #return type(arg) is not str and isinstance(arg, collections.Sequence)
    
    dtypes = [list, tuple, np.ndarray, pd.DataFrame]
    return np.any([isinstance(arg, dtype) for dtype in dtypes])

def isnumeric(arg, warn=True):
    """
    Test element-wise for numerics (e.g. 2.4, 2).

    The result is returned as a boolean array.
    """
    
    # Force arg to be a numpy array
    arg = np.atleast_1d(arg)
    
    # Check for finitness
    try:
        return np.isfinite(arg)
    except TypeError:
        if warn is True:
            warnings.warn("[gu.isnumeric] invalid data type found in arg. Returning False", UserWarning, stacklevel=2)
        return np.array(False)

def isnone(arg, keepshape=True):
    """
    Test element-wise for nones.

    The result is returned as a boolean array.
    """

    # Force arg to be a numpy array
    arg = np.atleast_1d(arg)
    nones_full = np.zeros(arg.shape, dtype=bool)
    
    # Create index array of arg to reorganise after we need to flatten arg
    index = np.arange(arg.size).reshape(arg.shape)
    
    # Flatten arg array
    arg = np.hstack(arg.flat)
    index_flat = np.hstack(index)
    
    # Check for nones
    nones = np.array([val is None for val in arg], dtype=bool)
    
    if keepshape is True:
        
        # Return a restructured array
        for ind, val in zip(index_flat, nones):
            nones_full[np.where(index == ind)] = val
        
        return nones_full
    
    else:
        return nones


"""
Works out the length of string arrays element-wise

Reference
---------
https://stackoverflow.com/a/44587916/8765762
"""
arrlen = np.vectorize(len)
    
def ensure_dir(dirname, dir_or_file='dir'):
    """Ensure that a named directory exists; if it does not, attempt to create it.
    
    Sources
    -------
    http://stackoverflow.com/a/21349806
    """

    # Copy dirname to return to user
    dirname_return = dirname

    try:
        os.makedirs(dirname) if dir_or_file == 'dir' else os.makedirs(os.path.dirname(dirname))
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise

    return dirname

def del_folder(folder, sub=False): 
    """Deletes a folder. Option sub to make recursive"""

    for the_file in os.listdir(folder):
        file_path = os.path.join(folder, the_file)
        try:
            if os.path.isfile(file_path):
                os.unlink(file_path)
            elif os.path.isdir(file_path) and sub == True: 
                shutil.rmtree(file_path)
        except Exception as e:
            print(e)
            
def Print_Output(delay_time, t_start, it_tot, it):
  t = threading.Timer(delay_time, Print_Output(delay_time, t_start, it_tot, it)).start()
  os.system('cls' if os.name=='nt' else 'clear')
  print("Time Left (s): ", "%.0f" % ((it_tot-it)*((time.time()-t_start)/it)))
  print("Events Left: ", it_tot-it)

def File_Checker(files, dates, date_range, file_size_min=0):
    """Checks the availability of files
    
    Parameters
    ----------
    files : numpy array
        A list of file locations to be tested
    dates : numpy object array
        An array of datetimes corresponding to each file location in files.
    date_range : nump object array
        An array over the list of dates you want to test.
    file_size_min : int, optional
        The minimum file size (in bytes) required to be valid. This is useful for files that
        exist but don't contain any data.

    Returns
    -------
    File_Availability : list
        A boolean list with 1 for file exists and is above the minimum file size threshold
        and zero for either file doesn't exist or file is below or equal to minimum file
        threshold.        
    """
    
    File_Availability = []
    for i in xrange(date_range.shape[0]):
        if np.sum(dates == date_range[i]) == 1:            
            if os.stat(files[dates == date_range[i]][0]).st_size > file_size_min:
                File_Availability.append(1)
            else:
                File_Availability.append(2)
        else:
            File_Availability.append(0)
            
    return np.array(File_Availability, dtype=float)

def File_Aligner_NoSize(files, dates, date_range):
    """Checks the availability of files
    
    Parameters
    ----------
    files : numpy array
        A list of file locations to be tested
    dates : numpy object array
        An array of datetimes corresponding to each file location in files.
    date_range : nump object array
        An array over the list of dates you want to test.
    file_size_min : int, optional
        The minimum file size (in bytes) required to be valid. This is useful for files that
        exist but don't contain any data.

    Returns
    -------
    File_Availability : list
        A boolean list with 1 for file exists and is above the minimum file size threshold
        and zero for either file doesn't exist or file is below or equal to minimum file
        threshold.        
    """
    
    File_Date = np.zeros(date_range.shape[0], dtype=object)
    File_Loc = np.zeros(date_range.shape[0], dtype='S256')
    for i in xrange(date_range.shape[0]):
        if np.sum(dates == date_range[i]) >= 1:            
            File_Date[i] = date_range[i]
            File_Loc[i] = files[dates == date_range[i]][0]
            
    return np.array(File_Loc, dtype='S256'), np.array(File_Date, dtype=object)
    
def File_Aligner(files, dates, date_range, file_size_min=0):
    """Checks the availability of files
    
    Parameters
    ----------
    files : numpy array
        A list of file locations to be tested
    dates : numpy object array
        An array of datetimes corresponding to each file location in files.
    date_range : nump object array
        An array over the list of dates you want to test.
    file_size_min : int, optional
        The minimum file size (in bytes) required to be valid. This is useful for files that
        exist but don't contain any data.

    Returns
    -------
    File_Availability : list
        A boolean list with 1 for file exists and is above the minimum file size threshold
        and zero for either file doesn't exist or file is below or equal to minimum file
        threshold.        
    """
    
    File_Date = np.zeros(date_range.shape[0], dtype=object)
    File_Loc = np.zeros(date_range.shape[0], dtype='S256')
    for i in xrange(date_range.shape[0]):
        if np.sum(dates == date_range[i]) == 1:    
            if os.stat(files[dates == date_range[i]][0]).st_size > file_size_min:
                File_Date[i] = date_range[i]
                File_Loc[i] = files[dates == date_range[i]][0]
            else:
                File_Date[i] = 2
                File_Loc[i] = 2
            
    return np.array(File_Loc, dtype='S256'), np.array(File_Date, dtype=object)
    
def urllib_authentication(url, username, password, library='urllib2'):
    """
    When using urllib2 on websites which require authentication. This
    function can be used to access the website
    """
    
    p = urllib2.HTTPPasswordMgrWithDefaultRealm()

    p.add_password(None, url, username, password)

    handler = urllib2.HTTPBasicAuthHandler(p)
    opener = urllib2.build_opener(handler)
    urllib2.install_opener(opener)
    
    return