1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard.layout.expected;
18
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Locale;
22
23/**
24 * This class builds an expected keyboard for unit test.
25 *
26 * An expected keyboard is an array of rows, and a row consists of an array of {@link ExpectedKey}s.
27 * Each row may have different number of {@link ExpectedKey}s. While building an expected keyboard,
28 * an {@link ExpectedKey} can be specified by a row number and a column number, both numbers starts
29 * from 1.
30 */
31public final class ExpectedKeyboardBuilder extends AbstractKeyboardBuilder<ExpectedKey> {
32    public ExpectedKeyboardBuilder() {
33        super();
34    }
35
36    public ExpectedKeyboardBuilder(final ExpectedKey[][] rows) {
37        super(rows);
38    }
39
40    @Override
41    protected ExpectedKey defaultElement() {
42        return ExpectedKey.EMPTY_KEY;
43    }
44
45    @Override
46    ExpectedKey[] newArray(final int size) {
47        return new ExpectedKey[size];
48    }
49
50    @Override
51    ExpectedKey[][] newArrayOfArray(final int size) {
52        return new ExpectedKey[size][];
53    }
54
55    @Override
56    public ExpectedKey[][] build() {
57        return super.build();
58    }
59
60    // A replacement job to be performed.
61    private interface ReplaceJob {
62        // Returns a {@link ExpectedKey} objects to replace.
63        ExpectedKey[] replacingKeys(final ExpectedKey oldKey);
64        // Return true if replacing should be stopped at first occurrence.
65        boolean stopAtFirstOccurrence();
66    }
67
68    private static ExpectedKey[] replaceKeyAt(final ExpectedKey[] keys, final int columnIndex,
69            final ExpectedKey[] replacingKeys) {
70        // Optimization for replacing a key with another key.
71        if (replacingKeys.length == 1) {
72            keys[columnIndex] = replacingKeys[0];
73            return keys;
74        }
75        final int newLength = keys.length - 1 + replacingKeys.length;
76        // Remove the key at columnIndex.
77        final ExpectedKey[] newKeys = Arrays.copyOf(keys, newLength);
78        System.arraycopy(keys, columnIndex + 1, newKeys, columnIndex + replacingKeys.length,
79                keys.length - 1 - columnIndex);
80        // Insert replacing keys at columnIndex.
81        System.arraycopy(replacingKeys, 0, newKeys, columnIndex, replacingKeys.length);
82        return newKeys;
83
84    }
85
86    // Replace key(s) that has the specified visual.
87    private void replaceKeyOf(final ExpectedKeyVisual visual, final ReplaceJob job) {
88        int replacedCount = 0;
89        final int rowCount = getRowCount();
90        for (int row = 1; row <= rowCount; row++) {
91            ExpectedKey[] keys = getRowAt(row);
92            for (int columnIndex = 0; columnIndex < keys.length; /* nothing */) {
93                final ExpectedKey currentKey = keys[columnIndex];
94                if (!currentKey.getVisual().hasSameKeyVisual(visual)) {
95                    columnIndex++;
96                    continue;
97                }
98                final ExpectedKey[] replacingKeys = job.replacingKeys(currentKey);
99                keys = replaceKeyAt(keys, columnIndex, replacingKeys);
100                columnIndex += replacingKeys.length;
101                setRowAt(row, keys);
102                replacedCount++;
103                if (job.stopAtFirstOccurrence()) {
104                    return;
105                }
106            }
107        }
108        if (replacedCount == 0) {
109            throw new RuntimeException(
110                    "Can't find key that has visual: " + visual + " in\n" + this);
111        }
112    }
113
114    // Helper method to create {@link ExpectedKey} array by joining {@link ExpectedKey},
115    // {@link ExpectedKey} array, and {@link String}.
116    static ExpectedKey[] joinKeys(final Object ... keys) {
117        final ArrayList<ExpectedKey> list = new ArrayList<>();
118        for (final Object key : keys) {
119            if (key instanceof ExpectedKey) {
120                list.add((ExpectedKey)key);
121            } else if (key instanceof ExpectedKey[]) {
122                list.addAll(Arrays.asList((ExpectedKey[])key));
123            } else if (key instanceof String) {
124                list.add(ExpectedKey.newInstance((String)key));
125            } else {
126                throw new RuntimeException("Unknown expected key type: " + key);
127            }
128        }
129        return list.toArray(new ExpectedKey[list.size()]);
130    }
131
132    /**
133     * Set the row with specified keys.
134     * @param row the row number to set keys.
135     * @param keys the keys to be set at <code>row</code>. Each key can be {@link ExpectedKey},
136     *        {@link ExpectedKey} array, and {@link String}.
137     * @return this builder.
138     */
139    public ExpectedKeyboardBuilder setKeysOfRow(final int row, final Object ... keys) {
140        setRowAt(row, joinKeys(keys));
141        return this;
142    }
143
144    /**
145     * Set the "more keys" of the key that has the specified label.
146     * @param label the label of the key to set the "more keys".
147     * @param moreKeys the array of "more key" to be set. Each "more key" can be
148     *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
149     * @return this builder.
150     */
151    public ExpectedKeyboardBuilder setMoreKeysOf(final String label, final Object ... moreKeys) {
152        setMoreKeysOf(ExpectedKeyVisual.newInstance(label), joinKeys(moreKeys));
153        return this;
154    }
155
156    /**
157     * Set the "more keys" of the key that has the specified icon.
158     * @param iconId the icon id of the key to set the "more keys".
159     * @param moreKeys the array of "more key" to be set. Each "more key" can be
160     *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
161     * @return this builder.
162     */
163    public ExpectedKeyboardBuilder setMoreKeysOf(final int iconId, final Object ... moreKeys) {
164        setMoreKeysOf(ExpectedKeyVisual.newInstance(iconId), joinKeys(moreKeys));
165        return this;
166    }
167
168    private void setMoreKeysOf(final ExpectedKeyVisual visual, final ExpectedKey[] moreKeys) {
169        replaceKeyOf(visual, new ReplaceJob() {
170            @Override
171            public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
172                return new ExpectedKey[] { oldKey.setMoreKeys(moreKeys) };
173            }
174            @Override
175            public boolean stopAtFirstOccurrence() {
176                return true;
177            }
178        });
179    }
180
181    /**
182     * Set the "additional more keys position" of the key that has the specified label.
183     * @param label the label of the key to set the "additional more keys".
184     * @param additionalMoreKeysPosition the position in the "more keys" where
185     *        "additional more keys" will be merged. The position starts from 1.
186     * @return this builder.
187     */
188    public ExpectedKeyboardBuilder setAdditionalMoreKeysPositionOf(final String label,
189            final int additionalMoreKeysPosition) {
190        final int additionalMoreKeysIndex = additionalMoreKeysPosition - 1;
191        if (additionalMoreKeysIndex < 0) {
192            throw new RuntimeException("Illegal additional more keys position: "
193                    + additionalMoreKeysPosition);
194        }
195        final ExpectedKeyVisual visual = ExpectedKeyVisual.newInstance(label);
196        replaceKeyOf(visual, new ReplaceJob() {
197            @Override
198            public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
199                return new ExpectedKey[] {
200                        oldKey.setAdditionalMoreKeysIndex(additionalMoreKeysIndex)
201                };
202            }
203            @Override
204            public boolean stopAtFirstOccurrence() {
205                return true;
206            }
207        });
208        return this;
209    }
210
211    /**
212     * Insert the keys at specified position.
213     * @param row the row number to insert the <code>keys</code>.
214     * @param column the column number to insert the <code>keys</code>.
215     * @param keys the array of keys to insert at <code>row,column</code>. Each key can be
216     *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
217     * @return this builder.
218     * @throws RuntimeException if <code>row</code> or <code>column</code> is illegal.
219     */
220    public ExpectedKeyboardBuilder insertKeysAtRow(final int row, final int column,
221            final Object ... keys) {
222        final ExpectedKey[] expectedKeys = joinKeys(keys);
223        for (int index = 0; index < keys.length; index++) {
224            setElementAt(row, column + index, expectedKeys[index], true /* insert */);
225        }
226        return this;
227    }
228
229    /**
230     * Add the keys on the left most of the row.
231     * @param row the row number to add the <code>keys</code>.
232     * @param keys the array of keys to add on the left most of the row. Each key can be
233     *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
234     * @return this builder.
235     * @throws RuntimeException if <code>row</code> is illegal.
236     */
237    public ExpectedKeyboardBuilder addKeysOnTheLeftOfRow(final int row,
238            final Object ... keys) {
239        final ExpectedKey[] expectedKeys = joinKeys(keys);
240        // Keys should be inserted from the last to preserve the order.
241        for (int index = keys.length - 1; index >= 0; index--) {
242            setElementAt(row, 1, expectedKeys[index], true /* insert */);
243        }
244        return this;
245    }
246
247    /**
248     * Add the keys on the right most of the row.
249     * @param row the row number to add the <code>keys</code>.
250     * @param keys the array of keys to add on the right most of the row. Each key can be
251     *        {@link ExpectedKey}, {@link ExpectedKey} array, and {@link String}.
252     * @return this builder.
253     * @throws RuntimeException if <code>row</code> is illegal.
254     */
255    public ExpectedKeyboardBuilder addKeysOnTheRightOfRow(final int row,
256            final Object ... keys) {
257        final int rightEnd = getRowAt(row).length + 1;
258        insertKeysAtRow(row, rightEnd, keys);
259        return this;
260    }
261
262    /**
263     * Replace the most top-left key that has the specified label with the new keys.
264     * @param label the label of the key to set <code>newKeys</code>.
265     * @param newKeys the keys to be set. Each key can be {@link ExpectedKey}, {@link ExpectedKey}
266     *        array, and {@link String}.
267     * @return this builder.
268     */
269    public ExpectedKeyboardBuilder replaceKeyOfLabel(final String label,
270            final Object ... newKeys) {
271        final ExpectedKeyVisual visual = ExpectedKeyVisual.newInstance(label);
272        replaceKeyOf(visual, new ReplaceJob() {
273            @Override
274            public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
275                return joinKeys(newKeys);
276            }
277            @Override
278            public boolean stopAtFirstOccurrence() {
279                return true;
280            }
281        });
282        return this;
283    }
284
285    /**
286     * Replace the all specified keys with the new keys.
287     * @param key the key to be replaced by <code>newKeys</code>.
288     * @param newKeys the keys to be set. Each key can be {@link ExpectedKey}, {@link ExpectedKey}
289     *        array, and {@link String}.
290     * @return this builder.
291     */
292    public ExpectedKeyboardBuilder replaceKeysOfAll(final ExpectedKey key,
293            final Object ... newKeys) {
294        replaceKeyOf(key.getVisual(), new ReplaceJob() {
295            @Override
296            public ExpectedKey[] replacingKeys(final ExpectedKey oldKey) {
297                return joinKeys(newKeys);
298            }
299            @Override
300            public boolean stopAtFirstOccurrence() {
301                return false;
302            }
303        });
304        return this;
305    }
306
307    /**
308     * Convert all keys of this keyboard builder to upper case keys.
309     * @param locale the locale used to convert cases.
310     * @return this builder
311     */
312    public ExpectedKeyboardBuilder toUpperCase(final Locale locale) {
313        final int rowCount = getRowCount();
314        for (int row = 1; row <= rowCount; row++) {
315            final ExpectedKey[] lowerCaseKeys = getRowAt(row);
316            final ExpectedKey[] upperCaseKeys = new ExpectedKey[lowerCaseKeys.length];
317            for (int columnIndex = 0; columnIndex < lowerCaseKeys.length; columnIndex++) {
318                upperCaseKeys[columnIndex] = lowerCaseKeys[columnIndex].toUpperCase(locale);
319            }
320            setRowAt(row, upperCaseKeys);
321        }
322        return this;
323    }
324
325    @Override
326    public String toString() {
327        return toString(build());
328    }
329
330    /**
331     * Convert the keyboard to human readable string.
332     * @param rows the keyboard to be converted to string.
333     * @return the human readable representation of <code>rows</code>.
334     */
335    public static String toString(final ExpectedKey[][] rows) {
336        final StringBuilder sb = new StringBuilder();
337        for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) {
338            if (rowIndex > 0) {
339                sb.append("\n");
340            }
341            sb.append(Arrays.toString(rows[rowIndex]));
342        }
343        return sb.toString();
344    }
345}
346