/*
 * Copyright (C) 2014 Canonical, Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: James Henstridge <james.henstridge@canonical.com>
 */

#include <algorithm>
#include <iostream>
#include <map>
#include <set>

#include <unity/scopes/Category.h>
#include <unity/scopes/CategorisedResult.h>
#include <unity/scopes/PreviewReply.h>
#include <unity/scopes/PreviewWidget.h>
#include <unity/scopes/Registry.h>
#include <unity/scopes/ScopeExceptions.h>
#include <unity/scopes/VariantBuilder.h>

#include "scopes-scope.h"
#include "resultcollector.h"

using namespace unity::scopes;

static const char ONLINE_SCOPE_ID[] = "com.canonical.scopes.onlinescopes";

static const char MISSING_ICON[] = "/usr/share/icons/unity-icon-theme/places/svg/service-generic.svg";
static const char SCOPES_CATEGORY_DEFINITION[] = R"(
{
  "schema-version": 1,
  "template": {
    "category-layout": "grid",
    "card-size": "medium",
    "card-background": "color:///#E9E9E9"
  },
  "components": {
    "title": "title",
    "subtitle": "author",
    "mascot":  "icon",
    "background": "background"
  }
}
)";
// unconfuse emacs: "
static const char SEARCH_CATEGORY_DEFINITION[] = R"(
{
  "schema-version": 1,
  "template": {
    "category-layout": "grid",
    "card-size": "small",
    "card-layout": "horizontal"
  },
  "components": {
    "title": "title",
    "subtitle": "author",
    "mascot":  "icon"
  }
}
)";

int ScopesScope::start(std::string const&, RegistryProxy const &registry) {
    this->registry = registry;
    try {
        online_scope = registry->get_metadata(ONLINE_SCOPE_ID).proxy();
    } catch (std::exception &e) {
        std::cerr << "Could not instantiate online scopes scope: " << e.what() << std::endl;
    }
    return VERSION;
}

void ScopesScope::stop() {
    registry.reset();
}

SearchQueryBase::UPtr ScopesScope::search(CannedQuery const &q,
                                          SearchMetadata const &hints) {
    // FIXME: workaround for problem with no remote scopes on first run
    // until network becomes available
    if (online_scope == nullptr)
    {
        try
        {
            online_scope = registry->get_metadata(ONLINE_SCOPE_ID).proxy();
        } catch(std::exception &e)
        {
            // silently ignore
        }
    }

    SearchQueryBase::UPtr query(new ScopesQuery(*this, q));
    return query;
}

PreviewQueryBase::UPtr ScopesScope::preview(Result const &result,
                                     ActionMetadata const &hints) {
    PreviewQueryBase::UPtr previewer(new ScopesPreview(*this, result));
    return previewer;
}

ActivationQueryBase::UPtr ScopesScope::activate(Result const &result, ActionMetadata const& hints) {
    ActivationQueryBase::UPtr activation(new ScopesActivation(result));
    return activation;
}

ActivationQueryBase::UPtr ScopesScope::perform_action(Result const &result, ActionMetadata const& hints, std::string const &widget_id, std::string const &action_id) {
    ActivationQueryBase::UPtr activation(new ScopesActivation(result));
    return activation;
}

ScopesQuery::ScopesQuery(ScopesScope &scope, CannedQuery const &query)
    : scope(scope), query(query) {
}

void ScopesQuery::cancelled() {
}

static std::string lowercase(std::string s) {
    std::transform(s.begin(), s.end(), s.begin(), ::tolower);
    return s;
}

enum ScopeCategory {
    CAT_FEATURED,
    CAT_ENTERTAINMENT,
    CAT_OTHER,
    N_CATEGORIES
};

static const std::map<std::string,ScopeCategory> category_mapping {
    {"com.canonical.scopes.amazon", CAT_FEATURED},
    {"com.canonical.scopes.ebay", CAT_FEATURED},
    {"com.canonical.scopes.grooveshark", CAT_ENTERTAINMENT},
    {"com.canonical.scopes.weatherchannel", CAT_FEATURED},
    {"com.canonical.scopes.wikipedia", CAT_FEATURED},
    {"musicaggregator", CAT_ENTERTAINMENT},
    {"videoaggregator", CAT_ENTERTAINMENT},
    {"clickscope", CAT_ENTERTAINMENT},
};

static const std::map<std::string,std::string> background_mapping {
    {"com.canonical.scopes.amazon", "color:///#FFFFFF"},
    {"com.canonical.scopes.ebay", "color:///#F4F4F4"},
    {"com.canonical.scopes.grooveshark", "color:///#F67F00"},
    {"com.canonical.scopes.weatherchannel", "color:///#244AA5"},
    {"com.canonical.scopes.wikipedia", "color:///#F6F6F6"},
};

