1 /**
2   Utility types that wrap specific utility functionality, without directly
3   representing data/modeling from Discord.
4 
5   TODO: moveme
6 */
7 module dscord.types.util;
8 
9 import dscord.types.message;
10 
11 import std.format,
12        std.array,
13        std.conv,
14        std.algorithm.sorting;
15 
16 /**
17   Utility class for constructing messages that can be sent over discord, allowing
18   for inteligent limiting of size and stripping of formatting characters.
19 */
20 class MessageBuffer : BaseSendable {
21   private {
22     bool codeBlock;
23     bool filter;
24     size_t _maxLength;
25     string[] lines;
26   }
27 
28   /**
29     Params:
30       codeBlock = if true, this message will be sent within a codeblock
31       filter = if true, appended lines will be filtered for codeblocks/newlines
32       maxLength = maximum length of this message, defaults to 2000
33   */
34   this(bool codeBlock = true, bool filter = true, size_t maxLength = 2000) {
35     this.codeBlock = codeBlock;
36     this.filter = filter;
37     this._maxLength = maxLength;
38   }
39 
40   override immutable(string) getContents() {
41     return this.contents;
42   }
43 
44   /// Remove the last line in the buffer
45   string popBack() {
46     string line = this.lines[$-1];
47     this.lines = this.lines[0..$-2];
48     return line;
49   }
50 
51   /// Max length of this message (subtracting for formatting)
52   @property size_t maxLength() {
53     size_t value = this._maxLength;
54 
55     // Codeblock backticks
56     if (this.codeBlock) {
57       value -= 6;
58     }
59 
60     // Newlines
61     value -= this.lines.length;
62 
63     return value;
64   }
65 
66   /**
67     Current length of this message (without formatting)
68   */
69   @property size_t length() {
70     size_t len;
71 
72     foreach (line; lines) {
73       len += line.length;
74     }
75 
76     return len;
77   }
78 
79   /**
80     Formatted contents of this message.
81   */
82   @property string contents() {
83     string contents = this.lines.join("\n");
84 
85     // Only format as a codeblock if we actually have contents
86     if (this.codeBlock && contents.length) {
87       return "```" ~ contents ~ "```";
88     }
89 
90     return contents;
91   }
92 
93   /**
94     Append a line to this message. Returns false if the buffer is full.
95   */
96   bool append(string line) {
97     string raw = line;
98 
99     // TODO: make this smarter
100     if (this.filter) {
101       if (this.codeBlock) {
102         raw = raw.replace("`", "");
103       }
104 
105       raw = raw.replace("\n", "");
106     }
107 
108     if (this.length + raw.length > this.maxLength) {
109       return false;
110     }
111 
112     this.lines ~= raw;
113     return true;
114   }
115 
116   /**
117     Format and append a line to this message. Returns false if the buffer is
118     full.
119   */
120   bool appendf(T...)(string fmt, T args) {
121     return this.append(format(fmt, args));
122   }
123 }
124 
125 /**
126   Utility class for constructing tabulated messages.
127 */
128 class MessageTable : BaseSendable {
129   private {
130     string[] header;
131     string[][] entries;
132     size_t[] sizes;
133     string delim;
134     bool wrapped;
135   }
136 
137   // Message buffer used to compile the message
138   MessageBuffer buffer;
139 
140   /**
141     Creates a new MessageTable
142 
143     Params:
144       delim = deliminator to use between table columns
145       wrapped = whether to place a deliminator at the left/right margins
146   */
147   this(string delim=" | ", bool wrapped=true, MessageBuffer buffer=null) {
148     this.delim = delim;
149     this.wrapped = wrapped;
150     this.buffer = buffer;
151   }
152 
153   override immutable(string) getContents() {
154     if (!this.buffer) this.buffer = new MessageBuffer;
155     if (!this.buffer.length) this.appendToBuffer(this.buffer);
156     return buffer.getContents();
157   }
158 
159   /**
160     Sort entries by column. Column must be integral.
161 
162     Params:
163       column = column index to sort by (0 based)
164   */
165   void sort(uint column, int delegate(string) conv=null) {
166     if (conv) {
167       this.entries = std.algorithm.sorting.sort!(
168         (a, b) => conv(a[column]) < conv(b[column])
169       )(this.entries.dup).array;
170     } else {
171       this.entries = std.algorithm.sorting.sort!(
172         (a, b) => a[column] < b[column])(this.entries.dup).array;
173     }
174   }
175 
176   /**
177     Set a header row. Will not be sorted or modified.
178   */
179   void setHeader(string[] args...) {
180     this.header = this.indexSizes(args.dup);
181   }
182 
183   /// Resets sizes index for a given row
184   private string[] indexSizes(string[] row) {
185     size_t pos = 0;
186 
187     foreach (part; row) {
188       size_t size = to!wstring(part).length;
189 
190       if (this.sizes.length <= pos) {
191         this.sizes ~= size;
192       } else if (this.sizes[pos] < size) {
193         this.sizes[pos] = size;
194       }
195       pos++;
196     }
197     return row;
198   }
199 
200   /**
201     Add a row to the table.
202 
203     Params:
204       args = sorted columns
205   */
206   void add(string[] args...) {
207     this.entries ~= this.indexSizes(args.dup);
208   }
209 
210   string compileEntry(string[] entry) {
211     size_t pos;
212     string line;
213 
214     // If we're wrapped, add left margin deliminator
215     if (this.wrapped) line ~= this.delim;
216 
217     foreach (part; entry) {
218       ulong size = to!wstring(part).length;
219       line ~= part ~ " ".replicate(cast(size_t)(this.sizes[pos] - size)) ~ this.delim;
220       pos++;
221     }
222 
223     // If we're not wrapped, remove the end deliminator
224     if (!this.wrapped) line = line[0..($ - this.delim.length)];
225 
226     return line;
227   }
228 
229   /// Returns all entries in the table (incl. header)
230   string[][] all() {
231     string[][] ents;
232 
233     if (this.header.length) {
234       ents ~= this.header;
235     }
236 
237     ents ~= this.entries;
238     return ents;
239   }
240 
241   /// Appends the output of this table to a message buffer
242   void appendToBuffer(MessageBuffer buffer) {
243     foreach (entry; this.all()) {
244       buffer.append(this.compileEntry(entry));
245     }
246   }
247 
248   /// Appends the output of this table to N many message buffers
249   MessageBuffer[] appendToBuffers(MessageBuffer delegate() createBuffer=null){
250     MessageBuffer[] bufs = [createBuffer ? createBuffer() : new MessageBuffer];
251 
252     foreach (entry; this.all()) {
253       string compiled = this.compileEntry(entry);
254 
255       if (!bufs[$-1].append(compiled)) {
256         bufs ~= createBuffer ? createBuffer() : new MessageBuffer;
257         bufs[$-1].append(compiled);
258       }
259     }
260 
261     return bufs;
262   }
263 
264   string[][] iterEntries() {
265     return this.header ~ this.entries;
266   }
267 }