import requests
import io
import threading
import time
import tempfile
import json
import glob
import sys

import gdown
import pandas as pnd



class SimpleLoadingWheel():
    # a simple endless loading wheel
    
    msg = ''
    wheel_figures = ['|', '/', '-', '\\']
    wheel_cnt = 0
    
    def __init__(self, msg='Please wait... '):
        self.msg = msg
    
    def proceed(self):
        print(f"{self.msg}{self.wheel_figures[self.wheel_cnt]}", file=sys.stderr, end='\r')
        self.wheel_cnt += 1
        if self.wheel_cnt == 4: self.wheel_cnt = 0
        time.sleep(0.5)
        
    def clear(self):
        clear_str = ''.join([' ' for i in range(len(self.msg)+1)])
        print(f'{clear_str}', file=sys.stderr, end='\r')
        wheel_cnt = 0
    


def get_dbuni(logger):
    
    
    sheet_id = "1dXJBIFjCghrdvQtxEOYlVNWAQU4mK-nqLWyDQeUZqek"
    #sheet_id = "15fIBewG1B1jIbg1_9pMnyPJL7LWCLMD8s_vlf3ZUno0"  # alternative
    url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
    response = requests.get(url)  # download the requested file
    if response.status_code == 200:
        excel_data = io.BytesIO(response.content)   # load into memory
        exceldb = pnd.ExcelFile(excel_data)
    else:
        logger.error(f"Error during download. Retry! If persists, please contact the developer.")
        return 1
    
    
    # check table presence
    sheet_names = exceldb.sheet_names
    for i in ['M', 'R', 'T', 'A', 'G', 'B', 'curators']: 
        if i not in sheet_names:
            logger.error(f"Sheet '{i}' is missing!")
            return 1
        
        
    # load the tables
    dbuni = {}
    dbuni['M'] = exceldb.parse('M')
    dbuni['R'] = exceldb.parse('R')
    dbuni['T'] = exceldb.parse('T')
    dbuni['A'] = exceldb.parse('A')
    dbuni['G'] = exceldb.parse('G')
    dbuni['B'] = exceldb.parse('B')
    dbuni['E'] = exceldb.parse('E')
    dbuni['curators'] = exceldb.parse('curators')
    
    
    # check table headers
    headers = {}
    headers['M'] = ['pure_mid', 'formula', 'charge', 'kc', 'name', 'inchikey', 'curator', 'notes']
    headers['R'] = ['rid', 'rstring', 'kr', 'gpr_manual', 'name', 'curator', 'notes']
    headers['T'] = ['rid', 'rstring', 'kr', 'gpr_manual', 'name', 'curator', 'notes']
    headers['A'] = ['pure_mid', 'type', 'curator', 'notes']
    headers['G'] = ['gr_id', 'rids', 'curator', 'notes']
    headers['B'] = ['pure_mid', 'class', 'evaluation', 'bs_groups', 'curator', 'notes']
    headers['E'] = ['map_id', 'location', 'curator', 'notes']
    headers['curators'] = ['username', 'first_name', 'last_name', 'role', 'mail']
    for i in dbuni.keys(): 
        if set(dbuni[i].columns) != set(headers[i]):
            logger.error(f"Sheet '{i}' is missing the columns {set(headers[i]) - set(dbuni[i].columns)}.")
            return 1
        
    return dbuni