static bool compareMetadata(ScopeMetadata const &item1, ScopeMetadata const &item2) {
    ScopeCategory cat1, cat2;
    try {
        cat1 = category_mapping.at(item1.scope_id());
    } catch (std::out_of_range &e) {
        cat1 = CAT_OTHER;
    }
    try {
        cat2 = category_mapping.at(item2.scope_id());
    } catch (std::out_of_range &e) {
        cat2 = CAT_OTHER;
    }

    // If categories differ, we're done.
    if (cat1 != cat2) {
        return cat1 < cat2;
    }

    std::string name1, name2;
    try {
        name1 = item1.display_name();
    } catch (NotFoundException &e) {
        name1 = item1.scope_id();
    }
    try {
        name2 = item2.display_name();
    } catch (NotFoundException &e) {
        name2 = item2.scope_id();
    }

    return name1 < name2;
}

void ScopesQuery::run(SearchReplyProxy const &reply) {
    if (query.query_string().empty()) {
        surfacing_query(reply);
    } else {
        search_query(reply);
    }
}

void ScopesQuery::surfacing_query(SearchReplyProxy const &reply) {
    CategoryRenderer renderer(SCOPES_CATEGORY_DEFINITION);
    Category::SCPtr categories[N_CATEGORIES];
    categories[CAT_FEATURED] = reply->register_category(
        "popular", "Featured", "", renderer);
    categories[CAT_ENTERTAINMENT] = reply->register_category(
        "entertainment", "Entertainment", "", renderer);
    categories[CAT_OTHER] = reply->register_category(
        "scopes", "Other", "", renderer);

    MetadataMap all_scopes = scope.registry->list();
    std::vector<ScopeMetadata> scopes;
    for (const auto &pair : all_scopes) {
        const auto &item = pair.second;
        if (item.invisible())
            continue;
        scopes.push_back(item);
    }
    std::sort(scopes.begin(), scopes.end(), compareMetadata);

    for (const auto &item : scopes) {
        // TODO: categorisation of scopes should come from the
        // metadata rather than being hard coded.
        Category::SCPtr category;
        try {
            ScopeCategory cat_id = category_mapping.at(item.scope_id());
            category = categories[cat_id];
        } catch (std::out_of_range &e) {
            category = categories[CAT_OTHER];
        }
        push_scope_result(reply, item, category, nullptr);
    }
}

void ScopesQuery::search_query(SearchReplyProxy const &reply) {
    CategoryRenderer renderer(SEARCH_CATEGORY_DEFINITION);
    Category::SCPtr category = reply->register_category(
        "scopes", "Scopes", "", renderer);

    std::string term = lowercase(query.query_string());

    std::shared_ptr<ResultCollector> online_query;
    std::map<std::string, std::vector<Result>> recommended_scopes;
    std::list<CategorisedResult> online_results;

    if (scope.online_scope && !query.query_string().empty()) {
        online_query.reset(new ResultCollector);
        try {
            subsearch(scope.online_scope, query.query_string(), online_query);
            // give the server a second before we display the results
            bool finished = online_query->wait_until_finished(1000);
            online_results = online_query->take_results();
            if (finished) online_query.reset();
        } catch (...) {
            online_query.reset();
        }
    }

    // process online results, push smart results right away
    for (auto const& result : online_results) {
        if (result.category()->id() == "recommendations") {
            if (result.contains("scope_id")) {
                auto scope_id = result["scope_id"].get_string();
                recommended_scopes[scope_id].push_back(result);
            }
        } else {
            if (!reply->lookup_category(result.category()->id())) {
                reply->register_category(result.category());
            }
            reply->push(result);
        }
    }

    MetadataMap all_scopes = scope.registry->list();
    std::vector<ScopeMetadata> scopes;
    for (const auto &pair : all_scopes) {
        const auto &item = pair.second;
        if (item.invisible())
            continue;
        std::string display_name, description;
        try {
            display_name = lowercase(item.display_name());
        } catch (NotFoundException &e) {
        }
        try {
            description = lowercase(item.description());
        } catch (NotFoundException &e) {
        }
        if (display_name.find(term) != std::string::npos ||
            description.find(term) != std::string::npos ||
            recommended_scopes.find(item.scope_id()) != recommended_scopes.end()) {
            scopes.push_back(item);
        }
    }
    std::sort(scopes.begin(), scopes.end(), compareMetadata);

    std::set<std::string> pushed_scope_ids;

    for (const auto &item : scopes) {
        // don't display ourselves
        if (item.scope_id() == "scopes") continue;

        pushed_scope_ids.insert(item.scope_id());
        auto it = recommended_scopes.find(item.scope_id());
        if (it == recommended_scopes.end()) {
            push_scope_result(reply, item, category, nullptr);
        } else {
            for (auto &online_result : it->second) {
                push_scope_result(reply, item, category, &online_result);
            }
        }
    }

    // second round, wait for the server results without any time limits
    if (online_query) {
        recommended_scopes.clear();
        online_query->wait_until_finished();
        online_results = online_query->take_results();
        for (auto const& result : online_results) {
            if (result.category()->id() == "recommendations") {
                if (result.contains("scope_id")) {
                    auto scope_id = result["scope_id"].get_string();
                    if (pushed_scope_ids.find(scope_id) != pushed_scope_ids.end())
                        continue;
                    recommended_scopes[scope_id].push_back(result);
                }
            } else {
                if (!reply->lookup_category(result.category()->id())) {
                    reply->register_category(result.category());
                }
                reply->push(result);
            }
        }

        for (auto const& pair : recommended_scopes) {
            // get iterator to ScopeMetadata
            auto meta_it = all_scopes.find(pair.first);
            if (meta_it == all_scopes.end()) continue;

            auto const& item = meta_it->second;
            // push more results
            pushed_scope_ids.insert(item.scope_id());
            for (auto &online_result : pair.second) {
                push_scope_result(reply, item, category, &online_result);
            }
        }
    }
}

