//
//  The example is implemented from here https://sqlite.org/foreignkeys.html
//

#include <sqlite_orm/sqlite_orm.h>

#include <string>
#include <iostream>
#include <cassert>
#include <memory>

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

struct Artist {
    int artistId;
    std::string artistName;
};

struct Track {
    int trackId;
    std::string trackName;
    std::unique_ptr<int> trackArtist;  //  must map to &Artist::artistId
};

int main(int, char** argv) {
    cout << "path = " << argv[0] << endl;

    using namespace sqlite_orm;
    {  //  simple case with foreign key to a single column without actions
        auto storage = make_storage("foreign_key.sqlite",
                                    make_table("artist",
                                               make_column("artistid", &Artist::artistId, primary_key()),
                                               make_column("artistname", &Artist::artistName)),
                                    make_table("track",
                                               make_column("trackid", &Track::trackId, primary_key()),
                                               make_column("trackname", &Track::trackName),
                                               make_column("trackartist", &Track::trackArtist),
                                               foreign_key(&Track::trackArtist).references(&Artist::artistId)));
        auto syncSchemaRes = storage.sync_schema();
        for(auto& p: syncSchemaRes) {
            cout << p.first << " " << p.second << endl;
        }

        storage.remove_all<Track>();
        storage.remove_all<Artist>();

        storage.replace(Artist{1, "Dean Martin"});
        storage.replace(Artist{2, "Frank Sinatra"});

        storage.replace(Track{11, "That's Amore", std::make_unique<int>(1)});
        storage.replace(Track{12, "Christmas Blues", std::make_unique<int>(1)});
        storage.replace(Track{13, "My Way", std::make_unique<int>(2)});

        try {
            //  This fails because value inserted into the trackartist column (3)
            //  does not correspond to row in the artist table.
            storage.replace(Track{14, "Mr. Bojangles", std::make_unique<int>(3)});
            assert(0);
        } catch(const std::system_error& e) {
            cout << e.what() << endl;
        }

        //  This succeeds because a NULL is inserted into trackartist. A
        //  corresponding row in the artist table is not required in this case.
        storage.replace(Track{14, "Mr. Bojangles", nullptr});

        //  Trying to modify the trackartist field of the record after it has
        //  been inserted does not work either, since the new value of trackartist (3)
        //  still does not correspond to any row in the artist table.
        try {
            storage.update_all(set(assign(&Track::trackArtist, 3)),
                               where(is_equal(&Track::trackName, "Mr. Bojangles")));
            assert(0);
        } catch(const std::system_error& e) {
            cout << e.what() << endl;
        }

        //  Insert the required row into the artist table. It is then possible to
        //  update the inserted row to set trackartist to 3 (since a corresponding
        //  row in the artist table now exists).
        storage.replace(Artist{3, "Sammy Davis Jr."});
        storage.update_all(set(assign(&Track::trackArtist, 3)), where(is_equal(&Track::trackName, "Mr. Bojangles")));

        //  Now that "Sammy Davis Jr." (artistid = 3) has been added to the database,
        //  it is possible to INSERT new tracks using this artist without violating
        //  the foreign key constraint:
        storage.replace(Track{15, "Boogie Woogie", std::make_unique<int>(3)});

        try {
            //  Attempting to delete the artist record for "Frank Sinatra" fails, since
            //  the track table contains a row that refer to it.
            storage.remove_all<Artist>(where(is_equal(&Artist::artistName, "Frank Sinatra")));
            assert(0);
        } catch(const std::system_error& e) {
            cout << e.what() << endl;
        }

        //  Delete all the records from the track table that refer to the artist
        //  "Frank Sinatra". Only then is it possible to delete the artist.
        storage.remove_all<Track>(where(is_equal(&Track::trackName, "My Way")));
        storage.remove_all<Artist>(where(is_equal(&Artist::artistName, "Frank Sinatra")));

        try {
            //  Try to update the artistid of a row in the artist table while there
            //  exists records in the track table that refer to it.
            storage.update_all(set(assign(&Artist::artistId, 4)), where(is_equal(&Artist::artistName, "Dean Martin")));
            assert(0);
        } catch(const std::system_error& e) {
            cout << e.what() << endl;
        }

        //  Once all the records that refer to a row in the artist table have
        //  been deleted, it is possible to modify the artistid of the row.
        storage.remove_all<Track>(where(in(&Track::trackName, {"That's Amore", "Christmas Blues"})));
        storage.update_all(set(c(&Artist::artistId) = 4), where(c(&Artist::artistName) == "Dean Martin"));
    }
    {  //  case with ON UPDATE CASCADE
        auto storage = make_storage(
            "foreign_key2.sqlite",
            make_table("artist",
                       make_column("artistid", &Artist::artistId, primary_key()),
                       make_column("artistname", &Artist::artistName)),
            make_table("track",
                       make_column("trackid", &Track::trackId, primary_key()),
                       make_column("trackname", &Track::trackName),
                       make_column("trackartist", &Track::trackArtist),
                       foreign_key(&Track::trackArtist).references(&Artist::artistId).on_update.cascade()));
        auto syncSchemaRes = storage.sync_schema();
        for(auto& p: syncSchemaRes) {
            cout << p.first << " " << p.second << endl;
        }

        storage.remove_all<Track>();
        storage.remove_all<Artist>();

        storage.replace(Artist{1, "Dean Martin"});
        storage.replace(Artist{2, "Frank Sinatra"});

        storage.replace(Track{11, "That's Amore", std::make_unique<int>(1)});
        storage.replace(Track{12, "Christmas Blues", std::make_unique<int>(1)});
        storage.replace(Track{13, "My Way", std::make_unique<int>(2)});

        //  Update the artistid column of the artist record for "Dean Martin".
        //  Normally, this would raise a constraint, as it would orphan the two
        //  dependent records in the track table. However, the ON UPDATE CASCADE clause
        //  attached to the foreign key definition causes the update to "cascade"
        //  to the child table, preventing the foreign key constraint violation.
        //  UPDATE artist SET artistid = 100 WHERE artistname = 'Dean Martin';
        storage.update_all(set(c(&Artist::artistId) = 100), where(c(&Artist::artistName) == "Dean Martin"));

        cout << "artists:" << endl;
        for(auto& artist: storage.iterate<Artist>()) {
            cout << artist.artistId << '\t' << artist.artistName << endl;
        }
        cout << endl;

        cout << "tracks:" << endl;
        for(auto& track: storage.iterate<Track>()) {
            cout << track.trackId << '\t' << track.trackName << '\t';
            if(track.trackArtist) {
                cout << *track.trackArtist;
            } else {
                cout << "null";
            }
            cout << endl;
        }
        cout << endl;
    }
    {  //  case with ON DELETE SET DEFAULT
        auto storage = make_storage(
            "foreign_key3.sqlite",
            make_table("artist",
                       make_column("artistid", &Artist::artistId, primary_key()),
                       make_column("artistname", &Artist::artistName)),
            make_table("track",
                       make_column("trackid", &Track::trackId, primary_key()),
                       make_column("trackname", &Track::trackName),
                       make_column("trackartist", &Track::trackArtist, default_value(0)),
                       foreign_key(&Track::trackArtist).references(&Artist::artistId).on_delete.set_default()));
        auto syncSchemaRes = storage.sync_schema();
        for(auto& p: syncSchemaRes) {
            cout << p.first << " " << p.second << endl;
        }

        storage.remove_all<Track>();
        storage.remove_all<Artist>();

        storage.replace(Artist{3, "Sammy Davis Jr."});

        storage.replace(Track{14, "Mr. Bojangles", std::make_unique<int>(3)});

        //  Deleting the row from the parent table causes the child key
        //  value of the dependent row to be set to integer value 0. However, this
        //  value does not correspond to any row in the parent table. Therefore
        //  the foreign key constraint is violated and an is exception thrown.
        //  DELETE FROM artist WHERE artistname = 'Sammy Davis Jr.';
        try {
            storage.remove_all<Artist>(where(c(&Artist::artistName) == "Sammy Davis Jr."));
            assert(0);
        } catch(const std::system_error& e) {
            cout << e.what() << endl;
        }

        //  This time, the value 0 does correspond to a parent table row. And
        //  so the DELETE statement does not violate the foreign key constraint
        //  and no exception is thrown.
        //  INSERT INTO artist VALUES(0, 'Unknown Artist');
        //  DELETE FROM artist WHERE artistname = 'Sammy Davis Jr.'
        storage.replace(Artist{0, "Unknown Artist"});
        storage.remove_all<Artist>(where(c(&Artist::artistName) == "Sammy Davis Jr."));

        cout << "artists:" << endl;
        for(auto& artist: storage.iterate<Artist>()) {
            cout << artist.artistId << '\t' << artist.artistName << endl;
        }
        cout << endl;

        cout << "tracks:" << endl;
        for(auto& track: storage.iterate<Track>()) {
            cout << track.trackId << '\t' << track.trackName << '\t';
            if(track.trackArtist) {
                cout << *track.trackArtist;
            } else {
                cout << "null";
            }
            cout << endl;
        }
        cout << endl;
    }

    return 0;
}
