#include <iostream>
#include <fstream>
#include <iterator>
#include <vector>
#include <memory>
#include <functional>
#include <experimental/filesystem>
#include <range/v3/all.hpp>

namespace fs = std::experimental::filesystem;
namespace rng = ranges::v3;
namespace rng_view = ranges::v3::view;

std::vector<uint8_t> read_file(const fs::path& filename);
auto path_to_entries(const fs::path& path);

struct FileEntry {
    fs::path filepath;
    uintmax_t size;
    time_t date_modified;
};

std::ostream& operator << (std::ostream& stream, const FileEntry& entry) {
    stream << entry.filepath << " " << entry.size << " " << entry.date_modified;
    return stream;
}

std::istream& operator >> (std::istream& stream, FileEntry& entry) {
    stream >> entry.filepath >> entry.size >> entry.date_modified;
    return stream;
}

struct BackupHeader {
    std::vector<FileEntry> entries;

    void write(std::ofstream& stream) const {
        stream.write(reinterpret_cast<char*>(entries.size()), sizeof(entries.size()));
        for (const FileEntry& entry : entries) {
            stream << entry;
        }
    }
};

class BackupFile {
public:
    BackupFile(const fs::path& name) : name(name) {}

    template <typename T>
    void init(T&& paths) {
        header.entries = rng_view::transform(paths, [] (const fs::path& path) {
            return rng::yield_from(path_to_entries(path));
        });
    }

    BackupHeader get_header() const {
        return header;
    }

    fs::path get_name() const {
        return name;
    }

    void do_backup() const {
        std::ofstream backup_file { name.c_str(), std::ios_base::out | std::ios_base::binary };
        header.write(backup_file);
        auto buffers = rng_view::transform(header.entries, [] (const FileEntry& entry) {
            return read_file(entry.filepath);
        });
        for (auto buffer : rng_view::bounded(buffers)) {
            backup_file.write(reinterpret_cast<char*>(buffer.data()), buffer.size());
        }
    }

private:
    fs::path name;
    BackupHeader header;
};

time_t get_date_modified(const fs::path& file) {
    auto date_modified = fs::last_write_time(file);
    return decltype(date_modified)::clock::to_time_t(date_modified);
}

std::vector<uint8_t> read_file(const fs::path& filename) {
    std::fstream file { filename.c_str(), std::ios_base::in | std::ios_base::binary  };
    auto size = fs::file_size(filename);
    std::vector<uint8_t> buffer;
    buffer.resize(size, 0);
    file.read(reinterpret_cast<char*>(buffer.data()), size);
    return buffer;
}

FileEntry make_file_entry(const fs::path& file) {
    return { file, fs::file_size(file), get_date_modified(file) };
}

auto make_file_entries(const fs::path& dir) {
    auto iter = fs::recursive_directory_iterator(dir);
    return rng::make_iterator_range(fs::begin(iter), fs::end(iter))
        | rng_view::filter([] (const fs::path& path) { return !fs::is_directory(path); })
        | rng_view::transform(&make_file_entry);
}

auto path_to_entries(const fs::path& path) {
    if (fs::is_directory(path)) {
        return make_file_entries(path);
    } else {
        return rng_view::single(make_file_entry(path));
    }
}

int main() {
    auto dir = fs::path("../..");
    auto dir_iter = fs::directory_iterator(dir);
    auto subfiles = make_file_entries(dir);
    BackupFile backup { "backup" };
    backup.init(rng::make_iterator_range(fs::begin(dir_iter), fs::end(dir_iter)));

    for (auto val : rng_view::bounded(subfiles)) {
        std::cout << val << std::endl;
    }

    return 0;
}
