public static List <FileBlock> ComputeDelta(System.IO.Stream input, long inputSize, ChunkedChecksum chunks, out long deltaSize, Action <long, long> progress = null)
        {
            List <FileBlock> deltas = new List <FileBlock>();

            if (inputSize < chunks.ChunkSize)
            {
                deltas.Add(new FileBlock()
                {
                    Base = false, Offset = 0, Length = inputSize
                });
                deltaSize = 1 + 8 + inputSize;
                return(deltas);
            }
            List <Chunk>           sortedChunks = chunks.Chunks.OrderBy(x => new Tuple <uint, long>(x.Adler32, x.Offset)).ToList();
            Dictionary <uint, int> adlerToIndex = new Dictionary <uint, int>();

            for (int i = 0; i < sortedChunks.Count; i++)
            {
                adlerToIndex[sortedChunks[i].Adler32] = i;
            }

            BufferedView   view           = new BufferedView(input, 4 * 1024 * 1024);
            CircularBuffer circularBuffer = new CircularBuffer(chunks.ChunkSize);
            long           readHead       = 0;
            long           processHead    = 0;
            uint           checksum       = 0;
            FileBlock      lastMatch      = null;

            System.Security.Cryptography.SHA1 sha1 = null;
            if (chunks.HashType == HashMode.SHA1)
            {
                sha1 = System.Security.Cryptography.SHA1.Create();
            }
            while (processHead != inputSize)
            {
                // first run through
                if (readHead == processHead)
                {
                    for (int i = 0; i < chunks.ChunkSize; i++)
                    {
                        circularBuffer.AddByte(view.Next());
                    }
                    readHead = chunks.ChunkSize;
                    checksum = FastHash(circularBuffer.ToArray(chunks.ChunkSize), chunks.ChunkSize);
                }

                int remainder = (int)(readHead - processHead);
                if (progress != null)
                {
                    progress(inputSize, processHead);
                }
                int   possibleChunkLookup;
                Chunk match = null;
                if (adlerToIndex.TryGetValue(checksum, out possibleChunkLookup))
                {
                    byte[] data     = circularBuffer.ToArray(chunks.ChunkSize > remainder ? remainder : chunks.ChunkSize);
                    byte[] realHash = null;
                    if (sha1 != null)
                    {
                        realHash = sha1.ComputeHash(data, 0, data.Length);
                    }
                    else
                    {
                        realHash = (new Utilities.Murmur3()).ComputeHash(data);
                    }
                    while (true)
                    {
                        Chunk inspectedChunk = sortedChunks[possibleChunkLookup];
                        if (inspectedChunk.Adler32 != checksum)
                        {
                            break;
                        }
                        if (inspectedChunk.Length == remainder &&
                            System.Collections.StructuralComparisons.StructuralEqualityComparer.Equals(realHash, inspectedChunk.Hash))
                        {
                            match = inspectedChunk;
                            break;
                        }
                        possibleChunkLookup--;
                    }
                }
                if (match != null)
                {
                    if (lastMatch == null || (lastMatch.Base == false || lastMatch.End != match.Offset))
                    {
                        lastMatch = new FileBlock()
                        {
                            Base = true, Length = 0, Offset = match.Offset
                        };
                        deltas.Add(lastMatch);
                    }
                    lastMatch.Length += match.Length;
                }
                else
                {
                    if (lastMatch == null || lastMatch.Base == true)
                    {
                        lastMatch = new FileBlock()
                        {
                            Base = false, Length = 0, Offset = processHead
                        };
                        deltas.Add(lastMatch);
                    }
                    lastMatch.Length++;
                }

                // eat next byte
                int bytesToEat = 1;
                if (match != null)
                {
                    bytesToEat = match.Length;
                }
                while (bytesToEat-- != 0)
                {
                    remainder = (int)(readHead - processHead);
                    if (readHead < inputSize)
                    {
                        byte add    = view.Next();
                        byte remove = circularBuffer.AddByte(add);
                        checksum = RotateHash(checksum, add, remove, remainder);
                        readHead++;
                    }
                    else if (bytesToEat == 0)
                    {
                        checksum = FastHash(circularBuffer.ToArray(remainder - 1), remainder - 1);
                    }
                    processHead++;
                }
            }

            deltaSize = 0;
            foreach (var x in deltas)
            {
                deltaSize++;
                if (x.Base == false)
                {
                    deltaSize += 8;
                    deltaSize += x.Length;
                }
                else
                {
                    deltaSize += 8;
                    deltaSize += 8;
                }
            }

            return(deltas);
        }