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; /// id is mainly used for syncing with habitrpg
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 	double weight = 1; /// Weight/priority of this Todo
63 
64 	private this() {}
65 
66 	this( string tle ) { 
67 		auto date_tup = parseAndRemoveDueDate( tle );
68 		due_date = date_tup[0];
69 
70 		creation_date = Date.now;
71 		mytitle = date_tup[1];
72 
73 		id = randomUUID;
74 	}
75 
76 	@property string title() const {
77 		return mytitle;
78 	}
79 
80 	override bool opEquals(Object t) const {
81 		auto td = cast(Todo)(t);
82 		return mytitle == td.mytitle;
83 	}
84 
85 	override int opCmp(Object t) const { 
86 		auto td = cast(Todo)(t);
87 		if ( this == td )
88 			return 0;
89 		else if ( title < td.title )
90 			return -1;
91 		return 1;
92 	}
93 
94 	private:
95 		string mytitle;
96 }
97 
98 unittest {
99 	Todo t1 = new Todo( "Todo 1" );
100 	assert( t1.title == "Todo 1" );
101 	assert( !t1.id.empty );
102 
103 	assert( t1.weight == 1 );
104 }
105 
106 string toString( Todo t ) {
107 	string str = t.title ~ " [ ";
108 	foreach ( tag; t.tags )
109 		str ~= tag.name ~ ", ";
110 	str ~= "]";
111 	return str;
112 }
113 
114 JSONValue toJSON( Todo t ) {
115 	JSONValue[string] jsonTODO;
116 	jsonTODO["title"] = t.title;
117 	JSONValue[] tags;
118 	foreach( tag; t.tags )
119 		tags ~= tag.toJSON();
120 	jsonTODO["tags"] = JSONValue(tags);
121 
122 	string[] progress_array;
123 	foreach( d; t.progress )
124 		progress_array ~= d.toStringDate;
125 	jsonTODO["progress"] = progress_array;
126 	jsonTODO["creation_date"] = t.creation_date.toStringDate;
127 	jsonTODO["due_date"] = t.due_date.toStringDate;
128 	jsonTODO["id"] = t.id.toString;
129 	jsonTODO["weight"] = t.weight;
130 	return JSONValue( jsonTODO );	
131 }
132 
133 Todo toTodo( const JSONValue json ) {
134 	Todo t = new Todo();
135 	const JSONValue[string] jsonAA = json.object;
136 	t.mytitle = jsonAA["title"].str;
137 	foreach ( tag; jsonAA["tags"].array )
138 		t.tags.add( Tag.parseJSON( tag ) );
139 	foreach ( js; jsonAA["progress"].array )
140 		t.progress ~= Date( js.str );
141 	t.creation_date = Date( jsonAA["creation_date"].str );
142 	if ("due_date" in jsonAA)
143 		t.due_date = Date( jsonAA["due_date"].str );
144 	if ("id" in jsonAA)
145 		t.id = UUID( jsonAA["id"].str );
146 	if ("weight" in jsonAA) {
147 		if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 
148 			t.weight = jsonAA["weight"].floating;
149 		else
150 			t.weight = cast(double)(jsonAA["weight"].integer);
151 	}
152 	return t;
153 }
154 
155 Todo toTodo( const JSONValue json, Tags tags ) {
156 	Todo t = new Todo();
157 	const JSONValue[string] jsonAA = json.object;
158 	t.mytitle = jsonAA["title"].str;
159 	foreach ( tag; jsonAA["tags"].array ) {
160 		auto newTag = Tag.parseJSON( tag );
161 		auto found = tags.find( newTag );
162 		if (found.length>0)
163 			t.tags.add( found[0] );
164 	}
165 	foreach ( js; jsonAA["progress"].array )
166 		t.progress ~= Date( js.str );
167 	t.creation_date = Date( jsonAA["creation_date"].str );
168 	if ("due_date" in jsonAA)
169 		t.due_date = Date( jsonAA["due_date"].str );
170 	if ("id" in jsonAA)
171 		t.id = UUID( jsonAA["id"].str );
172 	if ("weight" in jsonAA) {
173 		if (jsonAA["weight"].type == JSON_TYPE.FLOAT) 
174 			t.weight = jsonAA["weight"].floating;
175 		else
176 			t.weight = cast(double)(jsonAA["weight"].integer);
177 	}
178 	return t;
179 }
180 
181 unittest {
182 	Todo t1 = new Todo( "Todo 1 +tag" );
183 	assert( toJSON( t1 ).toTodo == t1 );
184 
185 	string missingDate = "{\"title\":\"Todo 1\",\"tags\":[{\"name\":\"tag\",\"id\":\"00000000-0000-0000-0000-000000000000\"}],\"progress\":[],\"creation_date\":\"2014-05-06\"}";
186 
187 	assert( toTodo( parseJSON(missingDate) ).title == "Todo 1" );
188 }
189 
190 /// Days since last progress. If no progress has been made then days since creation
191 auto lastProgress( const Todo t ) {
192 	Date currentDate = Date.now;
193 	if ( t.progress.length > 0 )
194 		return currentDate.substract( t.progress[$-1] );
195 	else
196 		return currentDate.substract( t.creation_date );
197 }
198 
199 /**
200 	Working on list of todos
201 	*/
202 alias Todos = Set!Todo;
203 version(unittest) {
204 	Todos generateSomeTodos() {
205 		Todo t1 = new Todo( "Todo 1" );
206 		t1.tags.add( [ new Tag( "tag1" ),new Tag( "tag2" ), new Tag( "tag3" ) ] );
207 		Todo t2 = new Todo( "Bla" );
208 		t2.tags.add( [ new Tag( "tag2" ), new Tag( "tag4" ) ] );
209 		Todos mytodos;
210 		mytodos.add( [t2,t1] );
211 		return mytodos;
212 	}
213 }
214 
215 unittest {
216 	auto ts = generateSomeTodos;
217 	assert( ts[1].tags.length == 3 );
218 	ts[1].tags.add( new Tag( "tag5" ) );
219 	assert( ts[1].tags.length == 4 );
220 }
221 
222 /**
223 	Select a weighted random set of Todos
224 	*/
225 Todo[] random( Todos ts, Tags allTags, TagDelta selected, 
226 		string searchString, 
227 		in Dependencies deps, 
228 		in double[string] defaultWeights, size_t no = 5 ) {
229 	if (ts.length > no) {
230 		return randomGillespie( ts, allTags, selected, searchString, deps, 
231 				defaultWeights, no );
232 	}	
233 	return ts.array;
234 }
235 
236 unittest {
237 	auto mytodos = generateSomeTodos().array;
238 	assert(	mytodos[0].title == "Bla" );
239 	assert(	mytodos[1].title == "Todo 1" );
240 }
241 
242 /// Return all existing tags
243 Tags allTags( Todos ts ) {
244 	Tags tags;
245 	foreach( t; ts ) {
246 		foreach( tag; t.tags ) {
247 			auto found = tags.find( tag );
248 			if (found.empty)
249 				tags.add( tag );
250 			else {
251 				if (tag.id.empty) {
252 					t.tags.remove( tag );
253 					t.tags.add( found[0] );
254 				} else {
255 					tags.remove( tag );
256 					tags.add( tag );
257 				}
258 			}
259 		}
260 	}
261 
262 	return tags;
263 }
264 
265 unittest {
266 	auto ts = generateSomeTodos();
267 	assert( equal( ts.allTags().array, [new Tag("tag1"), new Tag("tag2"), 
268 					new	Tag("tag3"), new Tag("tag4")] ) );
269 }
270 
271 /**
272 	Number of occurences of each given tag
273 	*/
274 size_t[Tag] tagOccurence( Todos ts, Tags tags ) {
275 	size_t[Tag] tagsCounts;
276 	foreach( t; ts ) {
277 		foreach( tag; t.tags ) {
278 			if (tags.canFind( tag ) ) {
279 				tagsCounts[tag]++;
280 			}
281 		}
282 		if (t.tags.length == 0)
283 			tagsCounts[new Tag("untagged")]++;
284 	}
285 	return tagsCounts;
286 }
287 
288 
289 unittest {
290 	auto ts = generateSomeTodos();
291 	auto expected = ["tag2":2, "tag3":1, "tag4":1, "tag1":1];
292 	foreach( k, v; ts.tagOccurence( ts.allTags ) ) {
293 		assert( v == expected[k.name] );
294 	}
295 }
296 
297 /**
298 	Turn Todos into a string
299 	*/
300 string toString( Todos ts ) {
301 	string str;
302 	foreach( t; ts ) {
303 		str = str ~ toString( t ) ~ "\n";
304 	}
305 	return str;
306 }
307 
308 /**
309 	Turn Todos into a JSONValue
310 	*/
311 JSONValue toJSON( Todos ts ) {
312 	JSONValue[] jsonTODOS;
313 	foreach (t; ts) 
314 		jsonTODOS ~= toJSON( t );
315 	JSONValue[string] json;
316 	json["todos"] = jsonTODOS;
317 	return JSONValue( json );	
318 }
319 
320 Todos toTodos( const JSONValue json ) {
321 	Todos ts;
322 	foreach ( js; json["todos"].array )
323 		ts.add( toTodo( js ) );
324 	return ts;
325 }
326 
327 unittest {
328 	auto mytodos = generateSomeTodos();
329 	assert(	toJSON( mytodos ).toTodos.array[0] == mytodos.array[0] );
330 }
331 
332 Todos loadTodos( GitRepo gr ) {
333 	Todos ts;
334 	auto todosFileName = "todos.json";
335 	auto content = readFile( gr.workPath, todosFileName );
336 	if (content != "")
337 		ts = toTodos( parseJSON( content ) );
338 	return ts;
339 }
340 
341 Todos loadTodos( GitRepo gr, Tags tags ) {
342 	Todos todos;
343 	auto todosFileName = "todos.json";
344 	auto content = readFile( gr.workPath, todosFileName );
345 	if (content != "")
346 		todos = jsonToSet!(Todo)( std.json.parseJSON( content )["todos"], 
347 				(js) => toTodo( js, tags ) );
348 	return todos;
349 }
350 
351 void writeTodos( Todos ts, GitRepo gr ) {
352 	auto todosFileName = "todos.json";
353 	writeToFile( gr.workPath, todosFileName, toJSON( ts ).toPrettyString );
354 	commitChanges( gr, todosFileName, "Updating todos file" );
355 }