1 /**
2 Copyright: Copyright (c) 2014 Andrey Penechko.
3 License: a$(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 
7 module emulator.utils.undoproxy;
8 
9 //version = debug_observer;
10 
11 import std.algorithm : sort, SwapStrategy, filter, map;
12 import std.array : array, RefAppender, Appender, appender;
13 import std.range : iota, take, popFrontN;
14 import std.bitmanip : append, peek;
15 version(debug_observer) import std.stdio;
16 import std..string : format;
17 
18 import emulator.utils.groupsequence;
19 
20 @trusted:
21 
22 /// Needs ubyte[] observableArray; in insertion context to work.
23 mixin template UndoHelper()
24 {
25 	enum maxUndoSeqLength = ubyte.max - 1;
26 	enum emptyFrameMarker = ubyte.max;
27 
28 	static if (observableArray.length <= ubyte.max)
29 	{
30 		alias PositionType = ubyte;
31 	}
32 	else static if (observableArray.length <= ushort.max)
33 	{
34 		alias PositionType = ushort;
35 	}
36 	else static if (observableArray.length <= uint.max)
37 	{
38 		alias PositionType = uint;
39 	}
40 
41 	// Saves initial value of ubyte at index
42 	ubyte[size_t] frameUndoMap;
43 
44 	Appender!(ubyte[]) undoStack;
45 
46 	ulong numUndoFrames;
47 
48 
49 	bool hasUncommitedChanges() @property
50 	{
51 		return frameUndoMap.length > 0;
52 	}
53 
54 	void discardFrame()
55 	{
56 		frameUndoMap = null;
57 	}
58 
59 	void commitFrame(ulong frameNumber)
60 	{
61 		scope(exit) ++numUndoFrames;
62 
63 		// Compress empty undos
64 		if (frameUndoMap.length == 0)
65 		{
66 			commitEmptyFrame();
67 			return;
68 		}
69 
70 		auto changeGroups = frameUndoMap
71 							.keys()
72 							.sort!("a<b", SwapStrategy.stable)
73 							.filter!(a => frameUndoMap[a] != observableArray[a])
74 							.groupSequence!"b-a == 1"; //[0] position, [1] count
75 
76 		undoStack.append!ubyte(0); // Frame separator;
77 
78 		foreach(changeGroup; changeGroups)
79 		{
80 			PositionType position = cast(PositionType)changeGroup[0];
81 
82 			size_t len = changeGroup[1];
83 
84 			if (len <= maxUndoSeqLength)
85 			{
86 				undoStack ~= iota(position, position+len).map!(a => frameUndoMap[a]);
87 				undoStack.append!PositionType(position);
88 				undoStack.append!ubyte(cast(ubyte)len);
89 			}
90 			else
91 			{
92 				auto undoElements = iota(position, position+len).map!(a => frameUndoMap[a]);
93 
94 				while (!undoElements.empty)
95 				{
96 					auto numElementsToAdd = undoElements.length > maxUndoSeqLength ?
97 											maxUndoSeqLength : undoElements.length;
98 					
99 					undoStack ~= undoElements.take(numElementsToAdd);
100 					undoStack.append!PositionType(position);
101 					undoStack.append!ubyte(cast(ubyte)numElementsToAdd);
102 
103 					undoElements.popFrontN(numElementsToAdd);
104 
105 					position += numElementsToAdd;
106 				}
107 			}
108 		}
109 
110 		discardFrame();
111 	}
112 
113 	// Adds empty frame or increases count of empty frames if one already exists on top of stack.
114 	void commitEmptyFrame()
115 	{
116 		if (undoStackSize > 0)
117 		{
118 			ubyte length = undoStack.data.peek!ubyte(undoStackSize - 1);
119 
120 			if (length == emptyFrameMarker)
121 			{
122 				ubyte numEmptyFrames = undoStack.data.peek!ubyte(undoStackSize - 2);
123 
124 				if (numEmptyFrames < ubyte.max) 
125 				{
126 					undoStack.data[undoStackSize - 2] = cast(ubyte)(numEmptyFrames + 1);
127 					return;
128 				}
129 			}
130 		}
131 
132 		// no empty frame on top of undo stack was found.
133 		addEmptyUndoFrame();
134 	}
135 
136 	void addEmptyUndoFrame()
137 	{
138 		undoStack.append!ubyte(1); // Num empty frames.
139 		undoStack.append!ubyte(emptyFrameMarker); // Marker.
140 	}
141 
142 	private void addUndoAction(size_t pos, ubyte[] data)
143 	in
144 	{
145 		assert((pos + data.length - 1) < observableArray.length);
146 	}
147 	body
148 	{
149 		foreach(index; pos..pos + data.length)
150 		{
151 			if (index !in frameUndoMap && observableArray[index] != data[index - pos])
152 			{
153 				frameUndoMap[index] = observableArray[index];
154 			}
155 		}
156 	}
157 
158 	void discardUndoStack()
159 	{
160 		undoStack.shrinkTo(0);
161 		numUndoFrames = 0;
162 	}
163 
164 	void undoFrames(ulong numFrames = 1)
165 	{
166 		ulong framesUndone = 0;
167 
168 		while(undoStackSize > 0 && framesUndone < numFrames)
169 		{
170 			while(undoStackSize > 0)
171 			{
172 				// extract undo data length
173 				ubyte length = undoStack.data.peek!ubyte(undoStackSize - 1);
174 
175 				// frame end found
176 				if (length == 0)
177 				{
178 					undoStack.shrinkTo(undoStackSize - 1);
179 					break;
180 				}
181 				else if (length == emptyFrameMarker)
182 				{
183 					ubyte numEmptyFrames = undoStack.data.peek!ubyte(undoStackSize - 2);
184 
185 					// Last empty frame
186 					if (numEmptyFrames == 1)
187 					{
188 						undoStack.shrinkTo(undoStackSize - 2);
189 						break;
190 					}
191 
192 					// Undo one empty frame by decreasing counter.
193 					undoStack.data[$ - 2] = cast(ubyte)(numEmptyFrames - 1);
194 
195 					break;
196 				}
197 
198 				// extract target positon
199 				size_t position = undoStack.data.peek!PositionType(undoStackSize - PositionType.sizeof - 1);
200 
201 				// extract undo data
202 				auto undoData = undoStack.data[$ - (PositionType.sizeof + 1 + length)..$ - PositionType.sizeof - 1];
203 
204 				// Make undo
205 				observableArray[position..position + undoData.length] = undoData;
206 
207 				// Shrink undo stack
208 				auto shrinkDelta = ubyte.sizeof + PositionType.sizeof + length;
209 				undoStack.shrinkTo(undoStackSize - shrinkDelta);
210 			}
211 
212 			++framesUndone;
213 			--numUndoFrames;
214 			version(debug_observer) writefln("Undo stack %s\n", undoStack.data);
215 		}
216 	}
217 
218 	size_t undoStackSize() @property
219 	{
220 		return undoStack.data.length;
221 	}
222 }
223 
224 struct UndoableStruct(Struct, ArrayElement)
225 {
226 	union
227 	{
228 		Struct data;
229 		ubyte[Struct.sizeof] observableArray;
230 	}
231 
232 	mixin UndoHelper;
233 
234 	void reset()
235 	{
236 		observableArray[] = 0;
237 		discardFrame();
238 		discardUndoStack();
239 	}
240 
241 	auto opMemberAssign(string member, string op, T = ubyte)(T value = 1)
242 	{
243 		alias Member = typeof(__traits(getMember, data, member));
244 
245 		return opDispatch!(member)(
246 			cast(Member)(mixin("opDispatch!(member)() "~op~" value"))
247 		);
248 	}
249 
250 	alias inc(string member) = opMemberAssign!(member, "+");
251 	alias dec(string member) = opMemberAssign!(member, "-");
252 
253 	// setter
254 	auto opDispatch(string member, T)(T newValue)
255 	{
256 		alias Member = typeof(__traits(getMember, data, member));
257 
258 		auto value = __traits(getMember, data, member);
259 
260 		if (value == newValue) return value;
261 
262 		Member newValueCasted = cast(Member)newValue;
263 
264 		addUndoAction(
265 			__traits(getMember, data, member).offsetof,
266 			*(cast(ubyte[Member.sizeof]*)&newValueCasted)
267 		);
268 
269 		__traits(getMember, data, member) = newValueCasted;
270 
271 		return __traits(getMember, data, member);
272 	}
273 
274 	// getter
275 	auto opDispatch(string member)()
276 	{
277 		return __traits(getMember, data, member);
278 	}
279 
280 	auto opIndex(size_t index)
281 	{
282 		return (cast(ArrayElement[])observableArray)[index];
283 	}
284 
285 	void opSliceAssign(ArrayElement[] data, size_t i, size_t j)
286 	in
287 	{
288 		assert(j <= (cast(ArrayElement[])observableArray).length);
289 		assert(i < j);
290 		assert(data.length == j - i, format("Arrays have different sizes %s and %s", data.length, j - i));
291 	}
292 	body
293 	{
294 
295 		addUndoAction(i * ArrayElement.sizeof, cast(ubyte[])data);
296 
297 		(cast(ArrayElement[])observableArray)[i..j] = data;
298 	}
299 
300 	ArrayElement opIndexAssign(ArrayElement data, size_t index)
301 	{
302 		assert(index <= (cast(ArrayElement[])observableArray).length);
303 
304 		addUndoAction(index * ArrayElement.sizeof, *(cast(ubyte[ArrayElement.sizeof]*)&data));
305 
306 		return (cast(ArrayElement[])observableArray)[index] = data;
307 	}
308 }