###
# Copyright (c) 2011-2021, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

###

import os
import io
import sys
import json
import shutil
import tarfile

import supybot.log as log
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.utils.minisix as minisix
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring

_ = PluginInternationalization('PluginDownloader')

class Repository:
    pass

class VersionnedRepository(Repository):
    pass

class GitRepository(VersionnedRepository):
    pass

class GithubRepository(GitRepository):
    def __init__(self, username, reponame, path='/', branch='HEAD'):
        self._username = username
        self._reponame = reponame
        self._branch = branch
        if not path.startswith('/'):
            path = '/' + path
        if not path.endswith('/'):
            path += '/'
        self._path = path

        self._downloadUrl = 'https://github.com/%s/%s/tarball/%s' % \
                            (
                            self._username,
                            self._reponame,
                            self._branch
                            )


    _apiUrl = 'https://api.github.com'
    def _query(self, type_, uri_end, args={}):
        args = dict([(x,y) for x,y in args.items() if y is not None])
        url = '%s/%s/%s?%s' % (self._apiUrl, type_, uri_end,
                               utils.web.urlencode(args))
        return json.loads(utils.web.getUrl(url).decode('utf8'))

    def getPluginList(self):
        plugins = self._query('repos',
                              '%s/%s/contents%s' % (self._username,
                                                    self._reponame,
                                                    self._path),
                              args={'ref': self._branch}
                             )
        if plugins is None:
            log.error((
                      'Cannot get plugins list from repository %s/%s '
                      'at Github'
                      ) % (self._username, self._reponame))
            return []
        plugins = [x['name'] for x in plugins if x['type'] == 'dir']
        return plugins

    def _download(self, plugin):
        try:
            response = utils.web.getUrlFd(self._downloadUrl)
            if minisix.PY2:
                assert response.getcode() == 200, response.getcode()
            else:
                assert response.status == 200, response.status
            fileObject = minisix.io.BytesIO()
            fileObject.write(response.read())
        finally: # urllib does not handle 'with' statements :(
            response.close()
        fileObject.seek(0)
        return tarfile.open(fileobj=fileObject, mode='r:gz')

    def install(self, plugin):
        archive = self._download(plugin)
        prefix = archive.getnames()[0]
        dirname = ''.join((self._path, plugin))
        directories = conf.supybot.directories.plugins()
        directory = self._getWritableDirectoryFromList(directories)
        assert directory is not None, \
                'No valid directory in supybot.directories.plugins.'

        possibly_incompatible = False
        try:
            assert archive.getmember(prefix + dirname).isdir(), \
                'This is not a valid plugin (it is a file, not a directory).'

            for file in archive.getmembers():
                if file.name.startswith(prefix + dirname):
                    extractedFile = archive.extractfile(file)
                    newFileName = os.path.join(*file.name.split('/')[1:])
                    newFileName = newFileName[len(self._path)-1:]
                    newFileName = os.path.join(directory, newFileName)
                    if os.path.exists(newFileName):
                        assert os.path.isdir(newFileName), newFileName + \
                                'should not be a file.'
                        shutil.rmtree(newFileName)
                    if extractedFile is None:
                        os.mkdir(newFileName)
                    else:
                        with open(newFileName, 'ab') as fd:
                            for line in extractedFile.readlines():
                                if file.name.endswith('__init__.py') and \
                                        line.startswith((b'import config', b'import plugin')):
                                    possibly_incompatible = True
                                fd.write(line)
        finally:
            archive.close()
            del archive
        if possibly_incompatible:
            return _('Plugin installed. However, it may be incompatible with '
                     'Python 3 and require manual code changes to work correctly.')
        return _('Plugin successfully installed.')

    def getInfo(self, plugin):
        archive = self._download(plugin)
        prefix = archive.getnames()[0]
        dirname = ''.join((self._path, plugin))
        for file in archive.getmembers():
            if file.name.startswith(prefix + dirname + '/README'):
                extractedFile = archive.extractfile(file)
                content = extractedFile.read()
                if minisix.PY3:
                    content = content.decode()
                return content

    def _getWritableDirectoryFromList(self, directories):
        for directory in directories:
            if os.access(directory, os.W_OK):
                return directory
        return None


