/*
 * 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/UnityExceptions.h>
#include <unity/scopes/VariantBuilder.h>

#include "scopes-scope.h"
#include "resultcollector.h"
#include "i18n.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": "subtitle",
    "mascot":  "mascot",
    "background": "background"
  }
}
)";
// unconfuse emacs: "
static const char SEARCH_CATEGORY_DEFINITION[] = R"(
{
  "schema-version": 1,
  "template": {
    "overlay": true,
    "card-size": "small"
  },
  "components": {
    "title": "title",
    "art": {
      "field": "art",
      "aspect-ratio": 0.6
    }
  }
}
)";

void ScopesScope::start(std::string const&) {
    setlocale(LC_ALL, "");
    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;
    }
}

void ScopesScope::stop() {
}

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, hints));
    return query;
}

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

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

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 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);
    }
}

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

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

    std::shared_ptr<ResultCollector> online_query;
    std::list<CategorisedResult> online_results;

    bool seen_recommended = false;
    std::list<CategorisedResult> before_recommended, online_recommendations, after_recommended;
    std::set<std::string> recommended_scope_ids;

    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") {
            seen_recommended = true;
            try
            {
                auto const canned_query = CannedQuery::from_uri(result.uri());
                auto const scope_id = canned_query.scope_id();
                recommended_scope_ids.insert(scope_id);
                online_recommendations.push_back(result);
            }
            catch (const unity::InvalidArgumentException &err)
            {
                std::cerr << "Cannot parse '" << result.uri() << "' as canned query: " << err.what() << std::endl;
            }
            catch (...)
            {
                std::cerr << "Cannot parse '" << result.uri() << "' as canned query" << std::endl;
            }
        } else {
            if (seen_recommended) {
                after_recommended.push_back(result);
            } else {
                before_recommended.push_back(result);
            }
        }
    }

    const MetadataMap all_scopes = scope.registry()->list();
    std::vector<ScopeMetadata> local_results;
    for (const auto &pair : all_scopes) {
        const auto &item = pair.second;
        if (item.invisible()) {
            continue;
        }
        // Favour online results.
        if (recommended_scope_ids.count(item.scope_id()) != 0) {
            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) {
            local_results.push_back(item);
        }
    }
    std::sort(local_results.begin(), local_results.end(), compareMetadata);

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

    // Push results that came before the recommended category
    for (const auto &result : before_recommended) {
        reply->push(result);
    }
    for (const auto &item : local_results) {
        pushed_scope_ids.insert(item.scope_id());
        push_scope_result(reply, item, category);
    }
    for (const auto &result : online_recommendations) {
        const auto scope_id = CannedQuery::from_uri(result.uri()).scope_id();
        // Skip results that are not installed.
        if (all_scopes.count(scope_id) == 0) {
            continue;
        }
        pushed_scope_ids.insert(scope_id);
        reply->push(result);
    }
    // Now push other results from the online scope
    for (const auto &result : after_recommended) {
        reply->push(result);
    }

    // second round, wait for the server results without any time limits
    if (online_query) {
        online_query->wait_until_finished();
        online_results = online_query->take_results();
        for (auto const& result : online_results) {
            if (result.category()->id() == "recommendations") {
                try
                {
                    auto const canned_query = CannedQuery::from_uri(result.uri());
                    auto const scope_id = canned_query.scope_id();
                    // Ignore if we've already pushed out this scope,
                    // or if the scope is not installed.
                    if (pushed_scope_ids.count(scope_id) != 0 ||
                        all_scopes.count(scope_id) == 0) {
                        continue;
                    }
                    reply->push(result);
                }
                catch (const unity::InvalidArgumentException &err)
                {
                    std::cerr << "Cannot parse '" << result.uri() << "' as canned query: " << err.what() << std::endl;
                }
                catch (...)
                {
                    std::cerr << "Cannot parse '" << result.uri() << "' as canned query" << std::endl;
                }
            } else {
                reply->push(result);
            }
        }
    }
}

void ScopesQuery::push_scope_result(SearchReplyProxy const &reply, ScopeMetadata const& item, Category::SCPtr const& category)
{
    CategorisedResult result(category);

    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["subtitle"] = item.author();
    result["description"] = item.description();
    const std::string uri = CannedQuery(item.scope_id()).to_uri();
    result.set_uri(uri);
    result.set_dnd_uri(uri);
    std::string icon;
    try {
        icon = item.icon();
    } catch (NotFoundException &e) {
    }
    if (icon.empty()) {
        result["mascot"] = MISSING_ICON;
    } else {
        result["mascot"] = icon;
    }

    try {
        result["background"] = item.appearance_attributes().at("background");
    } catch (const std::out_of_range &e) {
    }
    try {
        result["overlay-color"] = item.appearance_attributes().at("logo-overlay-color");
    } catch (const std::out_of_range &e) {
    }
    reply->push(result);
}

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

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");

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

    PreviewWidget description("description", "text");
    description.add_attribute_mapping("text", "description");
    description.add_attribute_value("title", Variant(_("Description")));

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

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

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

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