# -*- coding: ascii -*-

###########################################################################
# clive, video extraction utility
# Copyright (C) 2007-2008 Toni Gundogdu
#
# This file is part of clive.
#
# clive 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.
#
# clive 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 clive.  If not, see <http://www.gnu.org/licenses/>.
###########################################################################

## The classes for writing and reading cache

__all__ = ['Cache','Browse']

from clive.path import ConfigDir
from clive.progress import Progress
from clive.unicode import tostr
from clive.modules import Modules
from clive.opts import Options

## The class that wraps Cache access
class Cache:

    ## Constructor
    def __init__(self, say):
        self._say = say
        self._md5 = Modules().getinst('md5')
        self._time = Modules().getinst('time')
        self._pysqlite_avail = \
            Modules().check('pysqlite')['installed']

        if self._pysqlite_avail:
            self._pysqlite = Modules().getinst('pysqlite')
            self._conn = self._pysqlite.connect(ConfigDir().cachefile())
            self._conn.row_factory = self._pysqlite.Row
            self._cur = self._conn.cursor()
            self._upgrade_if_needed()
            self._create_table()

    ## Destructor
    def __del__(self):
        if self._pysqlite_avail:
            self._cur.execute('vacuum')
            self._cur.close()

    ## Write a new cache entry
    def write(self, v_info):
        hash = self._md5.new(v_info['url'].strip('&fmt=18')).hexdigest()
        r = self.read(v_info['url'].strip('&fmt=18'))
        if not r:
            try:
                self._cur.execute('insert into cache(' \
                    'cache_hash, ' \
                    'cache_time, ' \
                    'cache_title, ' \
                    'cache_length, ' \
                    'cache_xurl, ' \
                    'cache_vid, ' \
                    'cache_lowq, ' \
                    'cache_url)' \
                    'values(?,?,?,?,?,?,?,?)',
                    (hash, self._time.time(), v_info['page_title'], 
                    v_info['length_bytes'], v_info['xurl'], v_info['v_id'],
                    v_info['low_quality'], v_info['url'].strip('&fmt=18')))
            except self._pysqlite.IntegrityError, err:
                pass # Ignore any "url hash not unique" errors
            except self._pysqlite.OperationalError, err:
                pass # Ignore any "database locked" etc. errors
        else: # Update existing record (e.g. if quality setting was changed)
            if r['cache_lowq'] != v_info['low_quality']:
                self._cur.execute('update cache set ' \
                    'cache_time=?, ' \
                    'cache_title=?, ' \
                    'cache_length=?, ' \
                    'cache_xurl=?, ' \
                    'cache_vid=?, ' \
                    'cache_lowq=?, ' \
                    'cache_url=? where cache_hash=?',
                    (self._time.time(), v_info['page_title'],
                    v_info['length_bytes'],
                    v_info['xurl'], v_info['v_id'], v_info['low_quality'],
                    v_info['url'].strip('&fmt=18'), hash))
        self._conn.commit()

    ## Read cache entry for URL
    def read(self, url):
        try:
            hash = self._md5.new(url.strip('&fmt=18')).hexdigest()
            self._cur.execute('select ' \
                'cache_title,' \
                'cache_length,' \
                'cache_xurl,' \
                'cache_vid, ' \
                'cache_lowq ' \
                'from cache where cache_hash=?', (hash,))
        except self._pysqlite.OperationalError, err:
            if 'no such column' in err.message:
                # Should database upgrade fail for some reason, re-create
                # the table. This will, of course, remove all existing
                # cache data also. See: http://bugs.debian.org/479315
                self._say('cache: recreate cache due to ' +
                    '"no such column" error')
                self._recreate_table()
                return None
            else:
                raise err
        return self._cur.fetchone()

    ## Check if extraction URL has expired
    def has_xurl_expired(self, url):
        hosts = [('youtube.', 90),('dailymotion.', 2)]
        for (host, expires_min) in hosts: # Only these hosts req. check
            if host in url.lower():
                hash = self._md5.new(url.strip('&fmt=18')).hexdigest()
                self._cur.execute('select ' \
                    'cache_time from cache where cache_hash=?', (hash,))
                row = self._cur.fetchone()
                if not row: return 0 # Not found -> not expired
                elapsed = self._time.time() - float(row['cache_time'])
                gmt = self._time.gmtime(elapsed)
                return (gmt[3]*60 + gmt[4]) >= expires_min
        return 0 # No such host -> not expired

    ## Updates extraction URL for an URL (hash)
    def update_expired_xurl(self, url, xurl):
        hash = self._md5.new(url.strip('&fmt=18')).hexdigest()
        self._cur.execute('update cache set cache_time=?, cache_xurl=? ' \
            'where cache_hash=?', (self._time.time(), xurl, hash))
        self._conn.commit()

    ## Select all cache entries, return cursor
    def select_all(self):
        self._cur.execute('select * from cache')
        return self._cur

    ## Clear cache data
    def clear(self, enable_confirm):
        Modules().getinst('pysqlite') # SystemExits if needed
        try:
            self._cur.execute('select * from cache limit 1')
            row = self._cur.fetchone()
            if not row:
                self._say('error: cache: nothing to clear')
            else:
                a = 'y'
                if enable_confirm:
                    a = raw_input('> clear cache, are you sure? (y/N): ')
                if len(a) and a.lower()[0] == 'y':
                    self._recreate_table()
        except self._pysqlite.OperationalError, err:
            if err.message == 'no such table' != -1: pass

    def _recreate_table(self):
        self._cur.execute('drop table cache')
        self._create_table()

    def _create_table(self):
        try:
            self._cur.execute('create table cache(' \
                'cache_id integer primary key, ' \
                'cache_hash text not null unique, ' \
                'cache_time float not null, ' \
                'cache_title text not null, ' \
                'cache_length long not null, ' \
                'cache_xurl text not null, ' \
                'cache_vid text not null,' \
                'cache_lowq integer not null,' \
                'cache_url text not null)')
        except self._pysqlite.OperationalError, err:
            # Ignore only "table exists" errors only
            if 'table exists already' in err.message:
                pass

    def _upgrade_if_needed(self):
        try:
            self._cur.execute('select * from cache limit 1')
            row = self._cur.fetchone()
            if not row: return
            if len(row) < 8:
                # Must be <= 0.4.10 database
                self._alter_table("'cache_lowq' integer not null default -1")
            if len(row) < 9:
                # Must be > 0.4.10 <= 0.4.16 database
                self._alter_table("'cache_url' text not null default ''")
        except self._pysqlite.OperationalError, err:
            # Ignore "no such table" errors only
            if err.message == 'no such table' != -1: pass

    def _alter_table(self, s):
        self._cur.execute('alter table cache add column %s' % s)
        self._conn.commit()