repositories = utils.InsensitivePreservingDict({
               'progval':          GithubRepository(
                                                   'progval',
                                                   'Supybot-plugins'
                                                   ),
               'SpiderDave':       GithubRepository(
                                                   'SpiderDave',
                                                   'spidey-supybot-plugins',
                                                   'Plugins',
                                                   ),
               'Hoaas':            GithubRepository(
                                                   'Hoaas',
                                                   'Supybot-plugins'
                                                   ),
               'nyuszika7h':       GithubRepository(
                                                   'nyuszika7h',
                                                   'limnoria-plugins'
                                                   ),
               'frumious':         GithubRepository(
                                                   'frumiousbandersnatch',
                                                   'sobrieti-plugins',
                                                   'plugins',
                                                   ),
               'skgsergio':        GithubRepository(
                                                   'skgsergio',
                                                   'Limnoria-plugins',
                                                   ),
               'jlu5':             GithubRepository(
                                                   'jlu5',
                                                   'SupyPlugins',
                                                   ),
               'Iota':             GithubRepository(
                                                   'IotaSpencer',
                                                   'supyplugins',
                                                   ),
               'waratte':          GithubRepository(
                                                   'waratte',
                                                   'supybot',
                                                   ),
               't3chguy':          GithubRepository(
                                                   't3chguy',
                                                   'Limnoria-Plugins',
                                                   ),
               'prgmrbill':        GithubRepository(
                                                   'prgmrbill',
                                                   'limnoria-plugins',
                                                   ),
               'fudster':          GithubRepository(
                                                   'fudster',
                                                   'supybot-plugins',
                                                   ),
               'oddluck':          GithubRepository(
                                                   'oddluck',
                                                   'limnoria-plugins',
                                                   ),
               'appas':            GithubRepository(
                                                   'matiasw',
                                                   'my-limnoria-plugins',
                                                   ),
               'spithash':         GithubRepository(
                                                   'spithash',
                                                   'Limnoria-Plugins',
                                                   ),
               })

class PluginDownloader(callbacks.Plugin):
    """
    This plugin allows you to install unofficial plugins from
    multiple repositories easily. Use the "repolist" command to see list of
    available repositories and "repolist <repository>" to list plugins,
    which are available in that repository. When you want to install a plugin,
    just run command "install <repository> <plugin>".

    First start by using the `plugindownloader repolist` command to see the
    available repositories.

    To see the plugins inside repository, use command
    `plugindownloader repolist <repository>`

    When you see anything interesting, you can use
    `plugindownloader info <repository> <plugin>` to see what the plugin is
    for.

    And finally to install the plugin,
    `plugindownloader install <repository> <plugin>`.

    Examples
    ^^^^^^^^

    ::

        < Mikaela> @load PluginDownloader
        < Limnoria> Ok.
        < Mikaela> @plugindownloader repolist
        < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem
        < Mikaela> @plugindownloader repolist progval
        < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)
        < Mikaela> more
        < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather
        < Mikaela> @plugindownloader info progval Wikipedia
        < Limnoria> Grabs data from Wikipedia.
        < Mikaela> @plugindownloader install progval Wikipedia
        < Limnoria> Ok.
        < Mikaela> @load Wikipedia
        < Limnoria> Ok.
    """

    threaded = True

    @internationalizeDocstring
    def repolist(self, irc, msg, args, repository):
        """[<repository>]

        Displays the list of plugins in the <repository>.
        If <repository> is not given, returns a list of available
        repositories."""

        global repositories
        if repository is None:
            irc.reply(_(', ').join(sorted(x for x in repositories)))
        elif repository not in repositories:
            irc.error(_(
                       'This repository does not exist or is not known by '
                       'this bot.'
                       ))
        else:
            plugins = repositories[repository].getPluginList()
            if plugins == []:
                irc.error(_('No plugin found in this repository.'))
            else:
                irc.reply(_(', ').join([x for x in plugins]))
    repolist = wrap(repolist, [optional('something')])

    @internationalizeDocstring
    def install(self, irc, msg, args, repository, plugin):
        """<repository> <plugin>

        Downloads and installs the <plugin> from the <repository>."""
        if not conf.supybot.commands.allowShell():
            irc.error(_('This command is not available, because '
                'supybot.commands.allowShell is False.'), Raise=True)
        global repositories
        if repository not in repositories:
            irc.error(_(
                       'This repository does not exist or is not known by '
                       'this bot.'
                       ))
        elif plugin not in repositories[repository].getPluginList():
            irc.error(_('This plugin does not exist in this repository.'))
        else:
            try:
                irc.reply(repositories[repository].install(plugin))
            except Exception as e:
                import traceback
                traceback.print_exc()
                log.error(str(e))
                irc.error('The plugin could not be installed. Check the logs '
                        'for a more detailed error.')

    install = wrap(install, ['owner', 'something', 'something'])

    @internationalizeDocstring
    def info(self, irc, msg, args, repository, plugin):
        """<repository> <plugin>

        Displays informations on the <plugin> in the <repository>."""
        global repositories
        if repository not in repositories:
            irc.error(_(
                       'This repository does not exist or is not known by '
                       'this bot.'
                       ))
        elif plugin not in repositories[repository].getPluginList():
            irc.error(_('This plugin does not exist in this repository.'))
        else:
            info = repositories[repository].getInfo(plugin)
            if info is None:
                irc.error(_('No README found for this plugin.'))
            else:
                if info.startswith('Insert a description of your plugin here'):
                    irc.error(_('This plugin has no description.'))
                else:
                    info = info.split('\n\n')[0]
                    irc.reply(info.replace('\n', ' '))
    info = wrap(info, ['something', optional('something')])


PluginDownloader = internationalizeDocstring(PluginDownloader)
Class = PluginDownloader


# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
