from twisted.internet import defer
from twisted.python import log

import p2pool
from p2pool.bitcoin import data as bitcoin_data
from p2pool.util import deferral, forest, jsonrpc, variable

class HeaderWrapper(object):
    __slots__ = 'hash previous_hash'.split(' ')
    
    @classmethod
    def from_header(cls, header):
        return cls(bitcoin_data.hash256(bitcoin_data.block_header_type.pack(header)), header['previous_block'])
    
    def __init__(self, hash, previous_hash):
        self.hash, self.previous_hash = hash, previous_hash

class HeightTracker(object):
    '''Point this at a factory and let it take care of getting block heights'''
    
    def __init__(self, best_block_func, factory, backlog_needed):
        self._best_block_func = best_block_func
        self._factory = factory
        self._backlog_needed = backlog_needed
        
        self._tracker = forest.Tracker()
        
        self._watch1 = self._factory.new_headers.watch(self._heard_headers)
        self._watch2 = self._factory.new_block.watch(self._request)
        
        self._requested = set()
        self._clear_task = deferral.RobustLoopingCall(self._requested.clear)
        self._clear_task.start(60)
        
        self._last_notified_size = 0
        
        self.updated = variable.Event()
        
        self._think_task = deferral.RobustLoopingCall(self._think)
        self._think_task.start(15)
        self._think2_task = deferral.RobustLoopingCall(self._think2)
        self._think2_task.start(15)
    
    def _think(self):
        try:
            highest_head = max(self._tracker.heads, key=lambda h: self._tracker.get_height_and_last(h)[0]) if self._tracker.heads else None
            if highest_head is None:
                return # wait for think2
            height, last = self._tracker.get_height_and_last(highest_head)
            if height < self._backlog_needed:
                self._request(last)
        except:
            log.err(None, 'Error in HeightTracker._think:')
    
    def _think2(self):
        self._request(self._best_block_func())
    
    def _heard_headers(self, headers):
        changed = False
        for header in headers:
            hw = HeaderWrapper.from_header(header)
            if hw.hash in self._tracker.items:
                continue
            changed = True
            self._tracker.add(hw)
        if changed:
            self.updated.happened()
        self._think()
        
        if len(self._tracker.items) >= self._last_notified_size + 100:
            print 'Have %i/%i block headers' % (len(self._tracker.items), self._backlog_needed)
            self._last_notified_size = len(self._tracker.items)
    
    @defer.inlineCallbacks
    def _request(self, last):
        if last in self._tracker.items:
            return
        if last in self._requested:
            return
        self._requested.add(last)
        (yield self._factory.getProtocol()).send_getheaders(version=1, have=[], last=last)
    
    def get_height_rel_highest(self, block_hash):
        # callers: highest height can change during yields!
        best_height, best_last = self._tracker.get_height_and_last(self._best_block_func())
        height, last = self._tracker.get_height_and_last(block_hash)
        if last != best_last:
            return -1000000000 # XXX hack
        return height - best_height

@defer.inlineCallbacks
def get_height_rel_highest_func(bitcoind, factory, best_block_func, net):
    if '\ngetblock ' in (yield deferral.retry()(bitcoind.rpc_help)()):
        @deferral.DeferredCacher
        @defer.inlineCallbacks
        def height_cacher(block_hash):
            try:
                block_hash_str = '%x' % (block_hash,)
                if len(block_hash_str) != 64:
                    block_hash_str = '0' * (64 - len(block_hash_str)) + block_hash_str
                x = yield bitcoind.rpc_getblock(block_hash_str)
            except jsonrpc.Error_for_code(-5): # Block not found
                if not p2pool.DEBUG:
                    raise deferral.RetrySilentlyException()
                else:
                    raise
            defer.returnValue(x['blockcount'] if 'blockcount' in x else x['height'])
        best_height_cached = variable.Variable((yield deferral.retry()(height_cacher)(best_block_func())))
        def get_height_rel_highest(block_hash):
            this_height = height_cacher.call_now(block_hash, 0)
            best_height = height_cacher.call_now(best_block_func(), 0)
            best_height_cached.set(max(best_height_cached.value, this_height, best_height))
            return this_height - best_height_cached.value
    else:
        get_height_rel_highest = HeightTracker(best_block_func, factory, 5*net.SHARE_PERIOD*net.CHAIN_LENGTH/net.PARENT.BLOCK_PERIOD).get_height_rel_highest
    defer.returnValue(get_height_rel_highest)
