1'use strict'
2var align = require('wide-align')
3var validate = require('aproba')
4var wideTruncate = require('./wide-truncate')
5var error = require('./error')
6var TemplateItem = require('./template-item')
7
8function renderValueWithValues (values) {
9  return function (item) {
10    return renderValue(item, values)
11  }
12}
13
14var renderTemplate = module.exports = function (width, template, values) {
15  var items = prepareItems(width, template, values)
16  var rendered = items.map(renderValueWithValues(values)).join('')
17  return align.left(wideTruncate(rendered, width), width)
18}
19
20function preType (item) {
21  var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1)
22  return 'pre' + cappedTypeName
23}
24
25function postType (item) {
26  var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1)
27  return 'post' + cappedTypeName
28}
29
30function hasPreOrPost (item, values) {
31  if (!item.type) {
32    return
33  }
34  return values[preType(item)] || values[postType(item)]
35}
36
37function generatePreAndPost (baseItem, parentValues) {
38  var item = Object.assign({}, baseItem)
39  var values = Object.create(parentValues)
40  var template = []
41  var pre = preType(item)
42  var post = postType(item)
43  if (values[pre]) {
44    template.push({ value: values[pre] })
45    values[pre] = null
46  }
47  item.minLength = null
48  item.length = null
49  item.maxLength = null
50  template.push(item)
51  values[item.type] = values[item.type]
52  if (values[post]) {
53    template.push({ value: values[post] })
54    values[post] = null
55  }
56  return function ($1, $2, length) {
57    return renderTemplate(length, template, values)
58  }
59}
60
61function prepareItems (width, template, values) {
62  function cloneAndObjectify (item, index, arr) {
63    var cloned = new TemplateItem(item, width)
64    var type = cloned.type
65    if (cloned.value == null) {
66      if (!(type in values)) {
67        if (cloned.default == null) {
68          throw new error.MissingTemplateValue(cloned, values)
69        } else {
70          cloned.value = cloned.default
71        }
72      } else {
73        cloned.value = values[type]
74      }
75    }
76    if (cloned.value == null || cloned.value === '') {
77      return null
78    }
79    cloned.index = index
80    cloned.first = index === 0
81    cloned.last = index === arr.length - 1
82    if (hasPreOrPost(cloned, values)) {
83      cloned.value = generatePreAndPost(cloned, values)
84    }
85    return cloned
86  }
87
88  var output = template.map(cloneAndObjectify).filter(function (item) {
89    return item != null
90  })
91
92  var remainingSpace = width
93  var variableCount = output.length
94
95  function consumeSpace (length) {
96    if (length > remainingSpace) {
97      length = remainingSpace
98    }
99    remainingSpace -= length
100  }
101
102  function finishSizing (item, length) {
103    if (item.finished) {
104      throw new error.Internal('Tried to finish template item that was already finished')
105    }
106    if (length === Infinity) {
107      throw new error.Internal('Length of template item cannot be infinity')
108    }
109    if (length != null) {
110      item.length = length
111    }
112    item.minLength = null
113    item.maxLength = null
114    --variableCount
115    item.finished = true
116    if (item.length == null) {
117      item.length = item.getBaseLength()
118    }
119    if (item.length == null) {
120      throw new error.Internal('Finished template items must have a length')
121    }
122    consumeSpace(item.getLength())
123  }
124
125  output.forEach(function (item) {
126    if (!item.kerning) {
127      return
128    }
129    var prevPadRight = item.first ? 0 : output[item.index - 1].padRight
130    if (!item.first && prevPadRight < item.kerning) {
131      item.padLeft = item.kerning - prevPadRight
132    }
133    if (!item.last) {
134      item.padRight = item.kerning
135    }
136  })
137
138  // Finish any that have a fixed (literal or intuited) length
139  output.forEach(function (item) {
140    if (item.getBaseLength() == null) {
141      return
142    }
143    finishSizing(item)
144  })
145
146  var resized = 0
147  var resizing
148  var hunkSize
149  do {
150    resizing = false
151    hunkSize = Math.round(remainingSpace / variableCount)
152    output.forEach(function (item) {
153      if (item.finished) {
154        return
155      }
156      if (!item.maxLength) {
157        return
158      }
159      if (item.getMaxLength() < hunkSize) {
160        finishSizing(item, item.maxLength)
161        resizing = true
162      }
163    })
164  } while (resizing && resized++ < output.length)
165  if (resizing) {
166    throw new error.Internal('Resize loop iterated too many times while determining maxLength')
167  }
168
169  resized = 0
170  do {
171    resizing = false
172    hunkSize = Math.round(remainingSpace / variableCount)
173    output.forEach(function (item) {
174      if (item.finished) {
175        return
176      }
177      if (!item.minLength) {
178        return
179      }
180      if (item.getMinLength() >= hunkSize) {
181        finishSizing(item, item.minLength)
182        resizing = true
183      }
184    })
185  } while (resizing && resized++ < output.length)
186  if (resizing) {
187    throw new error.Internal('Resize loop iterated too many times while determining minLength')
188  }
189
190  hunkSize = Math.round(remainingSpace / variableCount)
191  output.forEach(function (item) {
192    if (item.finished) {
193      return
194    }
195    finishSizing(item, hunkSize)
196  })
197
198  return output
199}
200
201function renderFunction (item, values, length) {
202  validate('OON', arguments)
203  if (item.type) {
204    return item.value(values, values[item.type + 'Theme'] || {}, length)
205  } else {
206    return item.value(values, {}, length)
207  }
208}
209
210function renderValue (item, values) {
211  var length = item.getBaseLength()
212  var value = typeof item.value === 'function' ? renderFunction(item, values, length) : item.value
213  if (value == null || value === '') {
214    return ''
215  }
216  var alignWith = align[item.align] || align.left
217  var leftPadding = item.padLeft ? align.left('', item.padLeft) : ''
218  var rightPadding = item.padRight ? align.right('', item.padRight) : ''
219  var truncated = wideTruncate(String(value), length)
220  var aligned = alignWith(truncated, length)
221  return leftPadding + aligned + rightPadding
222}
223