#include "ytjssearch.h"

#include "mainwindow.h"
#include "searchparams.h"
#include "video.h"
#include "ytsearch.h"

#include "js.h"

namespace {

int parseDuration(const QString &s) {
    const auto parts = s.splitRef(':');
    int secs = 0;
    int p = 0;
    for (auto i = parts.crbegin(); i != parts.crend(); ++i) {
        if (p == 0) {
            secs = i->toInt();
        } else if (p == 1) {
            secs += i->toInt() * 60;
        } else if (p == 2) {
            secs += i->toInt() * 60 * 60;
        }
        p++;
    }
    return secs;
}

QString parseChannelId(const QString &channelUrl) {
    int pos = channelUrl.lastIndexOf('/');
    if (pos >= 0) return channelUrl.mid(pos + 1);
    return QString();
}

QDateTime parsePublishedText(const QString &s) {
    int num = 0;
    const auto parts = s.splitRef(' ');
    for (const auto &part : parts) {
        num = part.toInt();
        if (num > 0) break;
    }
    if (num == 0) return QDateTime();

    auto now = QDateTime::currentDateTimeUtc();
    if (s.contains("hour")) {
        return now.addSecs(-num * 3600);
    } else if (s.contains("day")) {
        return now.addDays(-num);
    } else if (s.contains("week")) {
        return now.addDays(-num * 7);
    } else if (s.contains("month")) {
        return now.addMonths(-num);
    } else if (s.contains("year")) {
        return now.addDays(-num * 365);
    }
    return QDateTime();
}

} // namespace

YTJSSearch::YTJSSearch(SearchParams *searchParams, QObject *parent)
    : VideoSource(parent), searchParams(searchParams) {}

void YTJSSearch::loadVideos(int max, int startIndex) {
    aborted = false;

    auto &js = JS::instance();
    auto &engine = js.getEngine();

    QString q;
    if (!searchParams->keywords().isEmpty()) {
        if (searchParams->keywords().startsWith("http://") ||
            searchParams->keywords().startsWith("https://")) {
            q = YTSearch::videoIdFromUrl(searchParams->keywords());
        } else
            q = searchParams->keywords();
    }

    // Options

    QJSValue options = engine.newObject();

    if (startIndex > 1) {
        if (!nextpageRef.isEmpty())
            options.setProperty("nextpageRef", nextpageRef);
        else {
            // non-first page was requested but we have no continuation token
            emit error("No pagination token");
            return;
        }
    }
    options.setProperty("limit", max);

    switch (searchParams->safeSearch()) {
    case SearchParams::None:
        options.setProperty("safeSearch", false);
        break;
    case SearchParams::Strict:
        options.setProperty("safeSearch", true);
        break;
    }

    // Filters

    auto filterMap = engine.evaluate("new Map()");
    auto jsMapSet = filterMap.property("set");
    auto addFilter = [&filterMap, &jsMapSet](QString name, QString value) {
        jsMapSet.callWithInstance(filterMap, {name, value});
    };

    // addFilter("Type", "Video");

    switch (searchParams->sortBy()) {
    case SearchParams::SortByNewest:
        addFilter("Sort by", "Upload date");
        break;
    case SearchParams::SortByViewCount:
        addFilter("Sort by", "View count");
        break;
    case SearchParams::SortByRating:
        addFilter("Sort by", "Rating");
        break;
    }

    switch (searchParams->duration()) {
    case SearchParams::DurationShort:
        addFilter("Duration", "Short");
        break;
    case SearchParams::DurationMedium:
        addFilter("Duration", "Medium");
        break;
    case SearchParams::DurationLong:
        addFilter("Duration", "Long");
        break;
    }

    switch (searchParams->time()) {
    case SearchParams::TimeToday:
        addFilter("Upload date", "Today");
        break;
    case SearchParams::TimeWeek:
        addFilter("Upload date", "This week");
        break;
    case SearchParams::TimeMonth:
        addFilter("Upload date", "This month");
        break;
    case SearchParams::TimeYear:
        addFilter("Upload date", "This year");
        break;
    }

    switch (searchParams->quality()) {
    case SearchParams::QualityHD:
        addFilter("Features", "HD");
        break;
    case SearchParams::Quality4K:
        addFilter("Features", "4K");
        break;
    case SearchParams::QualityHDR:
        addFilter("Features", "HDR");
        break;
    }

    js.callFunction(new JSResult(this), "search", {q, options, filterMap})
            .onJson([this](auto &doc) {
                if (aborted) return;

                auto obj = doc.object();

                nextpageRef = obj["nextpageRef"].toString();

                const auto items = obj["items"].toArray();
                QVector<Video *> videos;
                videos.reserve(items.size());

                for (const auto &i : items) {
                    QString type = i["type"].toString();
                    if (type != "video") continue;

                    Video *video = new Video();

                    QString id = YTSearch::videoIdFromUrl(i["link"].toString());
                    video->setId(id);

                    QString title = i["title"].toString();
                    video->setTitle(title);

                    QString desc = i["description"].toString();
                    video->setDescription(desc);

                    const auto thumbs = i["thumbnails"].toArray();
                    for (const auto &t : thumbs) {
                        video->addThumb(t["width"].toInt(), t["height"].toInt(),
                                        t["url"].toString());
                    }

                    int views = i["views"].toInt();
                    video->setViewCount(views);

                    int duration = parseDuration(i["duration"].toString());
                    video->setDuration(duration);

                    auto published = parsePublishedText(i["uploaded_at"].toString());
                    if (published.isValid()) video->setPublished(published);

                    auto authorObj = i["author"];
                    QString channelName = authorObj["name"].toString();
                    video->setChannelTitle(channelName);
                    QString channelId = parseChannelId(authorObj["ref"].toString());
                    video->setChannelId(channelId);

                    videos << video;
                }

                if (videos.isEmpty()) {
                    emit error("No results");
                } else {
                    emit gotVideos(videos);
                    emit finished(videos.size());
                }
            })
            .onError([this, max, startIndex](auto &msg) {
                static int retries = 0;
                if (retries < 3) {
                    qDebug() << "Retrying...";
                    QTimer::singleShot(0, this,
                                       [this, max, startIndex] { loadVideos(max, startIndex); });
                    retries++;
                } else {
                    emit error(msg);
                }
            });
}

QString YTJSSearch::getName() {
    if (!name.isEmpty()) return name;
    if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
    return QString();
}

const QList<QAction *> &YTJSSearch::getActions() {
    static const QList<QAction *> channelActions = {
            MainWindow::instance()->getAction("subscribeChannel")};
    if (searchParams->channelId().isEmpty()) {
        static const QList<QAction *> noActions;
        return noActions;
    }
    return channelActions;
}
