import os
import urllib.request, urllib.error, urllib.parse
import time
import copy

from . import utils

def download(url, dest,headers=None, startByte=0, endByte=None, timeout=4, attempt_resume=True, shared_var=None, thread_shared_cmds=None, logger=None, retries=3):
    "The basic download function that runs at each thread."
    logger = logger or utils.DummyLogger()
    if not headers:
        download_headers = {}
    else:
        download_headers = copy.deepcopy(headers)
    if endByte:
        if attempt_resume and os.path.exists(dest):
            partial_size = os.path.getsize(dest)
            logger.info('Found partial chunks of file {} (found {} bytes).'.format(dest, partial_size))
            startByte += partial_size
            if shared_var:
                shared_var.value += partial_size

            if startByte-1 == endByte:
                logger.info('This chunk is 100% downloaded ({}).'.format(startByte))
                return
            elif startByte > endByte:
                logger.warning('startByte ({}) > endByte ({}). Chunks are corrupted. Overriding old chunks.'.format(startByte, endByte))
                attempt_resume = False
                if shared_var:
                    shared_var.value -= partial_size
        logger.debug("Downloading bytes=%d-%d" % (startByte, endByte))
        download_headers['headers']['Range'] = 'bytes=%d-%d' % (startByte, endByte)
    else:
        logger.info('without endByte, attempt_resume cannot be supported')
        attempt_resume = False
    
    logger.info("Downloading '{}' to '{}'...".format(url, dest))
    req = urllib.request.Request(url, **download_headers)
    try:
        urlObj = urllib.request.urlopen(req, timeout=timeout)
    except urllib.error.HTTPError as e:
        if e.code == 416:
            '''
            HTTP 416 Error: Requested Range Not Satisfiable. Happens when we ask
            for a range that is not available on the server. It will happen when
            the server will try to send us a .html page that means something like
            "you opened too many connections to our server". If this happens, we
            will wait for the other threads to finish their connections and try again.
            '''
            
            if retries > 0:
                logger.warning("Thread didn't got the file it was expecting. Retrying ({} times left)...".format(retries-1))
                time.sleep(5)
                return download(url, dest, startByte, endByte, download_headers, timeout, attempt_resume, shared_var, thread_shared_cmds, logger, retries-1)
            else:
                raise
        else:
            raise

    if not attempt_resume and os.path.exists(dest):
        logger.info('Deleting old {}...'.format(dest))
        os.unlink(dest)

    with open(dest, 'ab') as f:
        if endByte:
            filesize = endByte-startByte
        else:
            try:
                meta = urlObj.info()
                filesize = int(urlObj.headers["Content-Length"])
                logger.info("Content-Length is {}.".format(filesize))
            except (IndexError, KeyError, TypeError):
                logger.warning("Server did not send Content-Length.")
        
        filesize_dl = 0  # total downloaded size
        limitspeed_timestamp = time.time()
        limitspeed_filesize = 0
        block_sz = 8192
        while True:
            if thread_shared_cmds:
                if 'stop' in thread_shared_cmds:
                    logger.info('stop command received. Stopping.')
                    raise CanceledException()
                if 'pause' in thread_shared_cmds:
                    time.sleep(0.2)
                    continue
                if 'limit' in thread_shared_cmds:
                    now = time.time()
                    time_passed = now - limitspeed_timestamp
                    if time_passed > 0.1:  # we only observe the limit after 100ms
                        # if we passed the limit, we should
                        if (filesize_dl-limitspeed_filesize)/time_passed >= thread_shared_cmds['limit']:
                            time_to_sleep = (filesize_dl-limitspeed_filesize) / thread_shared_cmds['limit']
                            logger.debug('Thread has downloaded {} in {}. Limit is {}/s. Slowing down...'.format(utils.sizeof_human(filesize_dl-limitspeed_filesize), utils.time_human(time_passed, fmt_short=True, show_ms=True), utils.sizeof_human(thread_shared_cmds['limit'])))
                            time.sleep(time_to_sleep)
                            continue
                        else:
                            limitspeed_timestamp = now
                            limitspeed_filesize = filesize_dl
                
            try:
                buff = urlObj.read(block_sz)
            except Exception as e:
                logger.error(str(e))
                if shared_var:
                    shared_var.value -= filesize_dl
                raise
                
            if not buff:
                break

            filesize_dl += len(buff)
            if shared_var:
                shared_var.value += len(buff)
            f.write(buff)
            
    urlObj.close()