# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

"""
    net.py
    ~~~~~~
    
    Functions related to the network
"""

import os
from os.path import join, basename, dirname, exists, isabs
import urllib
import urllib2
from urlparse import urlparse
import logging
import json
from datetime import datetime
from collections import namedtuple
import base64
from hashlib import md5

from pypm.common import console, activestate
from pypm.common.util import BareDateTime, get_user_agent

LOG = logging.getLogger(__name__)

    
def download_file(url, to_directory, urlgetter_kargs=None):
    """Facade to download the ``url`` to ``to_directory`` using a progress bar.

    - urlgetter_kargs: kwargs to URLGetter.get(...) method

    Return the download file path.
    """
    from pypm.common.textui import DownloadProgressBar
    
    assert '\n' not in url
    canonical_url = _http_url_remove_anchor(url)
    
    if not urlgetter_kargs:
        urlgetter_kargs = {}
    
    # basename apparently works on URLs too
    # http://stackoverflow.com/questions/1112545/
    target_file = join(to_directory, basename(canonical_url))
    
    LOG.debug('Downloading %s', canonical_url)
    dlprogress = DownloadProgressBar(canonical_url)
    dlprogress.get(target_file, **urlgetter_kargs)
    return target_file


#
# URLGetter
#

class URLGetter(object):
    """A fine-grained URL getter. An improved `urllib.urlretrieve`"""

    class URLProperties(object):
        """An URL props file is a JSON data with two keys:

          - headers: the HTTP headers dictionary
          - custom: any custom data stored as a dictionary

        An URL property file is stored along with the downloaded file so as to
        retrieve any download related data in future. URLGetter.get(...,
        save_properties=True) will create them.
        """

        Data = namedtuple('URLProperties_Data', 'headers custom')

        def __init__(self, target_file):
            """
            - target_file: Local file where the URL is/would be downloaded
            """
            self.target_file = target_file

        @property
        def urlprops_filename(self):
            return _format_basename(self.target_file, '.{basename}.urlprops')

        def load(self):
            """Load URL properties, as saved by the `save` method above.

            Returned value is a namedtuple containing `headers` and `custom`
            dicts as passed to the `save()` method.

            Return None, if no target file or urlprops file is found.
            """
            if exists(self.target_file):
                fn = self.urlprops_filename
                if exists(fn):
                    LOG.debug('loading urlprops: %s', fn)
                    with open(fn) as f:
                        data = json.load(f)
                        assert tuple(sorted(data.keys())) == (u'custom', u'headers'), \
                            'invalid keys: %s' % data.keys()
                        return self.Data(data['headers'], data['custom'])
                    
        def save(self, headers_dict, custom_dict):
            """Save headers_dict and custom_dict"""
            assert exists(self.target_file)
            fn = self.urlprops_filename
            LOG.debug('writing urlprops: %s', fn)
            data = dict(headers=headers_dict, custom=custom_dict)
            json.dumps(data) # catch json errors early (before writing to file)
            with open(fn, 'w') as f:
                json.dump(data, f)


    def __init__(self, url):
        """
        - url: URL to download
        """
        self.url = url

    def get(self, target_file, info=None,
            use_cache=False, save_properties=False,
            auth=None):
        """Download the URL and save it to `target_file`

        Appropriate hook functions are automatically invoked

        - info: message to print (LOG.info) if transfer actually happens

        - use_cache: if True, download will not actually happen if `target_file`
          is already available *and* its ETag is not changed in the server. For
          this to work effectively, the previous ETag must be made available in
          the properties file that is created when `save_properties` flag is set
          to True in the *past* download call.

        - save_properties: if True, a file named .$target_file.urlprops
          containing request headers and other metadata is saved along side the
          target_file. (This is required for `use_cache` in future downloads)
          
        - auth: (username, password) -- optional http basic auth data
        """
        assert target_file

        urlprops = self.URLProperties(target_file)
        props = urlprops.load()
        if props:
            # write back the new value for last_attempt_utc *now* so we don't
            # have to deal with when an exception arises later.
            props.custom['last_attempt_utc'] = BareDateTime.to_string(datetime.utcnow())
            urlprops.save(props.headers, props.custom)

        if use_cache and props:
            req = urllib2.Request(self.url, headers={
                    'If-None-Match': props.headers['etag']})
        else:
            req = urllib2.Request(self.url)
            
        if auth:
            username, password = auth
            req.add_header('Authorization', _create_http_basicauth_header(
                username, password
            ))
            
        # Set User-Agent
        # XXX: urllib2.py does not expose its default user-agent string; so
        # so we copy-paste that code here (from urllib2.OpenerDirector)
        urllib2_user_agent = "Python-urllib/%s" % urllib2.__version__
        pypm_user_agent = get_user_agent(urllib2_user_agent)
        req.add_header('User-Agent', pypm_user_agent)
        
        u = urllib2.urlopen(req)
        bs = 1024*8 # from urllib.py:urlretrieve
        headers = u.info()

        # detect total size of the file to be downloaded
        if 'Content-Length' in headers:
            total_size = int(headers['Content-Length'])
            assert total_size >= 0
        else:
            total_size = None
            
        total_bytes_transferred = 0
        chunk_size = 0

        self.hook_initialize(total_size)

        if info:
            LOG.info(info)

        with open(target_file, 'wb') as f:
            while True:
                if total_size:
                    assert total_bytes_transferred <= total_size, str((
                        total_bytes_transferred, total_size))
                    
                self.hook_transferring(chunk_size, total_bytes_transferred)
                
                data = u.read(bs)
                chunk_size = len(data)
                
                if data == '':
                    break
                
                f.write(data)
                total_bytes_transferred += chunk_size

        self.hook_completed()
        
        # save the new headers and other properties
        if save_properties:
            custom_dict = props and props.custom or {} # reuse existing props.custom
            custom_dict['last_attempt_utc'] = BareDateTime.to_string(datetime.utcnow())
            urlprops.save(dict(headers), custom_dict)

    #
    # hooks for various download events
    #
    def hook_initialize(self, total_size):
        """Called just before the download starts
        
        - total_size: total size of the URL; None if it is unknown
        """
    def hook_transferring(self, chunk_size, total_bytes_transferred):
        """Called while the download happens
        
        - chunk_size: bytes transferred in this read
        - total_bytes_transferred: total bytes transferred so far
        """
    def hook_completed(self):
        """Called when the download is complete and the target file is closed"""
        

def _create_http_basicauth_header(username, password):
    base64string = base64.encodestring(
        '{0}:{1}'.format(username, password))[:-1]
    authheader = 'Basic {0}'.format(base64string)
    return authheader


def _http_url_remove_anchor(url):
    """Remove the #... part of the URL"""
    return url.split('#', 1)[0]


def _format_basename(path, fmt):
    """Format the basename of a path

    `fmt` will take a keyword called `basename`
    """
    assert isabs(path)
    return join(dirname(path),
                fmt.format(basename=basename(path)))
