# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder 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, see <http://www.gnu.org/licenses/>.
#

#
# dbsqlite.py -- SQLite persistence layer for gPodder
#
# 2008-06-13 Justin Forest <justin.forest@gmail.com>
# 2010-04-24 Thomas Perl <thp@gpodder.org>
#

import logging
import re
import sys
import threading
from sqlite3 import dbapi2 as sqlite

import gpodder
from gpodder import schema, util

_ = gpodder.gettext

logger = logging.getLogger(__name__)


class Database(object):
    TABLE_PODCAST = 'podcast'
    TABLE_EPISODE = 'episode'

    def __init__(self, filename):
        self.database_file = filename
        self._db = None
        self.lock = threading.RLock()

    def close(self):
        self.commit()

        with self.lock:
            self.db.isolation_level = None
            self.db.execute('VACUUM')
            self.db.isolation_level = ''

        self._db.close()
        self._db = None

    def purge(self, max_episodes, podcast_id):
        """
        Deletes old episodes.  Should be called
        before adding new episodes to a podcast.
        """
        if max_episodes == 0:
            return

        with self.lock:
            cur = self.cursor()

            logger.debug('Purge requested for podcast %d', podcast_id)
            sql = """
                DELETE FROM %s
                WHERE podcast_id = ?
                AND state <> ?
                AND id NOT IN
                (SELECT id FROM %s WHERE podcast_id = ?
                ORDER BY published DESC LIMIT ?)""" % (self.TABLE_EPISODE, self.TABLE_EPISODE)
            cur.execute(sql, (podcast_id, gpodder.STATE_DOWNLOADED, podcast_id, max_episodes))

            cur.close()

    @property
    def db(self):
        if self._db is None:
            self._db = sqlite.connect(self.database_file, check_same_thread=False)

            # Check schema version, upgrade if necessary
            schema.upgrade(self._db, self.database_file)

            # Sanity checks for the data in the database
            schema.check_data(self)

            logger.debug('Database opened.')
        return self._db

    def cursor(self):
        return self.db.cursor()

    def commit(self):
        with self.lock:
            try:
                logger.debug('Commit.')
                self.db.commit()
            except Exception as e:
                logger.error('Cannot commit: %s', e, exc_info=True)

    def get_content_types(self, id):
        """Given a podcast ID, returns the content types"""
        with self.lock:
            cur = self.cursor()
            cur.execute('SELECT mime_type FROM %s WHERE podcast_id = ?' % self.TABLE_EPISODE, (id,))
            for (mime_type,) in cur:
                yield mime_type
            cur.close()

    def get_podcast_statistics(self, podcast_id=None):
        """Given a podcast ID, returns the statistics for it

        If the podcast_id is omitted (using the default value), the
        statistics will be calculated over all podcasts.

        Returns a tuple (total, deleted, new, downloaded, unplayed)
        """
        total, deleted, new, downloaded, unplayed = 0, 0, 0, 0, 0

        with self.lock:
            cur = self.cursor()
            if podcast_id is not None:
                cur.execute('SELECT COUNT(*), state, is_new FROM %s '
                            'WHERE podcast_id = ? GROUP BY state, is_new'
                            % self.TABLE_EPISODE, (podcast_id,))
            else:
                cur.execute('SELECT COUNT(*), state, is_new FROM %s '
                            'GROUP BY state, is_new' % self.TABLE_EPISODE)
            for count, state, is_new in cur:
                total += count
                if state == gpodder.STATE_DELETED:
                    deleted += count
                elif state == gpodder.STATE_NORMAL and is_new:
                    new += count
                elif state == gpodder.STATE_DOWNLOADED:
                    downloaded += count
                    if is_new:
                        unplayed += count

            cur.close()

        return (total, deleted, new, downloaded, unplayed)

    def load_podcasts(self, factory):
        logger.info('Loading podcasts')

        sql = 'SELECT * FROM %s' % self.TABLE_PODCAST

        with self.lock:
            cur = self.cursor()
            cur.execute(sql)

            keys = [desc[0] for desc in cur.description]
            result = [factory(dict(list(zip(keys, row))), self) for row in cur]
            cur.close()

        return result

    def load_episodes(self, podcast, factory):
        assert podcast.id

        logger.info('Loading episodes for podcast %d', podcast.id)

        sql = 'SELECT * FROM %s WHERE podcast_id = ? ORDER BY published DESC' % self.TABLE_EPISODE
        args = (podcast.id,)

        with self.lock:
            cur = self.cursor()
            cur.execute(sql, args)

            keys = [desc[0] for desc in cur.description]
            result = [factory(dict(list(zip(keys, row)))) for row in cur]
            cur.close()

        return result

    def delete_podcast(self, podcast):
        assert podcast.id

        with self.lock:
            cur = self.cursor()
            logger.debug('delete_podcast: %d (%s)', podcast.id, podcast.url)

            cur.execute("DELETE FROM %s WHERE id = ?" % self.TABLE_PODCAST, (podcast.id, ))
            cur.execute("DELETE FROM %s WHERE podcast_id = ?" % self.TABLE_EPISODE, (podcast.id, ))

            cur.close()
            self.db.commit()

    def save_podcast(self, podcast):
        self._save_object(podcast, self.TABLE_PODCAST, schema.PodcastColumns)

    def save_episode(self, episode):
        self._save_object(episode, self.TABLE_EPISODE, schema.EpisodeColumns)

    def _save_object(self, o, table, columns):
        with self.lock:
            try:
                cur = self.cursor()
                values = [util.convert_bytes(getattr(o, name))
                        for name in columns]

                if o.id is None:
                    qmarks = ', '.join('?' * len(columns))
                    sql = 'INSERT INTO %s (%s) VALUES (%s)' % (table, ', '.join(columns), qmarks)
                    cur.execute(sql, values)
                    o.id = cur.lastrowid
                else:
                    qmarks = ', '.join('%s = ?' % name for name in columns)
                    values.append(o.id)
                    sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks)
                    cur.execute(sql, values)
            except Exception as e:
                logger.error('Cannot save %s: %s', o, e, exc_info=True)

            cur.close()

    def get(self, sql, params=None):
        """
        Returns the first cell of a query result, useful for COUNT()s.
        """
        with self.lock:
            cur = self.cursor()

            if params is None:
                cur.execute(sql)
            else:
                cur.execute(sql, params)

            row = cur.fetchone()
            cur.close()

        if row is None:
            return None
        else:
            return row[0]

    def podcast_download_folder_exists(self, foldername):
        """
        Returns True if a foldername for a channel exists.
        False otherwise.
        """
        foldername = util.convert_bytes(foldername)

        return self.get("SELECT id FROM %s WHERE download_folder = ?" %
                self.TABLE_PODCAST, (foldername,)) is not None

    def episode_filename_exists(self, podcast_id, filename):
        """
        Returns True if a filename for an episode exists.
        False otherwise.
        """
        filename = util.convert_bytes(filename)

        return self.get("SELECT id FROM %s WHERE podcast_id = ? AND download_filename = ?" %
                self.TABLE_EPISODE, (podcast_id, filename,)) is not None

    def get_last_published(self, podcast):
        """
        Look up the most recent publish date of a podcast.
        """
        return self.get('SELECT MAX(published) FROM %s WHERE podcast_id = ?' % self.TABLE_EPISODE, (podcast.id,))

    def delete_episode_by_guid(self, guid, podcast_id):
        """
        Deletes episodes that have a specific GUID for
        a given channel. Used after feed updates for
        episodes that have disappeared from the feed.
        """
        guid = util.convert_bytes(guid)

        with self.lock:
            cur = self.cursor()
            cur.execute('DELETE FROM %s WHERE podcast_id = ? AND guid = ?' %
                    self.TABLE_EPISODE, (podcast_id, guid))
