1 /*
2 	 -------------------------------------------------------------------
3 
4 	 Copyright (C) 2014, Edwin van Leeuwen
5 
6 	 This file is part of todod todo list manager.
7 
8 	 Todod is free software; you can redistribute it and/or modify
9 	 it under the terms of the GNU General Public License as published by
10 	 the Free Software Foundation; either version 3 of the License, or
11 	 (at your option) any later version.
12 
13 	 Todod is distributed in the hope that it will be useful,
14 	 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 	 GNU General Public License for more details.
17 
18 	 You should have received a copy of the GNU General Public License
19 	 along with Todod. If not, see <http://www.gnu.org/licenses/>.
20 
21 	 -------------------------------------------------------------------
22 	 */
23 
24 module todod.todo;
25 
26 import std.string;
27 import std.regex;
28 
29 import std.json;
30 
31 import std.stdio;
32 import std.file;
33 import std.conv;
34 
35 import std.algorithm;
36 import std.range;
37 import std.array;
38 
39 import std.random;
40 
41 import std.uuid;
42 
43 import todod.date;
44 import todod.dependency;
45 import todod.random;
46 import todod.set;
47 import todod.shell;
48 import todod.storage;
49 import todod.tag;
50 
51 /// A Todo
52 class Todo {
53 	UUID id;
54 
55 	Date[] progress; /// Keep track of how long/often we've worked on this
56 
57 	Tags tags;
58 
59 	Date creation_date;
60 	Date due_date;
61     
62     import std.datetime : SysTime;
63     SysTime done_time;
64 
65 	double weight = 1; /// Weight/priority of this Todo
66 
67 	private this() {
68         done_time = SysTime(0);
69     }
70 
71 	this( string tle ) { 
72 		auto date_tup = parseAndRemoveDueDate( tle );
73 		due_date = date_tup[0];
74 
75 		creation_date = Date.now;
76 		mytitle = date_tup[1];
77 
78 		id = randomUUID;
79 
80         done_time = SysTime(0);
81 	}
82 
83 	@property string title() const {
84 		return mytitle;
85 	}
86 
87 	override bool opEquals(Object t) const {
88 		auto td = cast(Todo)(t);
89 		return mytitle == td.mytitle;
90 	}
91 
92 	override int opCmp(Object t) const { 
93 		auto td = cast(Todo)(t);
94 		if ( this == td )
95 			return 0;
96 		else if ( title < td.title )
97 			return -1;
98 		return 1;
99 	}
100 
101 	private:
102 		string mytitle;
103 }
104 
105 unittest {
106 	Todo t1 = new Todo( "Todo 1" );
107 	assert( t1.title == "Todo 1" );
108 	assert( !t1.id.empty );
109 
110 	assert( t1.weight == 1 );
111 }
112 
113 string toString( Todo t ) {
114 	string str = t.title ~ " [ ";
115 	foreach ( tag; t.tags )
116 		str ~= tag.name ~ ", ";
117 	str ~= "]";
118 	return str;
119 }
120 
121 JSONValue toJSON( Todo t ) {
122 	JSONValue[string] jsonTODO;
123 	jsonTODO["title"] = t.title;
124 	JSONValue[] tags;
125 	foreach( tag; t.tags )
126 		tags ~= tag.toJSON();
127 	jsonTODO["tags"] = JSONValue(tags);
128 
129 	string[] progress_array;
130 	foreach( d; t.progress )
131 		progress_array ~= d.toStringDate;
132 	jsonTODO["progress"] = progress_array;
133 	jsonTODO["creation_date"] = t.creation_date.toStringDate;
134 	jsonTODO["due_date"] = t.due_date.toStringDate;
135 	jsonTODO["id"] = t.id.toString;
136 	jsonTODO["weight"] = t.weight;
137     jsonTODO["done_time"] = t.done_time.toISOExtString;
138 	return JSONValue( jsonTODO );	
139 }
140 
141 Todo toTodo( const JSONValue json ) {
142 	Todo t = new Todo();
143 	const JSONValue[string] jsonAA = json.object;
144 	t.mytitle = jsonAA["title"].str;
145 	foreach ( tag; jsonAA["tags"].array )
146 		t.tags.add( Tag.parseJSON( tag ) );
147 	foreach ( js; jsonAA["progress"].array )
148 		t.progress ~= Date( js.str );
149 	t.creation_date = Date( jsonAA["creation_date"].str );
150 	if ("due_date" in jsonAA)
151 		t.due_date = Date( jsonAA["due_date"].str );
152 	if ("id" in jsonAA)
153 		t.id = UUID( jsonAA["id"].str );
154 	if ("weight" in jsonAA) {
155 		if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 
156 			t.weight = jsonAA["weight"].floating;
157 		else
158 			t.weight = cast(double)(jsonAA["weight"].integer);
159 	}
160 
161     if ("done_time" in jsonAA) {
162         import std.datetime : SysTime;
163         t.done_time = SysTime.fromISOExtString(jsonAA["done_time"].str);
164     }
165 	return t;
166 }
167 
168 Todo toTodo( const JSONValue json, Tags tags ) {
169 	Todo t = new Todo();
170 	const JSONValue[string] jsonAA = json.object;
171 	t.mytitle = jsonAA["title"].str;
172 	foreach ( tag; jsonAA["tags"].array ) {
173 		auto newTag = Tag.parseJSON( tag );
174 		auto found = tags.find( newTag );
175 		if (found.length>0)
176 			t.tags.add( found[0] );
177 	}
178 	foreach ( js; jsonAA["progress"].array )
179 		t.progress ~= Date( js.str );
180 	t.creation_date = Date( jsonAA["creation_date"].str );
181 	if ("due_date" in jsonAA)
182 		t.due_date = Date( jsonAA["due_date"].str );
183 	if ("id" in jsonAA)
184 		t.id = UUID( jsonAA["id"].str );
185 	if ("weight" in jsonAA) {
186 		if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 
187 			t.weight = jsonAA["weight"].floating;
188 		else
189 			t.weight = cast(double)(jsonAA["weight"].integer);
190 	}
191 
192     if ("done_time" in jsonAA) {
193         import std.datetime : SysTime;
194         t.done_time = SysTime.fromISOExtString(jsonAA["done_time"].str);
195     }
196     return t;
197 }
198 
199 unittest {
200 	Todo t1 = new Todo( "Todo 1 +tag" );
201 	assert( toJSON( t1 ).toTodo == t1 );
202 
203 	string missingDate = "{\"title\":\"Todo 1\",\"tags\":[{\"name\":\"tag\",\"id\":\"00000000-0000-0000-0000-000000000000\"}],\"progress\":[],\"creation_date\":\"2014-05-06\"}";
204 
205 	assert( toTodo( parseJSON(missingDate) ).title == "Todo 1" );
206 }
207 
208 /// Days since last progress. If no progress has been made then days since creation
209 auto lastProgress( const Todo t ) {
210 	Date currentDate = Date.now;
211 	if ( t.progress.length > 0 )
212 		return currentDate.substract( t.progress[$-1] );
213 	else
214 		return currentDate.substract( t.creation_date );
215 }
216 
217 auto markDone(ref Todo t, Tag doneTag) {
218     import std.datetime : Clock;
219     t.tags.add(doneTag);
220     t.done_time = Clock.currTime();
221     return t;
222 }
223 
224 unittest {
225     Todo t = new Todo("test");
226     assert(t.tags.filter!((a) => a.name == "done").empty);
227     t.markDone(new Tag("done"));
228     assert(!t.tags.filter!((a) => a.name == "done").empty);
229 }
230 
231 /**
232 	Working on list of todos
233 	*/
234 alias Todos = Set!Todo;
235 version(unittest) {
236 	Todos generateSomeTodos() {
237 		Todo t1 = new Todo( "Todo 1" );
238 		t1.tags.add( [ new Tag( "tag1" ),new Tag( "tag2" ), new Tag( "tag3" ) ] );
239 		Todo t2 = new Todo( "Bla" );
240 		t2.tags.add( [ new Tag( "tag2" ), new Tag( "tag4" ) ] );
241 		Todos mytodos;
242 		mytodos.add( [t2,t1] );
243 		return mytodos;
244 	}
245 }
246 
247 unittest {
248 	auto ts = generateSomeTodos;
249 	assert( ts[1].tags.length == 3 );
250 	ts[1].tags.add( new Tag( "tag5" ) );
251 	assert( ts[1].tags.length == 4 );
252 }
253 
254 /**
255 	Select a weighted random set of Todos
256 	*/
257 Todo[] random(TODOS)(TODOS ts, Tags allTags, TagDelta selected, 
258 		string searchString, 
259 		in Dependencies deps, 
260 		in double[string] defaultWeights, size_t no = 5 ) {
261 	if (ts.length > no) {
262 		return randomGillespie( ts, allTags, selected, searchString, deps, 
263 				defaultWeights, no );
264 	}	
265 	return ts.array;
266 }
267 
268 unittest {
269 	auto mytodos = generateSomeTodos().array;
270 	assert(	mytodos[0].title == "Bla" );
271 	assert(	mytodos[1].title == "Todo 1" );
272 }
273 
274 /// Return all existing tags
275 Tags allTags( Todos ts ) {
276 	Tags tags;
277 	foreach( t; ts ) {
278 		foreach( tag; t.tags ) {
279 			auto found = tags.find( tag );
280 			if (found.empty)
281 				tags.add( tag );
282 			else {
283 				if (tag.id.empty) {
284 					t.tags.remove( tag );
285 					t.tags.add( found[0] );
286 				} else {
287 					tags.remove( tag );
288 					tags.add( tag );
289 				}
290 			}
291 		}
292 	}
293 
294 	return tags;
295 }
296 
297 unittest {
298 	auto ts = generateSomeTodos();
299 	assert( equal( ts.allTags().array, [new Tag("tag1"), new Tag("tag2"), 
300 					new	Tag("tag3"), new Tag("tag4")] ) );
301 }
302 
303 /**
304 	Number of occurences of each given tag
305 	*/
306 size_t[Tag] tagOccurence(TODOS)(TODOS ts, Tags tags, Tags exclude = Tags()) {
307 	size_t[Tag] tagsCounts;
308 	foreach( t; ts.filter!((a) => a.tags.filter!((b) => exclude.canFind(b)).empty)) {
309 		foreach( tag; t.tags ) {
310 			if (tags.canFind( tag ) ) {
311 				tagsCounts[tag]++;
312 			}
313 		}
314 		if (t.tags.length == 0)
315 			tagsCounts[new Tag("untagged")]++;
316 	}
317 	return tagsCounts;
318 }
319 
320 unittest {
321 	auto ts = generateSomeTodos();
322 	auto expected = ["tag2":2, "tag3":1, "tag4":1, "tag1":1];
323 	foreach( k, v; ts.tagOccurence( ts.allTags ) ) {
324 		assert( v == expected[k.name] );
325 	}
326 }
327 
328 /**
329 	Turn Todos into a string
330 	*/
331 string toString( Todos ts ) {
332 	string str;
333 	foreach( t; ts ) {
334 		str = str ~ toString( t ) ~ "\n";
335 	}
336 	return str;
337 }
338 
339 /**
340 	Turn Todos into a JSONValue
341 	*/
342 JSONValue toJSON( Todos ts ) {
343 	JSONValue[] jsonTODOS;
344 	foreach (t; ts) 
345 		jsonTODOS ~= toJSON( t );
346 	JSONValue[string] json;
347 	json["todos"] = jsonTODOS;
348 	return JSONValue( json );	
349 }
350 
351 Todos toTodos( const JSONValue json ) {
352 	Todos ts;
353 	foreach ( js; json["todos"].array )
354 		ts.add( toTodo( js ) );
355 	return ts;
356 }
357 
358 unittest {
359 	auto mytodos = generateSomeTodos();
360 	assert(	toJSON( mytodos ).toTodos.array[0] == mytodos.array[0] );
361 }
362 
363 Todos loadTodos( GitRepo gr ) {
364 	Todos ts;
365 	auto todosFileName = "todos.json";
366 	auto content = readFile( gr.workPath, todosFileName );
367 	if (content != "")
368 		ts = toTodos( parseJSON( content ) );
369 	return ts;
370 }
371 
372 Todos loadTodos( GitRepo gr, Tags tags ) {
373 	Todos todos;
374 	auto todosFileName = "todos.json";
375 	auto content = readFile( gr.workPath, todosFileName );
376 	if (content != "")
377 		todos = jsonToSet!(Todo)( std.json.parseJSON( content )["todos"], 
378 				(js) => toTodo( js, tags ) );
379 	return todos;
380 }
381 
382 void writeTodos( Todos ts, GitRepo gr ) {
383 	auto todosFileName = "todos.json";
384 	writeToFile( gr.workPath, todosFileName, toJSON( ts ).toPrettyString );
385 	commitChanges( gr, todosFileName, "Updating todos file" );
386 }