/**
 *  Our goal is to manage a Student class. Student has basic id, name and roll_number and one more thing:
 *  vector of marks. Mark is a subentity with one to many relation. This example shows how to manage this kind of case.
 *  First of all we got to understand how to keep data in the db. We need two tables: `students` and `marks`. Students
 *  table has column equal to all Student class members exept marks. Marks table has two columns: student_id and value
 *  itself. We create two functions here: inserting/updating student (with his/her marks) and getting student (also with
 * 	his/her marks). Schema is: `CREATE TABLE students (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, roll_no
 *  INTEGER NOT NULL)` `CREATE TABLE marks (mark INTEGER NOT NULL, student_id INTEGER NOT NULL)` One of the main ideas of
 *  `sqlite_orm` is to give a developer ability to name tables/columns as he/she wants. Many other ORM libraries manage
 *  subentities automatically and it is not correct cause developer must understand how everything works inside sqlite
 *  otherwise his/her app might not work properly. Also developer must know schema in case he or she needs a strict
 *  access with sqlite client.
 */

#include <sqlite_orm/sqlite_orm.h>
#include <iostream>

using std::cout;
using std::endl;

class Mark {
  public:
    int value;
    int student_id;
};

class Student {
  public:
    int id;
    std::string name;
    int roll_number;
    std::vector<decltype(Mark::value)> marks;
};

using namespace sqlite_orm;
auto storage =
    make_storage("subentities.sqlite",
                 make_table("students",
                            make_column("id", &Student::id, primary_key()),
                            make_column("name", &Student::name),
                            make_column("roll_no", &Student::roll_number)),
                 make_table("marks", make_column("mark", &Mark::value), make_column("student_id", &Mark::student_id)));

//  inserts or updates student and does the same with marks
int addStudent(const Student& student) {
    auto studentId = student.id;
    if(storage.count<Student>(where(c(&Student::id) == student.id))) {
        storage.update(student);
    } else {
        studentId = storage.insert(student);
    }
    //  insert all marks within a transaction
    storage.transaction([&] {
        storage.remove_all<Mark>(where(c(&Mark::student_id) == studentId));
        for(auto& mark: student.marks) {
            storage.insert(Mark{mark, studentId});
        }
        return true;
    });
    return studentId;
}

/**
 *  To get student from db we have to execute two queries:
 *  `SELECT * FROM students WHERE id = ?`
 *  `SELECT mark FROM marks WHERE student_id = ?`
 */
Student getStudent(int studentId) {
    auto res = storage.get<Student>(studentId);
    res.marks = storage.select(&Mark::value, where(c(&Mark::student_id) == studentId));
    return res;  //  must be moved automatically by compiler
}

int main(int, char**) {
    decltype(Student::id) mikeId;
    decltype(Student::id) annaId;

    {
        storage.sync_schema();  // create tables if they don't exist

        Student mike{-1, "Mike", 123, {}};  //  create student named `Mike` without marks and without id
        mike.marks = {3, 4, 5};
        mike.id = addStudent(mike);
        mikeId = mike.id;

        //  also let's create another students with marks..
        Student anna{-1, "Anna", 555, {}};
        anna.marks.push_back(6);
        anna.marks.push_back(7);
        anna.id = addStudent(anna);
        annaId = anna.id;
    }
    // now let's assume we forgot about object `mike`, let's try to get him with his marks
    //  assume we know `mikeId` variable only

    {
        auto mike = getStudent(mikeId);
        cout << "mike = " << storage.dump(mike) << endl;
        cout << "mike.marks = ";
        for(auto& m: mike.marks) {
            cout << m << " ";
        }
        cout << endl;

        auto anna = getStudent(annaId);
        cout << "anna = " << storage.dump(anna) << endl;
        cout << "anna.marks = ";
        for(auto& m: anna.marks) {
            cout << m << " ";
        }
        cout << endl;
    }

    return 0;
}