void ScopesQuery::push_scope_result(SearchReplyProxy const &reply, ScopeMetadata const& item, Category::SCPtr const& category, Result const *online_result)
{
    std::string uri = "scope://" + item.scope_id();
    CategorisedResult result(category);
    // Tell the shell to ask us to activate results.
    result.set_intercept_activation();
    result.set_uri(uri);
    try {
        result.set_title(item.display_name());
    } catch (NotFoundException &e) {
        result.set_title(item.scope_id());
    }
    try {
        result.set_art(item.art());
    } catch (NotFoundException &e) {
    }
    result.set_dnd_uri(uri);
    result["scope_id"] = item.scope_id();
    result["author"] = item.author();
    result["description"] = item.description();
    if (online_result != nullptr) {
        if (online_result->contains("subtitle")) {
            const auto subtitle = (*online_result)["subtitle"].get_string();
            if (!subtitle.empty()) {
                result["author"] = (*online_result)["subtitle"];
            }
        }
        if (online_result->contains("search_string")) {
            result["search_string"] = (*online_result)["search_string"];
        }
        if (online_result->contains("scope_department")) {
            result["scope_department"] = (*online_result)["scope_department"];
        }
    }
    std::string icon;
    try {
        icon = item.icon();
    } catch (NotFoundException &e) {
    }
    if (icon.empty()) {
        result["icon"] = MISSING_ICON;
    } else {
        result["icon"] = icon;
    }
    try {
        result["background"] = background_mapping.at(item.scope_id());
    } catch (std::out_of_range &e) {
    }
    reply->push(result);
}

ScopesPreview::ScopesPreview(ScopesScope &scope, Result const &result)
    : scope(scope), result(result) {
}

void ScopesPreview::cancelled() {
}

void ScopesPreview::run(PreviewReplyProxy const &reply) {
    ColumnLayout layout1col(1), layout2col(2), layout3col(3);
    layout1col.add_column({"art", "header", "actions", "description"});

    layout2col.add_column({"art"});
    layout2col.add_column({"header", "actions", "description"});

    layout3col.add_column({"art"});
    layout3col.add_column({"header", "actions", "description"});
    layout3col.add_column({});
    reply->register_layout({layout1col, layout2col, layout3col});

    PreviewWidget header("header", "header");
    header.add_attribute_mapping("title", "title");
    header.add_attribute_mapping("subtitle", "author");
    header.add_attribute_mapping("mascot", "icon");

    PreviewWidget art("art", "image");
    art.add_attribute_mapping("source", "art");

    PreviewWidget description("description", "text");
    description.add_attribute_mapping("text", "description");

    PreviewWidget actions("actions", "actions");
    {
        VariantBuilder builder;
        builder.add_tuple({
                {"id", Variant("search")},
                {"label", Variant("Search")}
            });
        actions.add_attribute_value("actions", builder.end());
    }

    reply->push({art, header, actions, description});
}

ScopesActivation::ScopesActivation(Result const &result)
    : result(result) {
}

ActivationResponse ScopesActivation::activate() {
    std::string scope_id = result["scope_id"].get_string();
    std::string search_string;
    std::string department_id;
    if (result.contains("search_string")) {
        search_string = result["search_string"].get_string();
    }
    if (result.contains("scope_department")) {
        department_id = result["scope_department"].get_string();
    }

    CannedQuery query(scope_id, search_string, department_id);
    return ActivationResponse(query);
}

extern "C" ScopeBase *UNITY_SCOPE_CREATE_FUNCTION() {
    return new ScopesScope;
}

extern "C" void UNITY_SCOPE_DESTROY_FUNCTION(ScopeBase *scope) {
    delete scope;
}
