1/*
2* Copyright (c) 2011-2013 Yorba Foundation
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU Lesser General Public
6* License as published by the Free Software Foundation; either
7* version 2.1 of the License, or (at your option) any later version.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*/
19
20public struct TagID {
21    public const int64 INVALID = -1;
22
23    public int64 id;
24
25    public TagID (int64 id = INVALID) {
26        this.id = id;
27    }
28
29    public bool is_invalid () {
30        return (id == INVALID);
31    }
32
33    public bool is_valid () {
34        return (id != INVALID);
35    }
36}
37
38public class TagRow {
39    public TagID tag_id;
40    public string name;
41    public Gee.Set<string>? source_id_list;
42    public int64 time_created;
43}
44
45public class TagTable : DatabaseTable {
46    private static TagTable instance = null;
47
48    private TagTable () {
49        set_table_name ("TagTable");
50
51        var stmt = create_stmt ("CREATE TABLE IF NOT EXISTS "
52                                 + "TagTable "
53                                 + "("
54                                 + "id INTEGER PRIMARY KEY, "
55                                 + "name TEXT UNIQUE NOT NULL, "
56                                 + "photo_id_list TEXT, "
57                                 + "time_created INTEGER"
58                                 + ")");
59
60        var res = stmt.step ();
61        if (res != Sqlite.DONE)
62            fatal ("create TagTable", res);
63    }
64
65    public static TagTable get_instance () {
66        if (instance == null)
67            instance = new TagTable ();
68
69        return instance;
70    }
71
72    public static void upgrade_for_htags () {
73        TagTable table = get_instance ();
74
75        try {
76            Gee.List < TagRow?> rows = table.get_all_rows ();
77
78            foreach (TagRow row in rows) {
79                row.name = row.name.replace (Tag.PATH_SEPARATOR_STRING, "-");
80                table.rename (row.tag_id, row.name);
81            }
82        } catch (DatabaseError e) {
83            error ("TagTable: can't upgrade tag names for hierarchical tag support: %s", e.message);
84        }
85    }
86
87    public TagRow add (string name) throws DatabaseError {
88        var stmt = create_stmt ("INSERT INTO TagTable (name, time_created) VALUES (?, ?)");
89
90        int64 time_created = now_sec ();
91
92        bind_text (stmt, 1, name);
93        bind_int64 (stmt, 2, time_created);
94
95        var res = stmt.step ();
96        if (res != Sqlite.DONE)
97            throw_error ("TagTable.add", res);
98
99        TagRow row = new TagRow ();
100        row.tag_id = TagID (db.last_insert_rowid ());
101        row.name = name;
102        row.source_id_list = null;
103        row.time_created = time_created;
104
105        return row;
106    }
107
108    // All fields but tag_id are respected in TagRow.
109    public TagID create_from_row (TagRow row) throws DatabaseError {
110        var stmt = create_stmt ("INSERT INTO TagTable (name, photo_id_list, time_created) VALUES (?, ?, ?)");
111
112        bind_text (stmt, 1, row.name);
113        bind_text (stmt, 2, serialize_source_ids (row.source_id_list));
114        bind_int64 (stmt, 3, row.time_created);
115
116        var res = stmt.step ();
117        if (res != Sqlite.DONE)
118            throw_error ("TagTable.create_from_row", res);
119
120        return TagID (db.last_insert_rowid ());
121    }
122
123    public void remove (TagID tag_id) throws DatabaseError {
124        delete_by_id (tag_id.id);
125    }
126
127    public string? get_name (TagID tag_id) throws DatabaseError {
128        Sqlite.Statement stmt;
129        if (!select_by_id (tag_id.id, "name", out stmt))
130            return null;
131
132        return stmt.column_text (0);
133    }
134
135    public TagRow? get_row (TagID tag_id) throws DatabaseError {
136        var stmt = create_stmt ("SELECT name, photo_id_list, time_created FROM TagTable WHERE id=?");
137
138        bind_int64 (stmt, 1, tag_id.id);
139
140        var res = stmt.step ();
141        if (res == Sqlite.DONE)
142            return null;
143        else if (res != Sqlite.ROW)
144            throw_error ("TagTable.get_row", res);
145
146        TagRow row = new TagRow ();
147        row.tag_id = tag_id;
148        row.name = stmt.column_text (0);
149        row.source_id_list = unserialize_source_ids (stmt.column_text (1));
150        row.time_created = stmt.column_int64 (2);
151
152        return row;
153    }
154
155    public Gee.List < TagRow?> get_all_rows () throws DatabaseError {
156        var stmt = create_stmt ("SELECT id, name, photo_id_list, time_created FROM TagTable");
157
158        Gee.List < TagRow?> rows = new Gee.ArrayList < TagRow?> ();
159
160        for (;;) {
161            var res = stmt.step ();
162            if (res == Sqlite.DONE)
163                break;
164            else if (res != Sqlite.ROW)
165                throw_error ("TagTable.get_all_rows", res);
166
167            // res == Sqlite.ROW
168            TagRow row = new TagRow ();
169            row.tag_id = TagID (stmt.column_int64 (0));
170            row.name = stmt.column_text (1);
171            row.source_id_list = unserialize_source_ids (stmt.column_text (2));
172            row.time_created = stmt.column_int64 (3);
173
174            rows.add (row);
175        }
176
177        return rows;
178    }
179
180    public void rename (TagID tag_id, string new_name) throws DatabaseError {
181        update_text_by_id_2 (tag_id.id, "name", new_name);
182    }
183
184    public void set_tagged_sources (TagID tag_id, Gee.Collection<string> source_ids) throws DatabaseError {
185        var stmt = create_stmt ("UPDATE TagTable SET photo_id_list=? WHERE id=?");
186
187        var serialized = serialize_source_ids (source_ids);
188        if (serialized == null) {
189            bind_null (stmt, 1);
190        } else {
191            bind_text (stmt, 1, serialized);
192        }
193
194        bind_int64 (stmt, 2, tag_id.id);
195
196        var res = stmt.step ();
197        if (res != Sqlite.DONE)
198            throw_error ("TagTable.set_tagged_photos", res);
199    }
200
201    private string? serialize_source_ids (Gee.Collection<string>? source_ids) {
202        if (source_ids == null)
203            return null;
204
205        StringBuilder result = new StringBuilder ();
206
207        foreach (string source_id in source_ids) {
208            result.append (source_id);
209            result.append (",");
210        }
211
212        return (result.len != 0) ? result.str : null;
213    }
214
215    private Gee.Set<string> unserialize_source_ids (string? text_list) {
216        Gee.Set<string> result = new Gee.HashSet<string> ();
217
218        if (text_list == null)
219            return result;
220
221        string[] split = text_list.split (",");
222        foreach (string token in split) {
223            if (is_string_empty (token))
224                continue;
225
226            // handle current and legacy encoding of source ids -- in the past, we only stored
227            // LibraryPhotos in tags so we only needed to store the numeric database key of the
228            // photo to uniquely identify it. Now, however, tags can store arbitrary MediaSources,
229            // so instead of simply storing a number we store the source id, a string that contains
230            // a typename followed by an identifying number (e.g., "video-022354").
231            if (token[0].isdigit ()) {
232                // this is a legacy entry
233                result.add (PhotoID.upgrade_photo_id_to_source_id (PhotoID (parse_int64 (token, 10))));
234            } else if (token[0].isalpha ()) {
235                // this is a modern entry
236                result.add (token);
237            }
238        }
239
240        return result;
241    }
242}
243