## The cache browsing class
class Browse:

    ## Constructor
    def __init__(self, say):
        self._last = None # Selection before hitting "Resize"
        self._opts = Options()._opts
        self._say = say
        self._newt = Modules().getinst('newt')

    ## Browse URLs for re-extraction
    def browse(self):
        self._cache = Cache(self._say)
        if not self._cache.select_all().fetchone():
            raise SystemExit('error: no cache data')
        while 1:
            sel = self._main(self._cache.select_all())
            if sel: break
        return sel            

    def _main(self, cur):
        sel = []
        try:
            scr = self._newt.SnackScreen()
            sel = self._show_cache(scr, cur)
        finally:
            scr.finish()
        return sel            

    def _ok(self):
        return self._ct.getSelection()

    def _cancel(self):
        raise SystemExit('Cancelled.')

    def _resize(self):
        self._last = self._ct.getSelection()
        return None

    def _show_cache(self, scr, cur):
        w = scr.width - (scr.width/6)
        ww = w-12
        ct = self._newt.CheckboxTree(scr.height/2, scroll=1)
        i = 0
        while 1:
            row = cur.fetchone()
            if not row: break
            if len(row['cache_url']): # Ignore any < 0.4.16 entries
                t = row['cache_title'][:w]
                try:
                    t = tostr(t)
                except UnicodeDecodeError, err:
                    continue # Skip item if decoding fails for some reason
                ct.append(t, row['cache_url'])
                # Recall last state: selected | unselected
                sel = False
                if self._last and row['cache_url'] in self._last:
                    sel = True
                u = row['cache_url'][:ww]
                u += (' (%s)' % Progress()._byteshuman(row['cache_length']))
                ct.addItem(u, (i, self._newt.snackArgs['append']),
                    row['cache_url'], selected=sel)
                i += 1                    
        g = self._newt.GridForm(scr, 'Cache: Videos found', 1,2)
        g.add(ct, col=0, row=0, padding=(0,0,0,1))
        b = self._newt.ButtonBar(scr,
            [('Resize',2),('Cancel',0), ('Extract',1)], compact=0)
        g.add(b, col=0, row=01, padding=(0,0,0,0))
        self._ct = ct
        d = {0:self._cancel, 1:self._ok, 2:self._resize,None:self._ok}
        return d.get(b.buttonPressed(g.runOnce()))()
