""" Helper class used by InsertingBundles to create hg bundle files
    and cache information about their sizes.

    Copyright (C) 2009 Darrell Karbott

    This library 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 2.0 of the License, or (at your option) any later version.

    This library 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 library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
"""

import os
import shutil
import random

from mercurial import commands

from fcpconnection import sha1_hexdigest

from graph import FIRST_INDEX, FREENET_BLOCK_LEN, MAX_REDUNDANT_LENGTH
from graphutil import get_rollup_bounds

def make_temp_file(temp_dir):
    """ Make a temporary file name. """
    return os.path.join(temp_dir, '_tmp_' + ('%0.16f' % random.random())[2:14])

def is_writable(dir_name):
    """ Check whether the directory exists and is writable.  """
    tmp_file = os.path.join(dir_name, '_tmp_test_write')
    out_file = None
    try:
        try:
            out_file = open(tmp_file, 'wb')
            out_file.write('Can I write here?\n')
            return True
        except IOError:
            return False
        return True
    finally:
        if not out_file is None:
            out_file.close()
        if os.path.exists(tmp_file):
            os.remove(tmp_file)


class BundleException(Exception):
    """ An Exception for problems encountered with bundles."""
    def __init__(self, msg):
        Exception.__init__(self, msg)

class BundleCache:
    """ Class to create hg bundle files and cache information about
        their sizes. """

    def __init__(self, repo, ui_, base_dir):
        self.graph = None
        self.repo = repo
        self.ui_ = ui_
        self.redundant_table = {}
        self.base_dir = os.path.abspath(base_dir)
        assert is_writable(self.base_dir)
        self.enabled = True

    def get_bundle_path(self, index_pair):
        """ INTERNAL: Get the full path to a bundle file for the given edge. """
        bundle_id = sha1_hexdigest(
            ''.join(self.graph.index_table[index_pair[0]][0])
            + '|' # hmmm really needed?
            +''.join(self.graph.index_table[index_pair[0]][1])

            +''.join(self.graph.index_table[index_pair[1]][0])
            + '|' # hmmm really needed?
            +''.join(self.graph.index_table[index_pair[1]][1])
            )

        return os.path.join(self.base_dir, "_tmp_%s.hg" % bundle_id)

    def get_cached_bundle(self, index_pair, out_file):
        """ INTERNAL: Copy the cached bundle file for the edge to out_file. """
        full_path = self.get_bundle_path(index_pair)
        if not os.path.exists(full_path):
            return None

        if not out_file is None:
            # can't do this for paths that don't exist
            #assert not os.path.samefile(out_file, full_path)
            if os.path.exists(out_file):
                os.remove(out_file)

            raised = True
            try:
                shutil.copyfile(full_path, out_file)
                raised = False
            finally:
                if raised and os.path.exists(out_file):
                    os.remove(out_file)

        return (os.path.getsize(full_path), out_file, index_pair)

    def update_cache(self, index_pair, out_file):
        """ INTERNAL: Store a file in the cache. """
        assert out_file != self.get_bundle_path(index_pair)

        raised = True
        try:
            shutil.copyfile(out_file, self.get_bundle_path(index_pair))
            raised = False
        finally:
            if raised and os.path.exists(out_file):
                os.remove(out_file)

    def make_bundle(self, graph, version_table, index_pair, out_file=None):
        """ Create an hg bundle file corresponding to the edge in graph. """
        #print "INDEX_PAIR:", index_pair
        assert not index_pair is None
        self.graph = graph

        cached = self.get_cached_bundle(index_pair, out_file)
        if not cached is None:
            #print "make_bundle -- cache hit: ", index_pair
            return cached

        delete_out_file = out_file is None
        if out_file is None:
            out_file = make_temp_file(self.base_dir)
        try:

            parents, heads = get_rollup_bounds(self.graph, self.repo,
                                               index_pair[0] + 1, # INCLUSIVE
                                               index_pair[1],
                                               version_table)

            # Hmmm... ok to suppress mercurial noise here.
            self.ui_.pushbuffer()
            try:
                #print 'PARENTS:', list(parents)
                #print 'HEADS:', list(heads)
                commands.bundle(self.ui_, self.repo, out_file,
                                None, base=list(parents),
                                rev=list(heads))
            finally:
                self.ui_.popbuffer()

            if self.enabled:
                self.update_cache(index_pair, out_file)
            file_field = None
            if not delete_out_file:
                file_field = out_file
            return (os.path.getsize(out_file), file_field, index_pair)
        finally:
            if delete_out_file and os.path.exists(out_file):
                os.remove(out_file)

    # INTENT: Freenet stores data in 32K blocks.  If we can stuff
    # extra changes into the bundle file under the block boundry
    # we get extra redundancy for free.
    def make_redundant_bundle(self, graph, version_table, last_index,
                              out_file=None):
        """ Make an hg bundle file including the changes in the edge and
            other earlier changes if it is possible to fit them under
            the 32K block size boundry. """
        self.graph = graph
        #print "make_redundant_bundle -- called for index: ", last_index

        if out_file is None and last_index in self.redundant_table:
            #print "make_redundant_bundle -- cache hit: ", last_index
            return self.redundant_table[last_index]

        size_boundry = None
        prev_length = None
        earliest_index = last_index - 1
        while earliest_index >= FIRST_INDEX:
            pair = (earliest_index, last_index)
            #print "PAIR:", pair
            bundle = self.make_bundle(graph,
                                      version_table,
                                      pair,
                                      out_file)

            #print "make_redundant_bundle -- looping: ", earliest_index, \
            #      last_index, bundle[0]
            assert bundle[0] > 0 # hmmmm

            if size_boundry is None:
                size_boundry = ((bundle[0] / FREENET_BLOCK_LEN)
                                * FREENET_BLOCK_LEN)
                prev_length = bundle[0]
                if (bundle[0] % FREENET_BLOCK_LEN) == 0:
                    # REDFLAG: test this code path
                    self.redundant_table[bundle[2]] = bundle
                    return bundle # Falls exactly on a 32k boundry
                else:
                    size_boundry += FREENET_BLOCK_LEN

                # Purely to bound the effort spent creating bundles.
                if bundle[0] > MAX_REDUNDANT_LENGTH:
                    #print "make_redundant_bundle -- to big for redundancy"
                    self.redundant_table[bundle[2]] = bundle
                    return bundle

            if bundle[0] > size_boundry:
                earliest_index += 1 # Can only happen after first pass???
                #print "make_redundant_bundle -- breaking"
                break

            earliest_index -= 1
            prev_length = bundle[0]

        bundle =  (prev_length, out_file,
                   (max(FIRST_INDEX, earliest_index), last_index))
        #           ^--- possible to fix loop so this is not required?

        #print "make_redundant_bundle -- return: ", bundle
        self.redundant_table[bundle[2]] = bundle
        return bundle

    def remove_files(self):
        """ Remove cached files. """
        for name in os.listdir(self.base_dir):
            # Only remove files that we created in case cache_dir
            # is set to something like ~/.
            if name.startswith("_tmp_"):
                os.remove(os.path.join(self.base_dir, name))