# The contents of this file are subject to the BitTorrent Open Source License
# Version 1.0 (the License).  You may not copy or use this file, in either
# source code or executable form, except in compliance with the License.  You
# may obtain a copy of the License at http://www.bittorrent.com/license/.
#
# Software distributed under the License is distributed on an AS IS basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
# for the specific language governing rights and limitations under the
# License.

# Written by Bram Cohen

from __future__ import division

from sha import sha
from array import array
from binascii import b2a_hex

from BitTorrent.bitfield import Bitfield
from BitTorrent import BTFailure, INFO, WARNING, ERROR, CRITICAL

def toint(s):
    return int(b2a_hex(s), 16)

def tobinary(i):
    return (chr(i >> 24) + chr((i >> 16) & 0xFF) +
        chr((i >> 8) & 0xFF) + chr(i & 0xFF))

NO_PLACE = -1

ALLOCATED = -1
UNALLOCATED = -2
FASTRESUME_PARTIAL = -3

class StorageWrapper(object):

    def __init__(self, storage, config, hashes, piece_size, finished,
            statusfunc, flag, data_flunked, infohash, errorfunc, resumefile):
        self.numpieces = len(hashes)
        self.storage = storage
        self.config = config
        check_hashes = config['check_hashes']
        self.hashes = hashes
        self.piece_size = piece_size
        self.data_flunked = data_flunked
        self.errorfunc = errorfunc
        self.total_length = storage.get_total_length()
        self.amount_left = self.total_length
        self.partial_mark = "BitTorrent - this part has not been "+\
                            "downloaded yet."+infohash+\
                            tobinary(config['download_slice_size'])
        if self.total_length <= piece_size * (self.numpieces - 1):
            raise BTFailure, _("bad data in responsefile - total too small")
        if self.total_length > piece_size * self.numpieces:
            raise BTFailure, _("bad data in responsefile - total too big")
        self.finished = finished
        self.numactive = array('H', [0] * self.numpieces)
        self.inactive_requests = [1] * self.numpieces
        self.amount_inactive = self.total_length
        self.endgame = False
        self.have = Bitfield(self.numpieces)
        self.waschecked = Bitfield(self.numpieces)
        if self.numpieces < 32768:
            typecode = 'h'
        else:
            typecode = 'l'
        self.places = array(typecode, [NO_PLACE] * self.numpieces)
        if not check_hashes:
            self.rplaces = array(typecode, range(self.numpieces))
            fastresume = True
        else:
            self.rplaces = self._load_fastresume(resumefile, typecode)
            if self.rplaces is not None:
                fastresume = True
            else:
                self.rplaces = array(typecode, [UNALLOCATED] * self.numpieces)
                fastresume = False
        self.holepos = 0
        self.stat_numfound = 0
        self.stat_numflunked = 0
        self.stat_numdownloaded = 0
        self.stat_active = {}
        self.stat_new = {}
        self.stat_dirty = {}
        self.download_history = {}
        self.failed_pieces = {}

        if self.numpieces == 0:
            return
        targets = {}
        total = 0
        if not fastresume:
            for i in xrange(self.numpieces):
                if self._waspre(i):
                    self.rplaces[i] = ALLOCATED
                    total += 1
                else:
                    targets[hashes[i]] = i
        if total and check_hashes:
            statusfunc(_("checking existing file"), 0)
        def markgot(piece, pos):
            if self.have[piece]:
                if piece != pos:
                    return
                self.rplaces[self.places[pos]] = ALLOCATED
                self.places[pos] = self.rplaces[pos] = pos
                return
            self.places[piece] = pos
            self.rplaces[pos] = piece
            self.have[piece] = True
            self.amount_left -= self._piecelen(piece)
            self.amount_inactive -= self._piecelen(piece)
            self.inactive_requests[piece] = None
            if not fastresume:
                self.waschecked[piece] = True
            self.stat_numfound += 1
        lastlen = self._piecelen(self.numpieces - 1)
        partials = {}
        for i in xrange(self.numpieces):
            if not self._waspre(i):
                if self.rplaces[i] != UNALLOCATED:
                    raise BTFailure(_("--check_hashes 0 or fastresume info "
                                      "doesn't match file state (missing data)"))
                continue
            elif fastresume:
                t = self.rplaces[i]
                if t >= 0:
                    markgot(t, i)
                    continue
                if t == UNALLOCATED:
                    raise BTFailure(_("Bad fastresume info (files contain more "
                                      "data)"))
                if t == ALLOCATED:
                    continue
                if t!= FASTRESUME_PARTIAL:
                    raise BTFailure(_("Bad fastresume info (illegal value)"))
                data = self.storage.read(self.piece_size * i,
                                         self._piecelen(i))
                self._check_partial(i, partials, data)
                self.rplaces[i] = ALLOCATED
            else:
                data = self.storage.read(piece_size * i, self._piecelen(i))
                sh = sha(buffer(data, 0, lastlen))
                sp = sh.digest()
                sh.update(buffer(data, lastlen))
                s = sh.digest()
                if s == hashes[i]:
                    markgot(i, i)
                elif s in targets and self._piecelen(i) == self._piecelen(targets[s]):
                    markgot(targets[s], i)
                elif not self.have[self.numpieces - 1] and sp == hashes[-1] and (i == self.numpieces - 1 or not self._waspre(self.numpieces - 1)):
                    markgot(self.numpieces - 1, i)
                else:
                    self._check_partial(i, partials, data)
                statusfunc(fractionDone = 1 - self.amount_left /
                           self.total_length)
            if flag.isSet():
                return
        self.amount_left_with_partials = self.amount_left
        for piece in partials:
            if self.places[piece] < 0:
                pos = partials[piece][0]
                self.places[piece] = pos
                self.rplaces[pos] = piece
                self._make_partial(piece, partials[piece][1])
        for i in xrange(self.numpieces):
            if self.rplaces[i] != UNALLOCATED:
                self.storage.allocated(piece_size * i, self._piecelen(i))
            if self.have[i]:
                self.storage.downloaded(piece_size * i, self._piecelen(i))

    def _waspre(self, piece):
        return self.storage.was_preallocated(piece * self.piece_size, self._piecelen(piece))

    def _piecelen(self, piece):
        if piece < self.numpieces - 1:
            return self.piece_size
        else:
            return self.total_length - piece * self.piece_size

    def _check_partial(self, pos, partials, data):
        index = None
        missing = False
        marklen = len(self.partial_mark)+4
        for i in xrange(0, len(data) - marklen,
                        self.config['download_slice_size']):
            if data[i:i+marklen-4] == self.partial_mark:
                ind = toint(data[i+marklen-4:i+marklen])
                if index is None:
                    index = ind
                    parts = []
                if ind >= self.numpieces or ind != index:
                    return
                parts.append(i)
            else:
                missing = True
        if index is not None and missing:
            i += self.config['download_slice_size']
            if i < len(data):
                parts.append(i)
            partials[index] = (pos, parts)

    def _make_partial(self, index, parts):
        length = self._piecelen(index)
        l = []
        self.inactive_requests[index] = l
        x = 0
        self.amount_left_with_partials -= self._piecelen(index)
        self.download_history[index] = {}
        request_size = self.config['download_slice_size']
        for x in xrange(0, self._piecelen(index), request_size):
            partlen = min(request_size, length - x)
            if x in parts:
                l.append((x, partlen))
                self.amount_left_with_partials += partlen
            else:
                self.amount_inactive -= partlen
                self.download_history[index][x] = None
        self.stat_dirty[index] = 1

    def _initalloc(self, pos, piece):
        assert self.rplaces[pos] < 0
        assert self.places[piece] == NO_PLACE
        p = self.piece_size * pos
        length = self._piecelen(pos)
        if self.rplaces[pos] == UNALLOCATED:
            self.storage.allocated(p, length)
        self.places[piece] = pos
        self.rplaces[pos] = piece
        # "if self.rplaces[pos] != ALLOCATED:" to skip extra mark writes
        mark = self.partial_mark + tobinary(piece)
        mark += chr(0xff) * (self.config['download_slice_size'] - len(mark))
        mark *= (length - 1) // len(mark) + 1
        self.storage.write(p, buffer(mark, 0, length))

    def _move_piece(self, oldpos, newpos):
        assert self.rplaces[newpos] < 0
        assert self.rplaces[oldpos] >= 0
        data = self.storage.read(self.piece_size * oldpos,
                                 self._piecelen(newpos))
        self.storage.write(self.piece_size * newpos, data)
        if self.rplaces[newpos] == UNALLOCATED:
            self.storage.allocated(self.piece_size * newpos, len(data))
        piece = self.rplaces[oldpos]
        self.places[piece] = newpos
        self.rplaces[oldpos] = ALLOCATED
        self.rplaces[newpos] = piece
        if not self.have[piece]:
            return
        data = data[:self._piecelen(piece)]
        if sha(data).digest() != self.hashes[piece]:
            raise BTFailure(_("data corrupted on disk - "
                              "maybe you have two copies running?"))

    def _get_free_place(self):
        while self.rplaces[self.holepos] >= 0:
            self.holepos += 1
        return self.holepos

    def get_amount_left(self):
        return self.amount_left

    def do_I_have_anything(self):
        return self.amount_left < self.total_length

    def _make_inactive(self, index):
        length = self._piecelen(index)
        l = []
        x = 0
        request_size = self.config['download_slice_size']
        while x + request_size < length:
            l.append((x, request_size))
            x += request_size
        l.append((x, length - x))
        self.inactive_requests[index] = l

    def _load_fastresume(self, resumefile, typecode):
        if resumefile is not None:
            try:
                r = array(typecode)
                r.fromfile(resumefile, self.numpieces)
                return r
            except Exception, e:
                self.errorfunc(WARNING, _("Couldn't read fastresume data: ") +
                               str(e))
        return None

    def write_fastresume(self, resumefile):
        for i in xrange(self.numpieces):
            if self.rplaces[i] >= 0 and not self.have[self.rplaces[i]]:
                self.rplaces[i] = FASTRESUME_PARTIAL
        self.rplaces.tofile(resumefile)

    def get_have_list(self):
        return self.have.tostring()

    def do_I_have(self, index):
        return self.have[index]

    def do_I_have_requests(self, index):
        return not not self.inactive_requests[index]

    def new_request(self, index):
        # returns (begin, length)
        if self.inactive_requests[index] == 1:
            self._make_inactive(index)
        self.numactive[index] += 1
        self.stat_active[index] = 1
        if index not in self.stat_dirty:
            self.stat_new[index] = 1
        rs = self.inactive_requests[index]
        r = min(rs)
        rs.remove(r)
        self.amount_inactive -= r[1]
        if self.amount_inactive == 0:
            self.endgame = True
        return r

    def piece_came_in(self, index, begin, piece, source = None):
        if self.places[index] < 0:
            if self.rplaces[index] == ALLOCATED:
                self._initalloc(index, index)
            else:
                n = self._get_free_place()
                if self.places[n] >= 0:
                    oldpos = self.places[n]
                    self._move_piece(oldpos, n)
                    n = oldpos
                if self.rplaces[index] < 0 or index == n:
                    self._initalloc(n, index)
                else:
                    self._move_piece(index, n)
                    self._initalloc(index, index)

        if index in self.failed_pieces:
            old = self.storage.read(self.places[index] * self.piece_size +
                                    begin, len(piece))
            if old != piece:
                self.failed_pieces[index][self.download_history[index][begin]]\
                    = None
        self.download_history.setdefault(index, {})
        self.download_history[index][begin] = source

        self.storage.write(self.places[index] * self.piece_size + begin, piece)
        self.stat_dirty[index] = 1
        self.numactive[index] -= 1
        if self.numactive[index] == 0:
            del self.stat_active[index]
        if index in self.stat_new:
            del self.stat_new[index]
        if not self.inactive_requests[index] and not self.numactive[index]:
            del self.stat_dirty[index]
            if sha(self.storage.read(self.piece_size * self.places[index], self._piecelen(index))).digest() == self.hashes[index]:
                self.have[index] = True
                self.storage.downloaded(index * self.piece_size,
                                        self._piecelen(index))
                self.inactive_requests[index] = None
                self.waschecked[index] = True
                self.amount_left -= self._piecelen(index)
                self.stat_numdownloaded += 1
                for d in self.download_history[index].itervalues():
                    if d is not None:
                        d.good(index)
                del self.download_history[index]
                if index in self.failed_pieces:
                    for d in self.failed_pieces[index]:
                        if d is not None:
                            d.bad(index)
                    del self.failed_pieces[index]
                if self.amount_left == 0:
                    self.finished()
            else:
                self.data_flunked(self._piecelen(index), index)
                self.inactive_requests[index] = 1
                self.amount_inactive += self._piecelen(index)
                self.stat_numflunked += 1

                self.failed_pieces[index] = {}
                allsenders = {}
                for d in self.download_history[index].itervalues():
                    allsenders[d] = None
                if len(allsenders) == 1:
                    culprit = allsenders.keys()[0]
                    if culprit is not None:
                        culprit.bad(index, bump = True)
                    del self.failed_pieces[index] # found the culprit already
                return False
        return True

    def request_lost(self, index, begin, length):
        self.inactive_requests[index].append((begin, length))
        self.amount_inactive += length
        self.numactive[index] -= 1
        if not self.numactive[index] and index in self.stat_active:
            del self.stat_active[index]
            if index in self.stat_new:
                del self.stat_new[index]

    def get_piece(self, index, begin, length):
        if not self.have[index]:
            return None
        if not self.waschecked[index]:
            if sha(self.storage.read(self.piece_size * self.places[index], self._piecelen(index))).digest() != self.hashes[index]:
                raise BTFailure, _("told file complete on start-up, but piece failed hash check")
            self.waschecked[index] = True
        if begin + length > self._piecelen(index):
            return None
        return self.storage.read(self.piece_size * self.places[index] + begin, length)
