1 /**
2   Utility types that wrap specific utility functionality, without directly
3   representing data/modeling from Discord.
4 
5   TODO: moveme
6 */
7 module dcord.types.util;
8 
9 import dcord.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       conv = delegate that returns an int and accepts a string, for sorting
165   */
166   void sort(uint column, int delegate(string) conv=null) {
167     if (conv) {
168       this.entries = std.algorithm.sorting.sort!(
169         (a, b) => conv(a[column]) < conv(b[column])
170       )(this.entries.dup).array;
171     } else {
172       this.entries = std.algorithm.sorting.sort!(
173         (a, b) => a[column] < b[column])(this.entries.dup).array;
174     }
175   }
176 
177   /**
178     Set a header row. Will not be sorted or modified.
179   */
180   void setHeader(string[] args...) {
181     this.header = this.indexSizes(args.dup);
182   }
183 
184   /// Resets sizes index for a given row
185   private string[] indexSizes(string[] row) {
186     size_t pos = 0;
187 
188     foreach (part; row) {
189       size_t size = to!wstring(part).length;
190 
191       if (this.sizes.length <= pos) {
192         this.sizes ~= size;
193       } else if (this.sizes[pos] < size) {
194         this.sizes[pos] = size;
195       }
196       pos++;
197     }
198     return row;
199   }
200 
201   /**
202     Add a row to the table.
203 
204     Params:
205       args = sorted columns
206   */
207   void add(string[] args...) {
208     this.entries ~= this.indexSizes(args.dup);
209   }
210   /**
211     Compile an entry.
212     Params:
213       entry = array of strings
214   */
215   string compileEntry(string[] entry) {
216     size_t pos;
217     string line;
218 
219     // If we're wrapped, add left margin deliminator
220     if (this.wrapped) line ~= this.delim;
221 
222     foreach (part; entry) {
223       ulong size = to!wstring(part).length;
224       line ~= part ~ " ".replicate(cast(size_t)(this.sizes[pos] - size)) ~ this.delim;
225       pos++;
226     }
227 
228     // If we're not wrapped, remove the end deliminator
229     if (!this.wrapped) line = line[0..($ - this.delim.length)];
230 
231     return line;
232   }
233 
234   /// Returns all entries in the table (incl. header)
235   string[][] all() {
236     string[][] ents;
237 
238     if (this.header.length) {
239       ents ~= this.header;
240     }
241 
242     ents ~= this.entries;
243     return ents;
244   }
245 
246   /// Appends the output of this table to a message buffer
247   void appendToBuffer(MessageBuffer buffer) {
248     foreach (entry; this.all()) {
249       buffer.append(this.compileEntry(entry));
250     }
251   }
252 
253   /// Appends the output of this table to N many message buffers
254   MessageBuffer[] appendToBuffers(MessageBuffer delegate() createBuffer=null){
255     MessageBuffer[] bufs = [createBuffer ? createBuffer() : new MessageBuffer];
256 
257     foreach (entry; this.all()) {
258       string compiled = this.compileEntry(entry);
259 
260       if (!bufs[$-1].append(compiled)) {
261         bufs ~= createBuffer ? createBuffer() : new MessageBuffer;
262         bufs[$-1].append(compiled);
263       }
264     }
265 
266     return bufs;
267   }
268 
269   /// Return the header concentated with the entries
270   string[][] iterEntries() {
271     return this.header ~ this.entries;
272   }
273 }