/*
    SPDX-FileCopyrightText: 2005 Joris Guisson <joris.guisson@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "torrentcreator.h"
#include "globals.h"
#include "statsfile.h"
#include "torrentcontrol.h"
#include <KLocalizedString>
#include <QDir>
#include <QFileInfo>
#include <bcodec/bencoder.h>
#include <ctime>
#include <diskio/chunkmanager.h>
#include <util/array.h>
#include <util/error.h>
#include <util/file.h>
#include <util/fileops.h>
#include <util/functions.h>
#include <util/log.h>
#include <util/sha1hash.h>
#include <version.h>

using namespace Qt::Literals::StringLiterals;

namespace bt
{
TorrentCreator::TorrentCreator(const QString &tar,
                               const QStringList &track,
                               const QList<QUrl> &webseeds,
                               Uint32 cs,
                               const QString &name,
                               const QString &comments,
                               bool priv,
                               bool decentralized)
    : target(tar)
    , trackers(track)
    , webseeds(webseeds)
    , chunk_size(cs)
    , name(name)
    , comments(comments)
    , cur_chunk(0)
    , priv(priv)
    , tot_size(0)
    , decentralized(decentralized)
    , stopped(false)
{
    this->chunk_size *= 1024;
    const QFileInfo fi(target);
    if (fi.isDir()) {
        if (!this->target.endsWith(bt::DirSeparator())) {
            this->target += bt::DirSeparator();
        }

        tot_size = 0;
        buildFileList(QString());
    } else {
        tot_size = bt::FileSize(target);
    }

    num_chunks = tot_size / chunk_size;
    last_size = tot_size % chunk_size;
    if (last_size == 0) {
        last_size = chunk_size;
    } else {
        ++num_chunks;
    }

    Out(SYS_GEN | LOG_DEBUG) << "Tot Size : " << tot_size << endl;
    Out(SYS_GEN | LOG_DEBUG) << "Num Chunks : " << num_chunks << endl;
    Out(SYS_GEN | LOG_DEBUG) << "Chunk Size : " << chunk_size << endl;
    Out(SYS_GEN | LOG_DEBUG) << "Last Size : " << last_size << endl;
}

TorrentCreator::~TorrentCreator()
{
}

void TorrentCreator::buildFileList(const QString &dir)
{
    const QDir d(target + dir);
    // first get all files (we ignore symlinks)
    const QStringList dfiles = d.entryList(QDir::Files);
    Uint32 cnt = 0; // counter to keep track of file index
    for (const QString &s : dfiles) {
        // add a TorrentFile to the list
        const Uint64 fs = bt::FileSize(target + dir + s);
        const TorrentFile f(nullptr, cnt, dir + s, tot_size, fs, chunk_size);
        files.append(f);
        // update total size
        tot_size += fs;
        cnt++;
    }

    // now for each subdir do a buildFileList
    const QStringList subdirs = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
    for (const QString &s : subdirs) {
        QString sd = dir + s;
        if (!sd.endsWith(bt::DirSeparator())) {
            sd += bt::DirSeparator();
        }
        buildFileList(sd);
    }
}

void TorrentCreator::saveTorrent(const QString &url)
{
    File fptr;
    if (!fptr.open(url, u"wb"_s)) {
        throw Error(i18n("Cannot open file %1: %2", url, fptr.errorString()));
    }

    BEncoder enc(&fptr);
    enc.beginDict(); // top dict

    if (!decentralized) {
        enc.write("announce");
        if (trackers.count() > 0) {
            enc.write(trackers[0].toUtf8());
        } else {
            enc.write(QByteArray());
        }

        if (trackers.count() > 1) {
            enc.write("announce-list");
            enc.beginList();
            for (const QString &t : std::as_const(trackers)) {
                enc.beginList();
                enc.write(t.toUtf8());
                enc.end();
            }
            enc.end();
        }
    }

    if (comments.length() > 0) {
        enc.write("comment", comments.toUtf8());
    }
    enc.write("created by", bt::GetVersionString().toLatin1());
    enc.write("creation date", (Uint64)time(nullptr));
    enc.write("info");
    saveInfo(enc);
    // save the nodes list after the info hash, keys must be sorted !
    if (decentralized) {
        // DHT torrent
        enc.write("nodes");
        enc.beginList();

        for (const QString &t : std::as_const(trackers)) {
            enc.beginList();
            enc.write(t.section(','_L1, 0, 0).toUtf8());
            enc.write((Uint32)t.section(','_L1, 1, 1).toInt());
            enc.end();
        }
        enc.end();
    }

    if (webseeds.count() == 1) {
        enc.write("url-list", webseeds[0].toDisplayString().toUtf8());
    } else if (webseeds.count() > 0) {
        enc.write("url-list");
        enc.beginList();
        for (const QUrl &u : std::as_const(webseeds)) {
            enc.write(u.toDisplayString().toUtf8());
        }
        enc.end();
    }

    enc.end();
}

void TorrentCreator::saveInfo(BEncoder &enc)
{
    enc.beginDict();

    const QFileInfo fi(target);
    if (fi.isDir()) {
        enc.write("files");
        enc.beginList();
        for (const TorrentFile &file : std::as_const(files)) {
            saveFile(enc, file);
        }

        enc.end();
    } else {
        enc.write("length", bt::FileSize(target));
    }
    enc.write("name", name.toUtf8());
    enc.write("piece length", (Uint64)chunk_size);
    enc.write("pieces");
    savePieces(enc);
    if (priv) {
        enc.write("private", (Uint64)1);
    }
    enc.end();
}

void TorrentCreator::saveFile(BEncoder &enc, const TorrentFile &file)
{
    enc.beginDict();
    enc.write("length", file.getSize());
    enc.write("path");
    enc.beginList();
    const QStringList sl = file.getPath().split(bt::DirSeparator());
    for (const QString &s : sl) {
        enc.write(s.toUtf8());
    }
    enc.end();
    enc.end();
}

void TorrentCreator::savePieces(BEncoder &enc)
{
    Array<Uint8> big_hash(num_chunks * 20);
    for (Uint32 i = 0; i < num_chunks; ++i) {
        memcpy(big_hash.data() + (20 * i), hashes[i].getData(), 20);
    }
    enc.write(big_hash);
}

bool TorrentCreator::calcHashSingle()
{
    Array<Uint8> buf(chunk_size);
    File fptr;
    if (!fptr.open(target, u"rb"_s)) {
        throw Error(i18n("Cannot open file %1: %2", target, fptr.errorString()));
    }

    const Uint32 s = cur_chunk != num_chunks - 1 ? chunk_size : last_size;
    fptr.seek(File::BEGIN, (Int64)cur_chunk * chunk_size);
    fptr.read(buf.data(), s);
    const SHA1Hash h = SHA1Hash::generate(buf.data(), s);
    hashes.append(h);
    cur_chunk++;
    return cur_chunk >= num_chunks;
}

bool TorrentCreator::calcHashMulti()
{
    const Uint32 s = cur_chunk != num_chunks - 1 ? chunk_size : last_size;
    // first find the file(s) the chunk lies in
    Array<Uint8> buf(s);
    QList<TorrentFile> file_list;
    int i = 0;
    while (i < files.size()) {
        const TorrentFile &tf = files[i];
        if (cur_chunk >= tf.getFirstChunk() && cur_chunk <= tf.getLastChunk()) {
            file_list.append(tf);
        }

        i++;
    }

    Uint32 read = 0;
    for (i = 0; i < file_list.count(); i++) {
        const TorrentFile &f = file_list[i];
        File fptr;
        if (!fptr.open(target + f.getPath(), u"rb"_s)) {
            throw Error(i18n("Cannot open file %1: %2", f.getPath(), fptr.errorString()));
        }

        // first calculate offset into file
        // only the first file can have an offset
        // the following files will start at the beginning
        Uint64 off = 0;
        if (i == 0) {
            off = f.fileOffset(cur_chunk, chunk_size);
        }

        Uint32 to_read = 0;
        // then the amount of data we can read from this file
        if (file_list.count() == 1) {
            to_read = s;
        } else if (i == 0) {
            to_read = f.getLastChunkSize();
        } else if (i == file_list.count() - 1) {
            to_read = s - read;
        } else {
            to_read = f.getSize();
        }

        // read part of data
        fptr.seek(File::BEGIN, (Int64)off);
        fptr.read(buf.data() + read, to_read);
        read += to_read;
    }

    // generate hash
    const SHA1Hash h = SHA1Hash::generate(buf.data(), s);
    hashes.append(h);

    cur_chunk++;
    //  Out(SYS_GEN|LOG_DEBUG) << "=============================================" << endl;
    return cur_chunk >= num_chunks;
}

bool TorrentCreator::calculateHash()
{
    if (cur_chunk >= num_chunks) {
        return true;
    }
    if (files.empty()) {
        return calcHashSingle();
    } else {
        return calcHashMulti();
    }
}

void TorrentCreator::run()
{
    if (hashes.empty()) {
        while (!stopped && !calculateHash())
            ;
    }
}

std::unique_ptr<TorrentControl> TorrentCreator::makeTC(const QString &data_dir)
{
    QString dd = data_dir;
    if (!dd.endsWith(bt::DirSeparator())) {
        dd += bt::DirSeparator();
    }

    // make data dir if necessary
    if (!bt::Exists(dd)) {
        bt::MakeDir(dd);
    }

    // save the torrent
    saveTorrent(dd + QLatin1String("torrent"));
    // write full index file
    File fptr;
    if (!fptr.open(dd + QLatin1String("index"), u"wb"_s)) {
        throw Error(i18n("Cannot create index file: %1", fptr.errorString()));
    }

    for (Uint32 i = 0; i < num_chunks; i++) {
        NewChunkHeader hdr;
        hdr.index = i;
        fptr.write(&hdr, sizeof(NewChunkHeader));
    }
    fptr.close();

    // now create the torrentcontrol object
    auto tc = std::make_unique<TorrentControl>();
    try {
        // get the parent dir of target
        const QFileInfo fi = QFileInfo(target);

        QString odir;
        StatsFile st(dd + QLatin1String("stats"));
        if (fi.fileName() == name) {
            st.write(u"OUTPUTDIR"_s, fi.path());
            odir = fi.path();
        } else {
            st.write(u"CUSTOM_OUTPUT_NAME"_s, u"1"_s);
            st.write(u"OUTPUTDIR"_s, target);
            odir = target;
        }
        st.write(u"UPLOADED"_s, u"0"_s);
        st.write(u"RUNNING_TIME_DL"_s, u"0"_s);
        st.write(u"RUNNING_TIME_UL"_s, u"0"_s);
        st.write(u"PRIORITY"_s, u"0"_s);
        st.write(u"AUTOSTART"_s, u"1"_s);
        st.write(u"IMPORTED"_s, QString::number(tot_size));
        st.sync();

        tc->init(nullptr, bt::LoadFile(dd + "torrent"_L1), dd, odir);
        tc->createFiles();
    } catch (...) {
        bt::Delete(dd, true);
        throw;
    }

    return tc;
}
}

#include "moc_torrentcreator.cpp"
