#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import with_statement
__license__   = 'GPL v3'
__docformat__ = 'restructuredtext en'


# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
#
# Requires Calibre version 0.7.55 or higher.
#
# All credit given to i♥cabbages for the original standalone scripts.
# I had the much easier job of converting them to a calibre plugin.
#
# This plugin is meant to decrypt Barnes & Noble Epubs that are protected
# with a version of Adobe's Adept encryption. It is meant to function without having to
# install any dependencies... other than having calibre installed, of course. It will still
# work if you have Python and PyCrypto already installed, but they aren't necessary.
#
# Configuration:
# Check out the plugin's configuration settings by clicking the "Customize plugin"
# button when you have the "BnN ePub DeDRM" plugin highlighted (under Preferences->
# Plugins->File type plugins). Once you have the configuration dialog open, you'll
# see a Help link on the top right-hand side.
#
# Revision history:
#   0.1.0 - Initial release
#   0.1.1 - Allow Windows users to make use of openssl if they have it installed.
#          - Incorporated SomeUpdates zipfix routine.
#   0.1.2 - bug fix for non-ascii file names in encryption.xml
#   0.1.3 - Try PyCrypto on Windows first
#   0.1.4 - update zipfix to deal with mimetype not in correct place
#   0.1.5 - update zipfix to deal with completely missing mimetype files
#   0.1.6 - update for the new calibre plugin interface
#   0.1.7 - Fix for potential problem with PyCrypto
#   0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
#   0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
#   0.2.1 - added zipfix.py and included zipfilerugged.py from 0.1.8
#   0.2.2 - added in potential fixes from 0.1.7 that had been missed.
#   0.2.3 - fixed possible output/unicode problem
#   0.2.4 - ditched nearly hopeless caselessStrCmp method in favor of uStrCmp.
#         - added ability to rename existing keys.
#   0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and
#         - ignoblekeygen 2.4 and later.
#   0.2.6 - Tweaked to eliminate issue with both ignoble and inept calibre plugins installed/enabled at once

"""
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
"""

PLUGIN_NAME = u"Ignoble Epub DeDRM"
PLUGIN_VERSION_TUPLE = (0, 2, 6)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'

import sys, os, re
import zipfile
from zipfile import ZipFile

class IGNOBLEError(Exception):
    pass

from calibre.customize import FileTypePlugin
from calibre.constants import iswindows, isosx
from calibre.gui2 import is_ok_to_use_qt

# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
    def __init__(self, stream):
        self.stream = stream
        self.encoding = stream.encoding
        if self.encoding == None:
            self.encoding = "utf-8"
    def write(self, data):
        if isinstance(data,unicode):
            data = data.encode(self.encoding,"replace")
        self.stream.write(data)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)


class IgnobleDeDRM(FileTypePlugin):
    name                    = PLUGIN_NAME
    description             = u"Removes DRM from secure Barnes & Noble epub files. Credit given to i♥cabbages for the original stand-alone scripts."
    supported_platforms     = ['linux', 'osx', 'windows']
    author                  = u"DiapDealer, Apprentice Alf and i♥cabbages"
    version                 = PLUGIN_VERSION_TUPLE
    minimum_calibre_version = (0, 7, 55)  # Compiled python libraries cannot be imported in earlier versions.
    file_types              = set(['epub'])
    on_import               = True
    priority                = 101

    def run(self, path_to_ebook):

        # make sure any unicode output gets converted safely with 'replace'
        sys.stdout=SafeUnbuffered(sys.stdout)
        sys.stderr=SafeUnbuffered(sys.stderr)

        print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))

        # First time use or first time after upgrade to new key-handling/storage method
        # or no keys configured. Give a visual prompt to configure.
        import calibre_plugins.ignobleepub.config as cfg
        if not cfg.prefs['configured']:
            titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION)
            errmsg = titlemsg + ' not (properly) configured!\n' + \
                    '\nThis may be the first time you\'ve used this plugin' + \
                    ' (or the first time since upgrading this plugin).' + \
                    ' You\'ll need to open the customization dialog (Preferences->Plugins->File type plugins)' + \
                    ' and follow the instructions there.\n' + \
                    '\nIf you don\'t use the ' + PLUGIN_NAME + ' plugin, you should disable or uninstall it.'
            if is_ok_to_use_qt():
                from PyQt4.Qt import QMessageBox
                d = QMessageBox(QMessageBox.Warning, titlemsg, errmsg )
                d.show()
                d.raise_()
                d.exec_()
            raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION))

        # Create a TemporaryPersistent file to work with.
        # Check original epub archive for zip errors.
        from calibre_plugins.ignobleepub import zipfix
        inf = self.temporary_file(u".epub")
        try:
            print u"{0} v{1}: Verifying zip archive integrity.".format(PLUGIN_NAME, PLUGIN_VERSION)
            fr = zipfix.fixZip(path_to_ebook, inf.name)
            fr.fix()
        except Exception, e:
            print u"{0} v{1}: Error \'{2}\' when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
            raise Exception(e)
            return

        #check the book
        from calibre_plugins.ignobleepub import ignobleepub
        if not ignobleepub.ignobleBook(inf.name):
            raise IGNOBLEError(u"{0} v{1}: {2} is not a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))

        # Attempt to decrypt epub with each encryption key (generated or provided).
        for keyname, userkey in cfg.prefs['keys'].items():
            keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname)
            print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked)
            of = self.temporary_file(u".epub")

            # Give the user key, ebook and TemporaryPersistent file to the decryption function.
            result = ignobleepub.decryptBook(userkey, inf.name, of.name)

            of.close()

            # Decryption was successful return the modified PersistentTemporary
            # file to Calibre's import process.
            if  result == 0:
                print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION)
                return of.name
                break

            print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION)

        # Something went wrong with decryption.
        # Import the original unmolested epub.
        raise IGNOBLEError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION))
        return

    def is_customizable(self):
        # return true to allow customization via the Plugin->Preferences.
        return True

    def config_widget(self):
        from calibre_plugins.ignobleepub.config import ConfigWidget
        # Extract the helpfile contents from in the plugin's zipfile.
        # The helpfile must be named <plugin name variable> + '_Help.htm'
        return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME])

    def load_resources(self, names):
        ans = {}
        with ZipFile(self.plugin_path, 'r') as zf:
            for candidate in zf.namelist():
                if candidate in names:
                    ans[candidate] = zf.read(candidate)
        return ans

    def save_settings(self, config_widget):
        config_widget.save_settings()
