1 SC.mixin( /** @scope SC */ {
  2   /**
  3     This function is similar to SC.metricsForString, but takes an extra argument after the string and before the exampleElement.
  4     That extra argument is *maxWidth*, which is the maximum allowable width in which the string can be displayed. This function
  5     will find the narrowest width (within *maxWidth*) that keeps the text at the same number of lines it would've normally wrapped
  6     to had it simply been put in a container of width *maxWidth*.
  7 
  8     If you have text that's 900 pixels wide on a single line, but pass *maxWidth* as 800, the metrics that will be returned will
  9     specify a height of two lines' worth of text, but a width of only around 450 pixels. The width this function determines will
 10     cause the text to be split as evenly as possible over both lines.
 11 
 12     If your text is 1500 pixels wide and *maxWidth* is 800, the width you'll get back will be approximately 750 pixels, because
 13     the 1500 horizontal pixels of text will still fit within two lines.
 14 
 15     If your text grows beyond 1600 horizontal pixels, it'll wrap to three lines. Suppose you have 1700 pixels of text. This much
 16     text would require three lines at 800px per line, but this function will return you a width of approximately 1700/3 pixels,
 17     in order to fill out the third line of text so it isn't just ~100px long.
 18 
 19     A binary search is used to find the optimimum width. There's no way to ask the browser this question, so the answer must be
 20     searched for. Understandably, this can cause a lot of measurements, which are NOT cheap.
 21 
 22     Therefore, very aggressive caching is used in order to get out of having to perform the search. The final optimimum width is a
 23     result of all the following values:
 24 
 25       - The string itself
 26       - The styles on the exampleElement
 27       - The classNames passed in
 28       - Whether ignoreEscape is YES or NO
 29 
 30     The caching goes against all of these in order to remember results. Note that maxWidth, though an argument, isn't one of them;
 31     this means that the optimal width will be searched for only once per distinct *number of lines of text* for a given string and
 32     styling. However, due to the fact that a passed exampleElement can have different styles a subsequent time it's passed in (but
 33     still remains the same object with the same GUID, etc), caching will not be enabled unless you either pass in a style string
 34     instead of an element, or unless your element has *cacheableForMetrics: YES* as a key on it. In most situations, the styles on
 35     an element won't change from call to call, so this is purely defensive and for arguably infrequent benefit, but it's good
 36     insurance. If you set the *cacheableForMetrics* key to YES on your exampleElement, caching will kick in, and repeated calls to
 37     this function will cease to have any appreciable amortized cost.
 38 
 39     The caching works by detecting and constructing known intervals of width for each number of lines required by widths in those
 40     intervals. As soon as you get a result from this function, it remembers that any width between the width it returned and the
 41     maxWidth you gave it will return that same result. This also applies to maxWidths greater than the with you passed in, up
 42     until the width at which the text can fit inside maxWidth with one fewer line break. However, at this point, the function
 43     can't know how MUCH larger maxWidth can get before getting to the next widest setting. A simple check can be done at this point
 44     to determine if the existing cached result can be used: if the height of the string at the new maxWidth is the same as the
 45     cached height, then we know the string didn't fit onto one fewer line, so return the cached value. If we did this check, we
 46     could return very quickly after only one string measurement, but EACH time we increase the maxWidth we'll have to do a new
 47     string measurement to check that we didn't end up with horizontal room for one fewer line. Because of this, instead of doing
 48     the check, the function will perform its binary search to go all the way UP to the minimum maxWidth at which one fewer line
 49     can be used to fit the text. After caching this value, all subsequent calls to the function will result in no string
 50     measurements as long as all the maxWidths are within the interval determined to lead to the cached result. So, the second call
 51     can in some cases be more expensive than it needs to be, but this saves A LOT of expense on all subsequent calls. The less
 52     often one calls metricsForString, the happier one's life is.
 53 
 54     The amount of time this function will take ranges from 0 to maybe 35ms on an old, slow machine, and, when used for window
 55     resizing, you'll see 35, 20, 0, 0, 0, ..., 0, 0, 35, 0, 0, 0, ..., 0, 0, 35, 0, 0, 0, ..., 0, 0, 0, 35, 0, 0, 0, ...
 56     After resizing through all the different caching intervals, the function will always execute quickly... under 1ms nearly always.
 57     The expensive calls are when a caching interval is crossed and a new cached set of metrics for the new number of lines of text
 58     must be calculated. And in reality, the number of sub-millisecond function calls will be much greater relative to the number
 59     of expensive calls, because window resizing just works like that.
 60 
 61     @param {String} string The text whose width you wish to optimize within your maximum width preference.
 62 
 63     @param {Number} maxWidth The maximum width the text is allowed to span, period. Can have "px" afterwards. Need not be a whole
 64                              number. It will be stripped of "px", and/or rounded up to the nearest integer, if necessary.
 65 
 66     @param {Element/String} exampleElement The element whose styles will be used to measure the width and height of the string.
 67                                            You can pass a string of CSSText here if you wish, just as with SC.metricsForString.
 68 
 69     @param {String} [classNames] Optional. Any class names you wish to also put on the measurement element.
 70 
 71     @param {Boolean} [ignoreEscape] Optional. If true, HTML in your string will not be escaped. If false or omitted, any HTML
 72                                               characters will be escaped for the measurement. If it's omitted where it should be
 73                                               true for correct results, the metrics returned will usually be much bigger than
 74                                               otherwise required.
 75   */
 76   bestStringMetricsForMaxWidth: function(string,maxWidth,exampleElement,classNames,ignoreEscape) {
 77     if(!maxWidth) { SC.warn("When calling bestMetricsForWidth, the second argument, maxWidth, is required. There's no reason to call this without a maxWidth."); return undefined; }
 78     maxWidth = Math.ceil(parseFloat(maxWidth));
 79     var                me = arguments.callee,
 80               exIsElement = SC.typeOf(exampleElement||(exampleElement=""))!==SC.T_STRING,
 81             savedMaxWidth = exIsElement ? exampleElement.style.maxWidth : undefined,
 82                     cache = (!exIsElement || exampleElement.cacheableForMetrics) ?
 83                               SC.cacheSlotFor(exampleElement,classNames,ignoreEscape,string) :
 84                               undefined,
 85                  applyMax = exIsElement ?
 86                               (me._applyMaxToEl||(me._applyMaxToEl=function(el,width) { el.style.maxWidth = width+"px"; return el; })) :
 87                               (me._applyMaxToStr||(me._applyMaxToStr=function(str,width) { return str.replace(/max-width:[^;]*;/g,'') + " max-width:"+width+"px"; })),
 88                 removeMax = exIsElement ?
 89                               (me._removeMaxFromEl||(me._removeMaxFromEl=function(el) { el.style.maxWidth = "none"; return el; })) :
 90                               (me._removeMaxFromStr||(me._removeMaxFromStr=function(str) { return str.replace(/max-width:[^;]*;/g,'') + " max-width:none"; })),
 91           searchingUpward = false;
 92     if(cache) {
 93       cache.list || (cache.list = [{width: Infinity, height:0}]);
 94       for(var i=1,l=cache.list.length,inner,outer,ret; i<l && !ret; i++) {
 95         inner = cache.list[i];
 96         outer = cache.list[i-1];
 97         if(!inner || !inner.width) continue;
 98         if(maxWidth>=inner.width) {
 99           if((outer && outer.width) || (maxWidth<=inner.maxWidth)) {
100             // console.error('returning from cache,',CW.Anim.enumerate(inner));
101             return inner;
102           }
103           // searchingUpward = true;  //commented because this is currently problematic. If this remains false, duplicate work will be done if increasing in maxWidth since previous calls, but at least the results will be correct.
104           ret = inner;
105         }
106       }
107     }
108     var            exEl = applyMax(exampleElement,maxWidth),
109                 metrics = SC.metricsForString(string,exEl,classNames,ignoreEscape),
110         necessaryHeight = metrics.height,
111           oneLineHeight = cache ? cache.parent.height || (cache.parent.height=SC.metricsForString('W',exEl,classNames).height) : SC.metricsForString('W',exEl,classNames).height,
112                   lines = Math.round( necessaryHeight / oneLineHeight );
113     if(searchingUpward) { lines--; necessaryHeight=lines*oneLineHeight; }
114     if(necessaryHeight > oneLineHeight) {
115       var hi = searchingUpward ? Math.ceil(metrics.width*2.5) : metrics.width,
116           lo = searchingUpward ? metrics.width : Math.floor(metrics.width/2.5),
117           middle ,
118           now = new Date()*1,
119           count = 0;
120       while(hi-lo>1 || (metrics.height>necessaryHeight&&!searchingUpward) || (metrics.height<necessaryHeight&&searchingUpward)) {
121         count++;
122         middle = (hi+lo)/2;
123         exEl = applyMax(exEl,middle);
124         metrics = SC.metricsForString(string,exEl,classNames,ignoreEscape);
125         if(metrics.height>necessaryHeight) lo = middle;
126         else                               hi = middle;
127       }
128       metrics.width = Math.ceil(middle);
129       metrics.height = necessaryHeight;
130       metrics.maxWidth = maxWidth;
131       metrics.lineHeight = oneLineHeight;
132       metrics.lines = lines;
133       metrics.searchPerformed = true;
134       metrics.searchTime = new Date()*1 - now;
135       metrics.searchCount = count;
136     } else {
137       if(searchingUpward) metrics = SC.metricsForString(string,exEl=removeMax(exEl),classNames,ignoreEscape);
138       metrics.maxWidth = maxWidth;
139       metrics.lineHeight = oneLineHeight;
140       metrics.lines = lines;
141       metrics.searchPerformed = false;
142     }
143     metrics.browserCorrection = 0;
144     if(SC.browser.isIE) metrics.browserCorrection = 1;
145     if(SC.browser.isMozilla) metrics.browserCorrection = 1;
146     metrics.width = Math.min(maxWidth,metrics.width+metrics.browserCorrection);
147     if(cache) {
148       var entry = cache.list[lines];
149       if(entry && entry.maxWidth<maxWidth) entry.maxWidth = maxWidth;
150       if(!entry) entry = cache.list[lines] = metrics;
151     }
152     if(exIsElement) exEl.style.maxWidth = savedMaxWidth;
153     ret = searchingUpward ? ret : metrics;
154     // console.error('returning at end'+(searchingUpward?" after searching upward and finding"+CW.Anim.enumerate(metrics):"")+'. Returned value is ',CW.Anim.enumerate(ret));
155     return ret;
156   },
157 
158   /**
159     Supply any number of arguments of any type, and this function will return you a hash associated with all those arguments.
160     Call it twice with the same arguments in the same order, and the hash is the same. This is great for getting out of
161     calculations whose answers depend on many different variables.
162 
163     @param {anything} your-arguments Any set of arguments whatsoever. If the FIRST argument is an array (including Arguments
164                                      arrays), all other arguments will be ignored and the array will be treated as if its
165                                      values at its numerical indices were passed in themselves as individual arguments.
166     @returns {Hash} A cached workspace mapped to the ordered *n*-tuple of arguments passed into it.
167   */
168   cacheSlotFor: function() {
169     var     me = arguments.callee.caller,
170           curr = me.cache || (me.cache={});
171     if(!arguments[0]) return curr;
172     var   args = (arguments[0] instanceof Array || arguments[0].callee) ? arguments[0] : arguments,
173         length = args.length,
174            arg ,
175              i ;
176     for(i=0; i<length; i++) {
177       if(typeof (arg=args[i]) === "object")
178         arg = SC.guidFor(arg);
179       curr = curr[arg] || (curr[arg]={parent:curr});
180     }
181     return curr;
182   },
183 
184   /**
185     Returns a wrapped copy of your function that caches its results according to its arguments. This function itself is cached, so
186     the function you receive when you pass in a particular function will always be the same function.
187 
188     How was does this function handle its own caching? Using itself, of course! :-D
189 
190     Use this only on functions without side effects you depend on, and only on functions whose outputs depend entirely on their
191     arguments and on nothing else external to them that could change.
192   */
193   cachedVersionOf: function() {
194     var ret = function(func) {
195       var ret = function() {     var cache = SC.cacheSlotFor(arguments);
196                                  return cache.result || (cache.result = arguments.callee.func.apply(this,arguments));    };
197       ret.func = func;
198       return ret;
199     };
200     return ret(ret);
201   }()
202 });
203