// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0           *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "binary.h"
#include <vector>
#include <chrono>

using namespace zen;
using AFS = AbstractFileSystem;

namespace
{
/*
1. there seems to be no perf improvement possible when using file mappings instad of ::ReadFile() calls on Windows:
    => buffered   access: same perf
    => unbuffered access: same perf on USB stick, file mapping 30% slower on local disk

2. Tests on Win7 x64 show that buffer size does NOT matter if files are located on different physical disks!

Impact of buffer size when files are on same disk:

buffer  MB/s
------------
  64    10
 128    19
 512    40
1024    48
2048    56
4096    56
8192    56
*/
const size_t BLOCK_SIZE_MAX =  16 * 1024 * 1024;


struct StreamReader
{
    StreamReader(const AbstractPath& filePath, const IOCallback& notifyUnbufferedIO) :
        stream_(AFS::getInputStream(filePath, notifyUnbufferedIO)), //throw FileError, (ErrorFileLocked), X
        defaultBlockSize_(stream_->getBlockSize()),
        dynamicBlockSize_(defaultBlockSize_) { assert(defaultBlockSize_ > 0); }

    void appendChunk(std::vector<char>& buffer) //throw FileError, X
    {
        assert(!eof_);
        if (eof_) return;

        buffer.resize(buffer.size() + dynamicBlockSize_);

        const auto startTime = std::chrono::steady_clock::now();
        const size_t bytesRead = stream_->read(&*(buffer.end() - dynamicBlockSize_), dynamicBlockSize_); //throw FileError, X; return "bytesToRead" bytes unless end of stream!
        const auto stopTime = std::chrono::steady_clock::now();

        buffer.resize(buffer.size() - dynamicBlockSize_ + bytesRead); //caveat: unsigned arithmetics

        if (bytesRead < dynamicBlockSize_)
        {
            eof_ = true;
            return;
        }

        size_t proposedBlockSize = 0;
        const auto loopTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stopTime - startTime).count();

        if (loopTimeMs >= 100)
            lastDelayViolation_ = stopTime;

        //avoid "flipping back": e.g. DVD-ROMs read 32MB at once, so first read may be > 500 ms, but second one will be 0ms!
        if (stopTime >= lastDelayViolation_ + std::chrono::seconds(2))
        {
            lastDelayViolation_ = stopTime;
            proposedBlockSize = dynamicBlockSize_ * 2;
        }
        if (loopTimeMs > 500)
            proposedBlockSize = dynamicBlockSize_ / 2;

        if (defaultBlockSize_ <= proposedBlockSize && proposedBlockSize <= BLOCK_SIZE_MAX)
            dynamicBlockSize_ = proposedBlockSize;
    }

    bool isEof() const { return eof_; }

private:
    const std::unique_ptr<AFS::InputStream> stream_;
    const size_t defaultBlockSize_;
    size_t dynamicBlockSize_;
    std::chrono::steady_clock::time_point lastDelayViolation_ = std::chrono::steady_clock::now();
    bool eof_ = false;
};
}


bool zen::filesHaveSameContent(const AbstractPath& filePath1, const AbstractPath& filePath2, const IOCallback& notifyUnbufferedIO) //throw FileError
{
    int64_t totalUnbufferedIO = 0;

    StreamReader reader1(filePath1, IOCallbackDivider(notifyUnbufferedIO, totalUnbufferedIO)); //throw FileError, (ErrorFileLocked), X
    StreamReader reader2(filePath2, IOCallbackDivider(notifyUnbufferedIO, totalUnbufferedIO)); //

    StreamReader* readerLow  = &reader1;
    StreamReader* readerHigh = &reader2;

    std::vector<char> bufferLow;
    std::vector<char> bufferHigh;

    for (;;)
    {
        readerLow->appendChunk(bufferLow); //throw FileError, X

        if (bufferLow.size() > bufferHigh.size())
        {
            bufferLow.swap(bufferHigh);
            std::swap(readerLow, readerHigh);
        }

        if (!std::equal(bufferLow. begin(), bufferLow.end(),
                        bufferHigh.begin()))
            return false;

        if (readerLow->isEof())
        {
            if (bufferLow.size() < bufferHigh.size())
                return false;
            if (readerHigh->isEof())
                break;
            //bufferLow.swap(bufferHigh); not needed
            std::swap(readerLow, readerHigh);
        }

        //don't let sliding buffer grow too large
        bufferHigh.erase(bufferHigh.begin(), bufferHigh.begin() + bufferLow.size());
        bufferLow.clear();
    }

    if (totalUnbufferedIO % 2 != 0)
        throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__));

    return true;
}