def get_dbexp(logger):
    
    
    sheet_id = "1qGbIIipHJgYQjk3M0xDWKvnTkeInPoTeH9unDQkZPwg"
    #sheet_id = "1qxTRf30SeT9WJFYxWm2ChCxkTR0sTn7BbDOFhUuUQIE"   # alternative
    url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
    response = requests.get(url)  # download the requested file
    if response.status_code == 200:
        excel_data = io.BytesIO(response.content)   # load into memory
        exceldb = pnd.ExcelFile(excel_data)
    else:
        logger.error(f"Error during download. Retry! If persists, please contact the developer.")
        return 1
    
    
    # check table presence
    sheet_names = exceldb.sheet_names
    for i in ['media', 'PM1', 'PM2A', 'PM3B', 'PM4A', 'MWF', 'DNA', 'RNA', 'PROTS', 'LIPIDS_PL', 'LIPIDS_FA', 'curators']: 
        if i not in sheet_names:
            logger.error(f"Sheet '{i}' is missing!")
            return 1
        
        
    # load the tables
    dbexp = {}
    dbexp['media'] = exceldb.parse('media')
    dbexp['PM1'] = exceldb.parse('PM1')
    dbexp['PM2A'] = exceldb.parse('PM2A')
    dbexp['PM3B'] = exceldb.parse('PM3B')
    dbexp['PM4A'] = exceldb.parse('PM4A')
    dbexp['MWF'] = exceldb.parse('MWF')
    dbexp['DNA'] = exceldb.parse('DNA')
    dbexp['RNA'] = exceldb.parse('RNA')
    dbexp['PROTS'] = exceldb.parse('PROTS')
    dbexp['LIPIDS_PL'] = exceldb.parse('LIPIDS_PL')
    dbexp['LIPIDS_FA'] = exceldb.parse('LIPIDS_FA')
    dbexp['curators'] = exceldb.parse('curators')
    
    
    # format tables (media):
    # assign substrates as index
    dbexp['media'].index = dbexp['media'].iloc[:, 1]
    # remove first 2 useless column (empty & substrates)
    dbexp['media'] = dbexp['media'].iloc[:, 2:]
    
    
    # format tables (Biolog(R)):
    for sheet in ['PM1', 'PM2A', 'PM3B', 'PM4A']:
        # assign wells as index
        dbexp[sheet].index = dbexp[sheet].iloc[:, 2]
        # remove first 3 useless columns
        dbexp[sheet] = dbexp[sheet].iloc[:, 3:]
        
        
    # format tables (biomass):
    dbexp['MWF'].index = dbexp['MWF'].iloc[:, 0]   # assign index
    dbexp['MWF'] = dbexp['MWF'].iloc[:, 1:]        # remove meaningless columns
    #
    dbexp['DNA'].index = dbexp['DNA'].iloc[:, 0]   # assign index
    dbexp['DNA'] = dbexp['DNA'].iloc[:, 1:]        # remove meaningless columns
    #
    dbexp['RNA'].index = dbexp['RNA'].iloc[:, 0]   # assign index
    dbexp['RNA'] = dbexp['RNA'].iloc[:, 1:]        # remove meaningless columns
    # 
    dbexp['PROTS'].index = dbexp['PROTS'].iloc[:, 1]   # assign index
    dbexp['PROTS'] = dbexp['PROTS'].iloc[:, 2:]        # remove meaningless columns
    #
    dbexp['LIPIDS_PL'].index = dbexp['LIPIDS_PL'].iloc[:, 1]   # assign index
    dbexp['LIPIDS_PL'] = dbexp['LIPIDS_PL'].iloc[:, 2:]        # remove meaningless columns
    #
    dbexp['LIPIDS_FA'].index = dbexp['LIPIDS_FA'].iloc[:, 1]   # assign index
    dbexp['LIPIDS_FA'] = dbexp['LIPIDS_FA'].iloc[:, 2:]        # remove meaningless columns
    
    
    return dbexp



def get_eschermap(logger, dbuni, map_id):
    
    
    # retrieve catalog from database:
    dict_escher_maps = {}
    E = dbuni['E'].set_index('map_id', drop=True, verify_integrity=True)
    for index, row in E.iterrows():
        dict_escher_maps[index] = row['location']
    
    
    # return None if not requested
    if map_id == None or map_id == '-': 
        return None
    
    
    # check the esistance of the map  
    if map_id not in dict_escher_maps.keys():
        logger.warning(f"No online folder is tracked for '{map_id}' yet. Please edit our database (sheet 'E') to track '{map_id}'.")
        return None    
    
    
    # object to be returned
    logger.info(f"Downloading last version of map '{map_id}'...")
    lastmap = dict()  
    
    
    # the temporary folder (/tmp/on-the-fly-code) will be deleted once exiting the with statement
    with tempfile.TemporaryDirectory() as tmp_dir:
        
        
        # get available versions without downloading:
        folder_id = dict_escher_maps[map_id]
        folder_url = f"https://drive.google.com/drive/folders/{folder_id}?usp=sharing"
        contents = gdown.download_folder(folder_url, output=tmp_dir, quiet=True, skip_download=True)

        # check empty folder
        if len(contents) == 0:
            logger.warning(f"Online folder for '{map_id}' seems empty. Please draw and upload '{map_id}-v1.json'.")
            return None
        
        # check crowded folder
        if len(contents) > 40:
            logger.warning(f"Online folder for '{map_id}' contains >40 versions. An error will be raised at 50 versions. Please free space by moving older versions into 'older/' now.")
        
        # create dict of available files:
        files = {i.path: i.id for i in contents}
        
        # check file name consistency
        map_ids = set([i.rsplit('-v',1)[0] for i in files.keys()])
        if len(map_ids) != 1:
            logger.error(f"Several different map names in online folder for '{map_id}'.")
            return 1
        
        # check concordance to mother folder
        if list(map_ids)[0] != map_id:
            logger.error(f"Online folder for '{map_id}' contains versions for another map.")
            return 1
        
        # get the last available version (natural order)
        last_version = max([int(i.rsplit('-v',1)[-1].replace('.json', '')) for i in files.keys()])
        last_file_name = list(files.keys())[0].rsplit('-v',1)[0] + f"-v{last_version}.json"
        last_file_id = files[last_file_name]
        lastmap['filename'] = last_file_name
        
        # download last version:
        try: pathfile_tmpfolder = gdown.download(id=last_file_id, output=f"{tmp_dir}/{last_file_name}", quiet=True)
        except: 
            logger.error(f"Downloading of last-version for '{map_id}'. Retry. If persists, please contact the developer.")
            return 1
        
        # load json
        with open(pathfile_tmpfolder, 'r') as file:
            json_data = json.load(file)
            lastmap['json'] = json_data

        
    return lastmap
        
    
    
