/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdint.h>
#include <iostream>

#include "ignite/ignite.h"
#include "ignite/ignition.h"

#include "ignite/examples/organization.h"
#include "ignite/examples/person.h"

using namespace ignite;
using namespace cache;
using namespace query;

using namespace examples;

/** Organizations cache name. */
const char* ORG_CACHE = "Organization";

/** Organizations type name. */
const char* ORG_TYPE = "Organization";

/** Persons cache name. */
const char* PERSON_CACHE = "Person";

/** Persons type name. */
const char* PERSON_TYPE = "Person";

/**
 * Example for SQL fields queries based on all employees working for a specific
 * organization (query uses distributed join).
 *
 * Note that SQL Fields Query can only be performed using fields that have been
 * listed in "QueryEntity" been of the config.
 */
void DoSqlQueryWithDistributedJoin()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // SQL clause query which joins on 2 types to select people for a specific organization.
    SqlFieldsQuery qry(
        "select firstName, lastName from Person "
        "inner join \"Organization\".Organization as org "
        "where Person.orgId = org._key "
        "and lower(org.name) = lower(?)");

    qry.AddArgument<std::string>("ApacheIgnite");

    // Enable distributed joins for query.
    qry.SetDistributedJoins(true);

    QueryFieldsCursor cursor = cache.Query(qry);

    // Printing first result set.
    std::cout << "Following people are 'ApacheIgnite' employees (distributed join): " << std::endl;

    while (cursor.HasNext())
    {
        QueryFieldsRow row = cursor.GetNext();

        std::cout << row.GetNext<std::string>() << " " << row.GetNext<std::string>() << std::endl;
    }

    std::cout << std::endl;

    qry.ClearArguments();

    qry.AddArgument<std::string>("Other");

    cursor = cache.Query(qry);

    // Printing second result set.
    std::cout << "Following people are 'Other' employees (distributed join): " << std::endl;

    while (cursor.HasNext())
    {
        QueryFieldsRow row = cursor.GetNext();

        std::cout << row.GetNext<std::string>() << " " << row.GetNext<std::string>() << std::endl;
    }

    std::cout << std::endl;
}

/**
 * Example for SQL-based fields queries that return only person name and company it works in.
 *
 * Note that SQL Fields Query can only be performed using fields that have been
 * listed in "QueryEntity" been of the config.
 */
void DoSqlQueryWithJoin()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // Execute query to get names of all employees.
    std::string sql(
        "select concat(firstName, ' ', lastName), org.name "
        "from Person, \"Organization\".Organization as org "
        "where Person.orgId = org._key");

    QueryFieldsCursor cursor = cache.Query(SqlFieldsQuery(sql));

    // Print persons' names and organizations' names.
    std::cout << "Names of all employees and organizations they belong to: " << std::endl;

    // In this particular case each row will have two elements with full name
    // of an employees and their organization.
    while (cursor.HasNext())
    {
        QueryFieldsRow row = cursor.GetNext();

        std::cout << row.GetNext<std::string>() << " is working in ";
        std::cout << row.GetNext<std::string>() << std::endl;
    }

    std::cout << std::endl;
}

/**
 * Example for SQL-based fields queries that uses SQL functions.
 *
 * Note that SQL Fields Query can only be performed using fields that have been
 * listed in "QueryEntity" been of the config.
 */
void DoSqlQueryWithFunction()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // Execute query to get names of all employees.
    QueryFieldsCursor cursor = cache.Query(SqlFieldsQuery(
        "select concat(firstName, ' ', lastName) from Person"));

    // Print names.
    std::cout << "Names of all employees: " << std::endl;

    // In this particular case each row will have one element with full name of an employees.
    while (cursor.HasNext())
        std::cout << cursor.GetNext().GetNext<std::string>() << std::endl;

    std::cout << std::endl;
}

/**
 * Example for SQL queries to calculate average salary for a specific organization.
 *
 * Note that SQL Fields Query can only be performed using fields that have been
 * listed in "QueryEntity" been of the config.
 */
