class RangesList_test(unittest.TestCase):
    def setUp(self):
        self._list = RangesList(0, 0)

    def tearDown(self):
        self._list.clear()

    def testUnion(self):
        self._list.add(1, 4)
        self._list.add(2, 5)
        self._list.add(0, 2)
        self._list.add(5, 7)
        self.assertEqual(self._list.size(), 1, ".size() error")
        self.assertEqual(self._list.ranges(), [(0, 7)], "Ranges union result error")
        self._list.add(8, 12)
        self._list.add(14, 16)
        self._list.add(15, 17)
        self._list.add(19, 24)
        self.assertEqual(self._list.size(), 4, ".size() error")
        self.assertEqual(self._list.ranges(), [(0, 7), (8, 12), (14, 17), (19, 24)], "Ranges union result error")

    def testSpliting(self):
        self._list.add(0, 7)
        self._list.add(8, 12)
        self._list.add(14, 17)
        self._list.add(19, 24)
        self._list.remove(2, 5)
        self._list.remove(6, 13)
        self._list.remove(14, 24)
        self.assertEqual(self._list.size(), 2, ".size() error")
        self.assertEquals(self._list.ranges(), [(0, 2), (5, 6)], "Ranges splitting result error")
class AutoResizeableFile(QtCore.QObject):
    """
        Class representing the auto-resizeable files
    """
    def __init__(self, filePath = None):
        """
            Default constructor. Opens specified file for reading and writing.
            'filePath' - path to the file. If file doesn't exist, new file created.
        If 'filePath' has None value, a temporary file created, which would be 
        removed after it's closing.
        """
        QtCore.QObject.__init__(self)
        self._isTemporary = False;
        if filePath == None:
            filePath = os.tempnam()
            self._isTemporary = True
        self._file = QtCore.QFile(filePath)
        if not self._file.open(QtCore.QIODevice.ReadWrite):
            raise IOError("File can't be opened!",
                          {"raisingObject": self, "path": filePath, "error": self._file.error()})
        self._validRanges = RangesList(0, self._file.size())
        self._packingListeners = []
        
        
    def __del__(self):
        """
            Default finalizing method (destructor).
        Packs, saves all changes and closes the file
        """
        self.close()
               
                  
    def close(self):
        """
            Packs, saves all changes and closes the file.
        Returns None.
        """
        if self._file.isOpen():
            self.flush()
            self._file.close()
            if self._isTemporary:
                self._file.remove()
                
                
    def path(self):
        """
            Returns file path
        """
        return self._file.fileName()
                        
                          
    def position(self):
        """
            Returns current file position
        """
        return self._file.pos()
    
    
    def size(self):
        """
            Returns real physical size of the file. 
        """
        return self._file.size()
    
    def validRanges(self):
        """
            Returns a list (each item - a 2-integer tuple) of valid file sections.
        Valid section is a one, which wasn't removed previously
        """
        return self._validRanges.ranges()
    
    
    def isOpened(self):
        """
            Returns True if file is opened, False otherwise
        """
        return self._file.isOpen()
    
    
    def endOfFile(self):
        """
            Returns True if current file position points to the end of physical file.
        """
        return self.position() >= self.size()
    
    
    def setPosition(self, position):
        """
            Sets new file position. Returns real value of position. 
            'position' - new file position (integer)
        """
        position = self._validateInteger(position)
        if not self._file.seek(position):
            raise IOError("Seeking position failed", 
                             {"raiseObject": self, "position": position, "error": self._file.error()})
        return self.position()
    
    
    def erase(self):
        """
            Erases all file content. Returns None
        """
        self._file.resize(0)
        self.setPosition(0)
        self._validRanges.clear()
    
    
    def addPackingListener(self, listener):
        """
            Adds a new file packing event listener. This event happens
        when all previously removed file sections are removed physically
        from the disk. Returns None
            'listener' - a FilePackingListener object reference
        """
        if not isinstance(listener, FilePackingListener):
            raise TypeError("Listener must has a 'FilePackingListener' type!",
                            {"raisingObject": self})
        self._packingListeners.append(listener)
        
        
    def removePackingListener(self, listener):
        """
            Removes listener added via 'addPackingListener' method.
            'listener' - a FilePackingListener object reference
        """
        if not isinstance(listener, FilePackingListener):
            raise TypeError("Listener must has a 'FilePackingListener' type!",
                            {"raisingObject": self})
        if not (listener in self._packingListeners):
            return
        del self._packingListeners[self._packingListeners.index(listener)]
        
        
    def realPositionFor(self, validPosition):
        """
            Translates any file position to real position in the physical
        file. This values may be different if any sections were removed 
        previously 
            'validPosition' - position to translate (integer)
        """
        ranges = self._validRanges.ranges()
        lastRangeIndex, currentSum = 0, 0
        rangeFound = False
        for lastRangeIndex in xrange(0, len(ranges)):
            currentRange = ranges[lastRangeIndex]
            if currentSum <= validPosition < currentSum + (currentRange[1] - currentRange[0]):
                rangeFound = True
                break
            else:
                currentSum += (currentRange[1] - currentRange[0])
        if not rangeFound:
            if currentSum == validPosition:
                return  validPosition 
            else:
                raise IndexError("Position is too large", {"raisingObject": self, 
                                                           "value": validPosition})
        lastEnumeratedRange = ranges[lastRangeIndex]
        neededDelta = validPosition - currentSum
        return lastEnumeratedRange[0] + neededDelta
    
        
    def readLine(self):
        """
            Reads and returns a line.
        """
        data = str(self._file.readLine())
        return data.strip()
    
    
    def read(self, size = -1):
        """
            Reads and returns a string with specified length
            'size' - needed length of string. If 'size' has -1 value,
        the whole file up to the end would be read
        """
        size = self._validateInteger(size)
        if size == -1:
            size = self._file.size() - self.position()   
        data = str(self._file.read(size))
        return data
    
    
    def readAll(self):
        """
            Reads and returns the whole file content, saves old filed position
        """
        oldPosition = self.position()
        self.setPosition(0)
        data = self.read()
        self.setPosition(oldPosition)
        return data
    
    
    def readInteger(self):
        """
            Reads next 4 bytes from the file and converts them to integer value,
        that would be returned 
        """
        oldPosition = self.position()
        string = self.read(4)
        if len(string) != 4:
            self._file.seek(oldPosition)
            raise EOFError("Can't read needed amount of bytes!",
                           {"raiseObject": self, "position": oldPosition, "fileSize": self._file.size()})
        return struct.unpack("i", string)[0]
    
    
    def write(self, string, eraseFirstSymbols = 0):
        """
            Writes data represented by 'string', previously removed
        'eraseFirstSymbols' bytes. Returns really written bytes amount
        """
        eraseFirstSymbols = self._validateInteger(eraseFirstSymbols)
        string = str(string)
        stringLength = len(string)
        eraseFirstSymbols = min(eraseFirstSymbols, self._file.size() - self.position())
        additionalSpace = stringLength - eraseFirstSymbols
        start= self.position() + eraseFirstSymbols
        newStart = start + additionalSpace
        oldPosition = self.position()
        self._shift(start, -1, newStart)
        written = self._file.writeData(string)
        self.setPosition(oldPosition + written)
        rangeToAdd = (oldPosition, oldPosition + stringLength)
        shiftingValue = newStart - start
        self._shiftValidRanges(start, shiftingValue)
        self._validRanges.add(rangeToAdd[0], rangeToAdd[1])
        return written
        
        
    def writeLine(self, string, eraseFirstSymbols = 0):
        """
            Writes a line represented by 'string', previously removed
        'eraseFirstSymbols' bytes. Returns really written bytes amount
        """
        string = str(string) + '\n'
        return self.write(string, eraseFirstSymbols)
        
    
    def writeInteger(self, value, eraseOld = False):
        """
            Writes an integer represented by 'value' with erasing
        previous value if 'eraseOld' is True. Returns really written bytes amount
        """
        value = self._validateInteger(value)
        eraseFirstSymbols = 0
        if eraseOld:
            eraseFirstSymbols = 4
        return self.write(struct.pack("i", value), eraseFirstSymbols)
    
    
    def flush(self):
        """
            Flushes file to the disk with previously packing.
        Returns None.
        """
        self.pack()
        self._file.flush()
        
        
    def pack(self):
        """
            Performs packing operation, that physically removes
        all removed previously file sections via 'removeRange' method.
        Returns None.
        """
        if self._validRanges.size() == 0:
            self.setPosition(0)
            self._file.resize(0)
            return
        shiftingInfos = []
        currentValidSize = 0
        totalShifting = 0
        for validRange in self._validRanges.ranges():
            shiftValue = currentValidSize - validRange[0]
            totalShifting += shiftValue
            if (validRange[0] != validRange[1]) and (totalShifting != 0):
                shiftingInfos.append(RangeShiftingInfo(validRange, totalShifting))
            self._shift(validRange[0], validRange[1], currentValidSize)
            currentValidSize += validRange[1] - validRange[0]
        self._file.resize(currentValidSize)
        if len(shiftingInfos) != 0:
            for listener in self._packingListeners:
                listener.filePackedEvent(shiftingInfos)
        self._validRanges.clear()
        self._validRanges.add(0, currentValidSize)
                        
    
    def removeRange(self, startPosition, endPosition):
        """
            Removes file section, specified by 'startPosition' and
        'endPosition' constrains ('endPosition' - excludly).Returns None
        """
        self._validRanges.remove(startPosition, endPosition)
        invalidSpacePercentage = (1 - (float(self._validRanges.size()) / float(self._file.size()))) * 100.0
        if invalidSpacePercentage > 25.0 or self._validRanges.size() >= 64:
            self.pack() 
        
    
    def _shift(self, start, end, newStart):
        if newStart == start:
            return
        if end == -1:
            end = self._file.size();
        start, end = min(start, end), max(start, end)
        oldPosition = self.position()
        self.setPosition(start)
        data = self.read(end - start)
        if newStart > self._file.size():
            self._file.resize(newStart)
        self.setPosition(newStart)
        self._file.writeData(data)
        self.setPosition(oldPosition)
        
    
    def _shiftValidRanges(self, fileStartPosition, shiftingValue):
        ranges = self._validRanges.ranges()
        firstShiftingRangeIndex = -1
        for i in xrange(0, len(ranges)):
            if ranges[i][0] <= fileStartPosition <= ranges[i][1]:
                firstShiftingRangeIndex = i
                break
        if firstShiftingRangeIndex == -1:
            return
        else:
            firstShiftingRange = ranges[firstShiftingRangeIndex]
            splitedRanges = [(firstShiftingRange[0], fileStartPosition), 
                             (fileStartPosition, firstShiftingRange[1])]
            self._validRanges.remove(splitedRanges[1][0], ranges[len(ranges) - 1][1])
            shiftingRanges = [(splitedRanges[1][0], splitedRanges[1][1])]
            for i in xrange(firstShiftingRangeIndex + 1, len(ranges)):
                if ranges[i][0] != ranges[i][1]:
                    shiftingRanges.append(ranges[i])
            shiftingInfos = []
            for item in shiftingRanges:
                self._validRanges.add(item[0] + shiftingValue, item[1] + shiftingValue)
                if (item[0] != item[1]) and (shiftingValue != 0):
                    shiftingInfos.append(RangeShiftingInfo(item, shiftingValue))
            if len(shiftingInfos) != 0:
                for listener in self._packingListeners:
                    listener.filePackedEvent(shiftingInfos)
                    
    
    def _validateInteger(self, value):
        try:
            return int(value)
        except (TypeError, AttributeError, ValueError):
            raise TypeError("Value mast be an integer", {"raisingObject": self, "value": value})