def get_databases(logger): 
    

    # define a function to fill the dict
    def run_with_result(func, logger, results_dict, key):
        result = func(logger)
        results_dict[key] = result
        
    
    # start threads
    results_dict = dict()
    t1 = threading.Thread(target=run_with_result, args=(
        get_dbuni, logger, results_dict, 'dbuni'))
    t2 = threading.Thread(target=run_with_result, args=(
        get_dbexp, logger, results_dict, 'dbexp'))

    
    # wait for the longest download:
    t1.start()
    t2.start()
    slw = SimpleLoadingWheel(msg="Please wait... ")
    while t1.is_alive() or t2.is_alive():
        slw.proceed()
    slw.clear()
    
    
    # check if errors where raised:
    if type(results_dict['dbuni'])==int:
        return 1
    if type(results_dict['dbexp'])==int:
        return 1
    
    
    return (results_dict['dbuni'], results_dict['dbexp'])



def eggnog_format_expansion(logger, eggnog):
    
    
    # linux/macos usually perform argument axpansion befor passing the parameter to python.
    if type(eggnog) == list: # already expanded by the terminal
        if len(eggnog)==1 and '*' in eggnog[0]:
            original_eggnog = eggnog[0]
            eggnog = glob.glob(eggnog[0])  # glob will append only existing files to the list
            if eggnog == []:
                logger.info(f"No file matching '{original_eggnog}'.")
    
    
    elif type(eggnog) == str:  # in the terminal, it can be specified by using single quotes
        original_eggnog = False
        if eggnog != '-': # user wanted to specify something
            original_eggnog = eggnog
        eggnog = glob.glob(eggnog)  # glob will append only existing files to the list
        if original_eggnog and eggnog == []:
            logger.info(f"No file matching '{original_eggnog}'.")
            
            
    if eggnog == [] or eggnog == ['-']:
        eggnog = '-'   # return always a list except for this case
        
        
    return eggnog
    


def check_taxon(logger, taxon, idcollection_dict):
    
    
    # verify presence of needed assets
    if 'ko_to_taxa' not in idcollection_dict.keys():
        logger.error(f"Asset 'ko_to_taxa' not found in 'gsrap.maps'. Please update 'gsrap.maps' with 'gsrap getmaps'.")
        return 1
    
    
    # extract level and name
    try: level, name = taxon.split(':')
    except: 
        logger.error(f"Provided --taxon is not well formatted: '{taxon}'.")
        return 1
    
    
    # compute available levels and check
    avail_levels = set(['kingdom', 'phylum'])            
    if level not in avail_levels:
        logger.error(f"Provided level is not acceptable: '{level}' (see --taxon). Acceptable levels are {avail_levels}.")
        return 1
    
    
    # compute available taxa at input level
    avail_taxa_at_level = set()
    ko_to_taxa = idcollection_dict['ko_to_taxa']
    for ko in ko_to_taxa.keys():
        for taxon_name in ko_to_taxa[ko][level]:
            avail_taxa_at_level.add(taxon_name)
    if name not in avail_taxa_at_level:
        logger.error(f"Provided taxon name is not acceptable: '{name}' (see --taxon). Acceptable taxon names for level '{level}' are {avail_taxa_at_level}.")
        return 1
    
    
    return 0
