1var canReorderSingle = require('./reorderable').canReorderSingle;
2var extractProperties = require('./extract-properties');
3var isMergeable = require('./is-mergeable');
4var tidyRuleDuplicates = require('./tidy-rule-duplicates');
5
6var Token = require('../../tokenizer/token');
7
8var cloneArray = require('../../utils/clone-array');
9
10var serializeBody = require('../../writer/one-time').body;
11var serializeRules = require('../../writer/one-time').rules;
12
13function naturalSorter(a, b) {
14  return a > b ? 1 : -1;
15}
16
17function cloneAndMergeSelectors(propertyA, propertyB) {
18  var cloned = cloneArray(propertyA);
19  cloned[5] = cloned[5].concat(propertyB[5]);
20
21  return cloned;
22}
23
24function restructure(tokens, context) {
25  var options = context.options;
26  var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
27  var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
28  var mergeLimit = options.compatibility.selectors.mergeLimit;
29  var multiplePseudoMerging = options.compatibility.selectors.multiplePseudoMerging;
30  var specificityCache = context.cache.specificity;
31  var movableTokens = {};
32  var movedProperties = [];
33  var multiPropertyMoveCache = {};
34  var movedToBeDropped = [];
35  var maxCombinationsLevel = 2;
36  var ID_JOIN_CHARACTER = '%';
37
38  function sendToMultiPropertyMoveCache(position, movedProperty, allFits) {
39    for (var i = allFits.length - 1; i >= 0; i--) {
40      var fit = allFits[i][0];
41      var id = addToCache(movedProperty, fit);
42
43      if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) {
44        removeAllMatchingFromCache(id);
45        break;
46      }
47    }
48  }
49
50  function addToCache(movedProperty, fit) {
51    var id = cacheId(fit);
52    multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || [];
53    multiPropertyMoveCache[id].push([movedProperty, fit]);
54    return id;
55  }
56
57  function removeAllMatchingFromCache(matchId) {
58    var matchSelectors = matchId.split(ID_JOIN_CHARACTER);
59    var forRemoval = [];
60    var i;
61
62    for (var id in multiPropertyMoveCache) {
63      var selectors = id.split(ID_JOIN_CHARACTER);
64      for (i = selectors.length - 1; i >= 0; i--) {
65        if (matchSelectors.indexOf(selectors[i]) > -1) {
66          forRemoval.push(id);
67          break;
68        }
69      }
70    }
71
72    for (i = forRemoval.length - 1; i >= 0; i--) {
73      delete multiPropertyMoveCache[forRemoval[i]];
74    }
75  }
76
77  function cacheId(cachedTokens) {
78    var id = [];
79    for (var i = 0, l = cachedTokens.length; i < l; i++) {
80      id.push(serializeRules(cachedTokens[i][1]));
81    }
82    return id.join(ID_JOIN_CHARACTER);
83  }
84
85  function tokensToMerge(sourceTokens) {
86    var uniqueTokensWithBody = [];
87    var mergeableTokens = [];
88
89    for (var i = sourceTokens.length - 1; i >= 0; i--) {
90      if (!isMergeable(serializeRules(sourceTokens[i][1]), mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging)) {
91        continue;
92      }
93
94      mergeableTokens.unshift(sourceTokens[i]);
95      if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
96        uniqueTokensWithBody.push(sourceTokens[i]);
97    }
98
99    return uniqueTokensWithBody.length > 1 ?
100      mergeableTokens :
101      [];
102  }
103
104  function shortenIfPossible(position, movedProperty) {
105    var name = movedProperty[0];
106    var value = movedProperty[1];
107    var key = movedProperty[4];
108    var valueSize = name.length + value.length + 1;
109    var allSelectors = [];
110    var qualifiedTokens = [];
111
112    var mergeableTokens = tokensToMerge(movableTokens[key]);
113    if (mergeableTokens.length < 2)
114      return;
115
116    var allFits = findAllFits(mergeableTokens, valueSize, 1);
117    var bestFit = allFits[0];
118    if (bestFit[1] > 0)
119      return sendToMultiPropertyMoveCache(position, movedProperty, allFits);
120
121    for (var i = bestFit[0].length - 1; i >=0; i--) {
122      allSelectors = bestFit[0][i][1].concat(allSelectors);
123      qualifiedTokens.unshift(bestFit[0][i]);
124    }
125
126    allSelectors = tidyRuleDuplicates(allSelectors);
127    dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens);
128  }
129
130  function fitSorter(fit1, fit2) {
131    return fit1[1] > fit2[1] ? 1 : (fit1[1] == fit2[1] ? 0 : -1);
132  }
133
134  function findAllFits(mergeableTokens, propertySize, propertiesCount) {
135    var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1);
136    return combinations.sort(fitSorter);
137  }
138
139  function allCombinations(tokensVariant, propertySize, propertiesCount, level) {
140    var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]];
141    if (tokensVariant.length > 2 && level > 0) {
142      for (var i = tokensVariant.length - 1; i >= 0; i--) {
143        var subVariant = Array.prototype.slice.call(tokensVariant, 0);
144        subVariant.splice(i, 1);
145        differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1));
146      }
147    }
148
149    return differenceVariants;
150  }
151
152  function sizeDifference(tokensVariant, propertySize, propertiesCount) {
153    var allSelectorsSize = 0;
154    for (var i = tokensVariant.length - 1; i >= 0; i--) {
155      allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? serializeRules(tokensVariant[i][1]).length : -1;
156    }
157    return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
158  }
159
160  function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) {
161    var i, j, k, m;
162    var allProperties = [];
163
164    for (i = mergeableTokens.length - 1; i >= 0; i--) {
165      var mergeableToken = mergeableTokens[i];
166
167      for (j = mergeableToken[2].length - 1; j >= 0; j--) {
168        var mergeableProperty = mergeableToken[2][j];
169
170        for (k = 0, m = properties.length; k < m; k++) {
171          var property = properties[k];
172
173          var mergeablePropertyName = mergeableProperty[1][1];
174          var propertyName = property[0];
175          var propertyBody = property[4];
176          if (mergeablePropertyName == propertyName && serializeBody([mergeableProperty]) == propertyBody) {
177            mergeableToken[2].splice(j, 1);
178            break;
179          }
180        }
181      }
182    }
183
184    for (i = properties.length - 1; i >= 0; i--) {
185      allProperties.unshift(properties[i][3]);
186    }
187
188    var newToken = [Token.RULE, allSelectors, allProperties];
189    tokens.splice(position, 0, newToken);
190  }
191
192  function dropPropertiesAt(position, movedProperty) {
193    var key = movedProperty[4];
194    var toMove = movableTokens[key];
195
196    if (toMove && toMove.length > 1) {
197      if (!shortenMultiMovesIfPossible(position, movedProperty))
198        shortenIfPossible(position, movedProperty);
199    }
200  }
201
202  function shortenMultiMovesIfPossible(position, movedProperty) {
203    var candidates = [];
204    var propertiesAndMergableTokens = [];
205    var key = movedProperty[4];
206    var j, k;
207
208    var mergeableTokens = tokensToMerge(movableTokens[key]);
209    if (mergeableTokens.length < 2)
210      return;
211
212    movableLoop:
213    for (var value in movableTokens) {
214      var tokensList = movableTokens[value];
215
216      for (j = mergeableTokens.length - 1; j >= 0; j--) {
217        if (tokensList.indexOf(mergeableTokens[j]) == -1)
218          continue movableLoop;
219      }
220
221      candidates.push(value);
222    }
223
224    if (candidates.length < 2)
225      return false;
226
227    for (j = candidates.length - 1; j >= 0; j--) {
228      for (k = movedProperties.length - 1; k >= 0; k--) {
229        if (movedProperties[k][4] == candidates[j]) {
230          propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]);
231          break;
232        }
233      }
234    }
235
236    return processMultiPropertyMove(position, propertiesAndMergableTokens);
237  }
238
239  function processMultiPropertyMove(position, propertiesAndMergableTokens) {
240    var valueSize = 0;
241    var properties = [];
242    var property;
243
244    for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
245      property = propertiesAndMergableTokens[i][0];
246      var fullValue = property[4];
247      valueSize += fullValue.length + (i > 0 ? 1 : 0);
248
249      properties.push(property);
250    }
251
252    var mergeableTokens = propertiesAndMergableTokens[0][1];
253    var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0];
254    if (bestFit[1] > 0)
255      return false;
256
257    var allSelectors = [];
258    var qualifiedTokens = [];
259    for (i = bestFit[0].length - 1; i >= 0; i--) {
260      allSelectors = bestFit[0][i][1].concat(allSelectors);
261      qualifiedTokens.unshift(bestFit[0][i]);
262    }
263
264    allSelectors = tidyRuleDuplicates(allSelectors);
265    dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens);
266
267    for (i = properties.length - 1; i >= 0; i--) {
268      property = properties[i];
269      var index = movedProperties.indexOf(property);
270
271      delete movableTokens[property[4]];
272
273      if (index > -1 && movedToBeDropped.indexOf(index) == -1)
274        movedToBeDropped.push(index);
275    }
276
277    return true;
278  }
279
280  function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) {
281    var propertyName = property[0];
282    var movedPropertyName = movedProperty[0];
283    if (propertyName != movedPropertyName)
284      return false;
285
286    var key = movedProperty[4];
287    var toMove = movableTokens[key];
288    return toMove && toMove.indexOf(token) > -1;
289  }
290
291  for (var i = tokens.length - 1; i >= 0; i--) {
292    var token = tokens[i];
293    var isRule;
294    var j, k, m;
295    var samePropertyAt;
296
297    if (token[0] == Token.RULE) {
298      isRule = true;
299    } else if (token[0] == Token.NESTED_BLOCK) {
300      isRule = false;
301    } else {
302      continue;
303    }
304
305    // We cache movedProperties.length as it may change in the loop
306    var movedCount = movedProperties.length;
307
308    var properties = extractProperties(token);
309    movedToBeDropped = [];
310
311    var unmovableInCurrentToken = [];
312    for (j = properties.length - 1; j >= 0; j--) {
313      for (k = j - 1; k >= 0; k--) {
314        if (!canReorderSingle(properties[j], properties[k], specificityCache)) {
315          unmovableInCurrentToken.push(j);
316          break;
317        }
318      }
319    }
320
321    for (j = properties.length - 1; j >= 0; j--) {
322      var property = properties[j];
323      var movedSameProperty = false;
324
325      for (k = 0; k < movedCount; k++) {
326        var movedProperty = movedProperties[k];
327
328        if (movedToBeDropped.indexOf(k) == -1 && (!canReorderSingle(property, movedProperty, specificityCache) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) ||
329            movableTokens[movedProperty[4]] && movableTokens[movedProperty[4]].length === mergeLimit)) {
330          dropPropertiesAt(i + 1, movedProperty, token);
331
332          if (movedToBeDropped.indexOf(k) == -1) {
333            movedToBeDropped.push(k);
334            delete movableTokens[movedProperty[4]];
335          }
336        }
337
338        if (!movedSameProperty) {
339          movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1];
340
341          if (movedSameProperty) {
342            samePropertyAt = k;
343          }
344        }
345      }
346
347      if (!isRule || unmovableInCurrentToken.indexOf(j) > -1)
348        continue;
349
350      var key = property[4];
351
352      if (movedSameProperty && movedProperties[samePropertyAt][5].length + property[5].length > mergeLimit) {
353        dropPropertiesAt(i + 1, movedProperties[samePropertyAt]);
354        movedProperties.splice(samePropertyAt, 1);
355        movableTokens[key] = [token];
356        movedSameProperty = false;
357      } else {
358        movableTokens[key] = movableTokens[key] || [];
359        movableTokens[key].push(token);
360      }
361
362      if (movedSameProperty) {
363        movedProperties[samePropertyAt] = cloneAndMergeSelectors(movedProperties[samePropertyAt], property);
364      } else {
365        movedProperties.push(property);
366      }
367    }
368
369    movedToBeDropped = movedToBeDropped.sort(naturalSorter);
370    for (j = 0, m = movedToBeDropped.length; j < m; j++) {
371      var dropAt = movedToBeDropped[j] - j;
372      movedProperties.splice(dropAt, 1);
373    }
374  }
375
376  var position = tokens[0] && tokens[0][0] == Token.AT_RULE && tokens[0][1].indexOf('@charset') === 0 ? 1 : 0;
377  for (; position < tokens.length - 1; position++) {
378    var isImportRule = tokens[position][0] === Token.AT_RULE && tokens[position][1].indexOf('@import') === 0;
379    var isComment = tokens[position][0] === Token.COMMENT;
380    if (!(isImportRule || isComment))
381      break;
382  }
383
384  for (i = 0; i < movedProperties.length; i++) {
385    dropPropertiesAt(position, movedProperties[i]);
386  }
387}
388
389module.exports = restructure;
390