void DoSqlQueryWithAggregation()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // Calculate average of salary of all persons in ApacheIgnite.
    // Note that we also join on Organization cache as well.
    SqlFieldsQuery qry(
        "select avg(salary) "
        "from Person, \"Organization\".Organization as org "
        "where Person.orgId = org._key "
        "and lower(org.name) = lower(?)");

    qry.AddArgument<std::string>("ApacheIgnite");

    QueryFieldsCursor cursor = cache.Query(qry);

    // Calculate average salary for a specific organization.
    std::cout << "Average salary for 'ApacheIgnite' employees: " << std::endl;

    while (cursor.HasNext())
        std::cout << cursor.GetNext().GetNext<double>() << std::endl;

    std::cout << std::endl;
}

/**
 * Example for SQL queries based on salary ranges.
 *
 * Note that SQL Query can only be performed using fields that have been
 * listed in "QueryEntity" been of the config.
 */
void DoSqlQuery()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // SQL clause which selects salaries based on range.
    SqlFieldsQuery qry(
        "select firstName, lastName, salary "
        "from Person "
        "where salary > ? and salary <= ?");

    // Execute queries for salary range 0 - 1000.
    std::cout << "People with salaries between 0 and 1000 (queried with SQL query): " << std::endl;

    qry.AddArgument(0);
    qry.AddArgument(1000);

    QueryFieldsCursor cursor = cache.Query(qry);

    while (cursor.HasNext())
    {
        QueryFieldsRow row = cursor.GetNext();

        std::cout
            << row.GetNext<std::string>() << " "
            << row.GetNext<std::string>() << " : "
            << row.GetNext<double>()
            << std::endl;
    }

    std::cout << std::endl;

    qry.ClearArguments();

    // Execute queries for salary range 1000 - 2000.
    std::cout << "People with salaries between 1000 and 2000 (queried with SQL query): " << std::endl;

    qry.AddArgument(1000);
    qry.AddArgument(2000);

    cursor = cache.Query(qry);

    while (cursor.HasNext())
    {
        QueryFieldsRow row = cursor.GetNext();

        std::cout
            << row.GetNext<std::string>() << " "
            << row.GetNext<std::string>() << " : "
            << row.GetNext<double>()
            << std::endl;
    }

    std::cout << std::endl;
}

/**
 * Example for TEXT queries using LUCENE-based indexing of people's resumes.
 *
 * Note that to be able to do so you have to add FULLTEXT index for the 'resume'
 * field of the Person type. See config for details.
 */
void DoTextQuery()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    typedef std::vector< CacheEntry<PersonKey, Person> > ResVector;

    //  Query for all people with "Master" in their resumes.
    ResVector masters;
    cache.Query(TextQuery(PERSON_TYPE, "Master")).GetAll(masters);

    // Query for all people with "Bachelor" in their resumes.
    ResVector bachelors;
    cache.Query(TextQuery(PERSON_TYPE, "Bachelor")).GetAll(bachelors);

    std::cout << "Following people have 'Master' in their resumes: " << std::endl;

    // Printing first result set.
    for (ResVector::const_iterator i = masters.begin(); i != masters.end(); ++i)
        std::cout << i->GetKey().ToString() << " : " << i->GetValue().ToString() << std::endl;

    std::cout << std::endl;

    std::cout << "Following people have 'Bachelor' in their resumes: " << std::endl;

    // Printing second result set.
    for (ResVector::const_iterator i = bachelors.begin(); i != bachelors.end(); ++i)
        std::cout << i->GetKey().ToString() << " : " << i->GetValue().ToString() << std::endl;

    std::cout << std::endl;
}

/**
 * Example for scan query based on a predicate.
 */
