# Copyright (C) 2008-2009 LottaNZB Development Team
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

"""
This module can be used to download NZB files from the Newzbin service. It
does not provide search capabilities and they are unlikely to be added in the
future.
"""

import re
import gzip
import shutil
import os

import logging
log = logging.getLogger(__name__)

from time import sleep
from gettext import ngettext
from httplib import HTTPConnection, HTTPException
from urllib import urlencode

from lottanzb import __version__, resources
from lottanzb.util import gproperty, gsignal, _
from lottanzb.core import App
from lottanzb.util import Thread

class Interface(Thread):
    """
    Abstract base class containing information required to connect to the
    Newzbin servers.
    """
    
    HOST = "v3.newzbin.com"
    AGENT = "lottanzb/%s" % __version__
    HEADERS = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept-Encoding": "gzip",
        "Accept": "text/plain"
    }
    
    username = gproperty(type=str)
    password = gproperty(type=str)
    
    def __init__(self, username, password):
        Thread.__init__(self)
        
        self.username = username
        self.password = password
        
        self.params = { "username": self.username, "password": self.password }
        
        # The only way to handle exceptions in a Thread seems to be by storing
        # the exception object as a property in the Thread object. Handlers
        # attached to the 'completed' event will have to check whether this
        # property is None or not.
        self.exception = None
    
    def run(self):
        """Needs to be implemented by subclasses."""
        
        raise NotImplementedError
    
class Downloader(Interface):
    """
    Asynchronously downloads a Newzbin NZB based on the corresponding report ID.
    """
    
    report_id = gproperty(
        type=int)
    
    name = gproperty(
        type=str,
        nick="Download name, as seen on Newzbin")
    
    category = gproperty(
        type=str,
        nick="English download category")
    
    more_info = gproperty(
        type=str,
        nick="URL referring to additional information about the download")
    
    nfo_id = gproperty(
        type=int,
        nick="ID of the NFO file bundled with the download, if any")
    
    filename = gproperty(
        type=str,
        nick="The absolute path and name of the downloaded NZB file")
    
    gsignal("retry", int)
    
    def __init__(self, username, password, report_id):
        Interface.__init__(self, username, password)
        
        self.report_id = report_id
        self.connection = None
        self.params["reportid"] = self.report_id
    
    def run(self):
        """
        Downloads the NZB file denoted by the Newzbin report ID. Instead of this
        method, the start method should be called to start the download
        opertation.
        
        The 'completed' event is emitted as soon as the download is complete or
        there was an error during the process.
        """
        
        try:
            try:
                self._download()
            except (NewzbinError, HTTPException, IOError), error:
                log.error(str(error))
                
                self.exception = error
        finally:
            # I wish I had a 'with' statement...
            self.connection.close()
        
        self.emit("completed")
    
    def _download(self):
        """Does the actual work."""
        
        log.debug("Downloading NZB for Newzbin file ID %i..." % self.report_id)
        
        self.connection = HTTPConnection(self.HOST)
        self.connection.request("POST", "api/dnzb/", \
            urlencode(self.params), self.HEADERS)
        
        # Synchronous
        response = self.connection.getresponse()
        
        if response.status == 500:
            raise InvalidResponseError()
        elif response.status == 503:
            raise ServiceUnavailableError()
        
        # Newzbin-specific response codes
        newzbin_code = response.getheader("x-dnzb-rcode")
        
        if newzbin_code == "400":
            raise ReportNotFoundError(self.report_id)
        if newzbin_code == "401":
            raise InvalidCredentialsError()
        elif newzbin_code == "402":
            raise NoPremiumAccountError()
        elif newzbin_code == "404":
            raise ReportNotFoundError(self.report_id)
        elif newzbin_code == "450":
            text = response.getheader("x-dnzb-rtext")
            match = re.search(r"wait (\d+) second", text)
            
            if match:
                # TODO: Needs testing.
                timeout = match.group(1)
                
                log.warning(ngettext(
                    _("Too many NZB download requests. Waiting %d second."),
                    _("Too many NZB download requests. Waiting %d seconds."),
                    timeout)
                )
                
                self.connection.close()
                self.emit("retry", timeout)
                
                sleep(timeout + 5)
                
                self._download()
            else:
                # Don't know if there are other reasons a 450 error could
                # show up.
                raise InvalidResponseError()
        
        elif newzbin_code == "200":
            # Extracting the meta data.
            # Remove / and \0 characters and limit the length to 255 characters.
            # The character blacklist will need to be extended as soon as we add
            # Windows support.
            self.name = response.getheader("x-dnzb-name")
            self.name = self.name.replace("/", "-")
            self.name = self.name.replace("\0", "")
            self.name = self.name[:255]
            
            self.category = response.getheader("x-dnzb-category")
            self.more_info = response.getheader("x-dnzb-moreinfo")
            self.nfo_id = response.getheader("x-dnzb-nfo")
            
            self.filename = resources.get_user_temp(self.name + ".nzb")
            
            dest_file = open(self.filename, "wb")
            
            if response.getheader("content-encoding") == "gzip":
                log.debug("Extracting compressed Newzbin NZB...")
                
                gzipped_filename = self.filename + ".gz"
                
                gzipped_file = open(gzipped_filename, "wb")
                gzipped_file.write(response.read())
                gzipped_file.close()
                
                gzipped_file = gzip.open(gzipped_filename)
                shutil.copyfileobj(gzipped_file, dest_file)
                gzipped_file.close()
                os.remove(gzipped_filename)
            else:
                dest_file.write(response.read())
            
            dest_file.close()

class NewzbinError(Exception):
    def __str__(self):
        return self.message

class InvalidResponseError(NewzbinError):
    message = _("Invalid response from the Newzbin server received.")

class ServiceUnavailableError(NewzbinError):
    message = _("The Newzbin service is currently unavailable.")

class InvalidCredentialsError(NewzbinError):
    message = _("The specified Newzbin username or password is invalid.")

class ReportNotFoundError(NewzbinError):
    def __init__(self, report_id):
        self.report_id = report_id
        
        NewzbinError.__init__(self)
    
    def __str__(self):
        return _("Newzbin report %i does not exist.") % self.report_id

class NoPremiumAccountError(NewzbinError):
    message = _("The specified account does not have any Premium credit left.")