void DoScanQuery()
{
    Cache<PersonKey, Person> cache = Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    ScanQuery scan;

    typedef std::vector< CacheEntry<PersonKey, Person> > ResVector;

    ResVector res;
    cache.Query(scan).GetAll(res);

    // Execute queries for salary ranges.
    std::cout << "People with salaries between 0 and 1000 (queried with SCAN query): " << std::endl;

    for (ResVector::const_iterator i = res.begin(); i != res.end(); ++i)
    {
        Person person(i->GetValue());

        if (person.salary <= 1000)
            std::cout << i->GetKey().ToString() << " : " << person.ToString() << std::endl;
    }

    std::cout << std::endl;
}

/**
 * Populate cache with test data.
 */
void Initialize()
{
    Cache<int64_t, Organization> orgCache =
        Ignition::Get().GetCache<int64_t, Organization>(ORG_CACHE);

    // Clear cache before running the example.
    orgCache.Clear();

    // Organizations.
    Organization org1("ApacheIgnite");
    Organization org2("Other");

    const int64_t org1Id = 1;
    const int64_t org2Id = 2;

    orgCache.Put(org1Id, org1);
    orgCache.Put(org2Id, org2);

    Cache<PersonKey, Person> personCache =
        Ignition::Get().GetCache<PersonKey, Person>(PERSON_CACHE);

    // Clear cache before running the example.
    personCache.Clear();

    // People.

    // Collocated by 1st organisation:
    Person p1(org1Id, "John", "Doe", "John Doe has Master Degree.", 2000);
    Person p2(org1Id, "Jane", "Doe", "Jane Doe has Bachelor Degree.", 1000);

    PersonKey pKey1 (1, org1Id);
    PersonKey pKey2 (2, org1Id);

    // Collocated by second organisation:
    Person p3(org2Id, "John", "Smith", "John Smith has Bachelor Degree.", 1000);
    Person p4(org2Id, "Jane", "Smith", "Jane Smith has Master Degree.", 2000);

    PersonKey pKey3 (3, org2Id);
    PersonKey pKey4 (4, org2Id);

    // Note that in this example we use custom affinity key for Person objects
    // to ensure that all persons are collocated with their organizations.
    personCache.Put(pKey1, p1);
    personCache.Put(pKey2, p2);
    personCache.Put(pKey3, p3);
    personCache.Put(pKey4, p4);
}

int main()
{
    try
    {
        IgniteConfiguration cfg;
        cfg.springCfgPath = "platforms/cpp/examples/query-example/config/query-example.xml";

        // Start a node.
        Ignite ignite = Ignition::Start(cfg);

        std::cout << std::endl;
        std::cout << ">>> Cache query example started." << std::endl;
        std::cout << std::endl;

        // Get organization cache instance.
        Cache<int64_t, Organization> orgCache = ignite.GetCache<int64_t, Organization>(ORG_CACHE);

        // Get person cache instance.
        Cache<PersonKey, Person> personCache = ignite.GetCache<PersonKey, Person>(PERSON_CACHE);

        // Populate cache.
        Initialize();

        // Example for SCAN-based query based on a predicate.
        DoScanQuery();

        // Example for TEXT-based querying for a given string in peoples resumes.
        DoTextQuery();

        // Example for SQL-based querying employees based on salary ranges.
        DoSqlQuery();

        // Example for SQL-based querying employees for a given organization (includes SQL join).
        DoSqlQueryWithJoin();

        // Example for SQL-based querying employees for a given organization (includes distributed SQL join).
        DoSqlQueryWithDistributedJoin();

        // Example for SQL-based fields queries that uses SQL functions.
        DoSqlQueryWithFunction();

        // Example for SQL-based querying to calculate average salary among all employees within a company.
        DoSqlQueryWithAggregation();

        // Stop node.
        Ignition::StopAll(false);

        std::cout << std::endl;
        std::cout << ">>> Example finished, press 'Enter' to exit ..." << std::endl;
        std::cout << std::endl;

        std::cin.get();
    }
    catch (IgniteError& err)
    {
        std::cout << "An error occurred: " << err.GetText() << std::endl;

        return err.GetCode();
    }

    return 0;
}
