/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2016, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import {
  SequenceOrArrayLike, asSequence
} from '../algorithm/sequence';


/**
 * A sizer object for use with the [[boxCalc]] function.
 *
 * #### Notes
 * A box sizer holds the geometry information for an object along a
 * layout orientation.
 *
 * For best performance, this class should be treated as a raw data
 * struct. It should not typically be subclassed.
 */
export
class BoxSizer {
  /**
   * The preferred size for the sizer.
   *
   * #### Notes
   * The sizer will be given this initial size subject to its size
   * bounds. The sizer will not deviate from this size unless such
   * deviation is required to fit into the available layout space.
   *
   * There is no limit to this value, but it will be clamped to the
   * bounds defined by [[minSize]] and [[maxSize]].
   *
   * The default value is `0`.
   */
  sizeHint = 0;

  /**
   * The minimum size of the sizer.
   *
   * #### Notes
   * The sizer will never be sized less than this value, even if
   * it means the sizer will overflow the available layout space.
   *
   * It is assumed that this value lies in the range `[0, Infinity)`
   * and that it is `<=` to [[maxSize]]. Failure to adhere to this
   * constraint will yield undefined results.
   *
   * The default value is `0`.
   */
  minSize = 0;

  /**
   * The maximum size of the sizer.
   *
   * #### Notes
   * The sizer will never be sized greater than this value, even if
   * it means the sizer will underflow the available layout space.
   *
   * It is assumed that this value lies in the range `[0, Infinity]`
   * and that it is `>=` to [[minSize]]. Failure to adhere to this
   * constraint will yield undefined results.
   *
   * The default value is `Infinity`.
   */
  maxSize = Infinity;

  /**
   * The stretch factor for the sizer.
   *
   * #### Notes
   * This controls how much the sizer stretches relative to its sibling
   * sizers when layout space is distributed. A stretch factor of zero
   * is special and will cause the sizer to only be resized after all
   * other sizers with a stretch factor greater than zero have been
   * resized to their limits.
   *
   * It is assumed that this value is an integer that lies in the range
   * `[0, Infinity)`. Failure to adhere to this constraint will yield
   * undefined results.
   *
   * The default value is `1`.
   */
  stretch = 1;

  /**
   * The computed size of the sizer.
   *
   * #### Notes
   * This value is the output of a call to [[boxCalc]]. It represents
   * the computed size for the object along the layout orientation,
   * and will always lie in the range `[minSize, maxSize]`.
   *
   * This value is output only.
   *
   * Changing this value will have no effect.
   */
  size = 0;

  /**
   * An internal storage property for the layout algorithm.
   *
   * #### Notes
   * This value is used as temporary storage by the layout algorithm.
   *
   * Changing this value will have no effect.
   */
  done = false;
}


/**
 * Compute the optimal layout sizes for a sequence of box sizers.
 *
 * This distributes the available layout space among the box sizers
 * according to the following algorithm:
 *
 * 1. Initialize the sizers's size to its size hint and compute the
 *    sums for each of size hint, min size, and max size.
 *
 * 2. If the total size hint equals the available space, return.
 *
 * 3. If the available space is less than the total min size, set all
 *    sizers to their min size and return.
 *
 * 4. If the available space is greater than the total max size, set
 *    all sizers to their max size and return.
 *
 * 5. If the layout space is less than the total size hint, distribute
 *    the negative delta as follows:
 *
 *    a. Shrink each sizer with a stretch factor greater than zero by
 *       an amount proportional to the negative space and the sum of
 *       stretch factors. If the sizer reaches its min size, remove
 *       it and its stretch factor from the computation.
 *
 *    b. If after adjusting all stretch sizers there remains negative
 *       space, distribute the space equally among the sizers with a
 *       stretch factor of zero. If a sizer reaches its min size,
 *       remove it from the computation.
 *
 * 6. If the layout space is greater than the total size hint,
 *    distribute the positive delta as follows:
 *
 *    a. Expand each sizer with a stretch factor greater than zero by
 *       an amount proportional to the postive space and the sum of
 *       stretch factors. If the sizer reaches its max size, remove
 *       it and its stretch factor from the computation.
 *
 *    b. If after adjusting all stretch sizers there remains positive
 *       space, distribute the space equally among the sizers with a
 *       stretch factor of zero. If a sizer reaches its max size,
 *       remove it from the computation.
 *
 * 7. return
 *
 * @param sizers - The sizers for a particular layout line.
 *
 * @param space - The available layout space for the sizers.
 *
 * #### Notes
 * The [[size]] of each sizer is updated with the computed size.
 *
 * This function can be called at any time to recompute the layout for
 * an existing sequence of sizers. The previously computed results will
 * have no effect on the new output. It is therefore not necessary to
 * create new sizer objects on each resize event.
 */
export
function boxCalc(object: SequenceOrArrayLike<BoxSizer>, space: number): void {
  // Bail early if there is nothing to do.
  let count = object.length;
  if (count === 0) {
    return;
  }

  // Cast the object to a sequence of sizers.
  let sizers = asSequence(object);

  // Setup the size and stretch counters.
  let totalMin = 0;
  let totalMax = 0;
  let totalSize = 0;
  let totalStretch = 0;
  let stretchCount = 0;

  // Setup the sizers and compute the totals.
  for (let i = 0; i < count; ++i) {
    let sizer = sizers.at(i);
    let min = sizer.minSize;
    let max = sizer.maxSize;
    let hint = sizer.sizeHint;
    sizer.done = false;
    sizer.size = Math.max(min, Math.min(hint, max));
    totalSize += sizer.size;
    totalMin += min;
    totalMax += max;
    if (sizer.stretch > 0) {
      totalStretch += sizer.stretch;
      stretchCount++;
    }
  }

  // If the space is equal to the total size, return.
  if (space === totalSize) {
    return;
  }

  // If the space is less than the total min, minimize each sizer.
  if (space <= totalMin) {
    for (let i = 0; i < count; ++i) {
      let sizer = sizers.at(i);
      sizer.size = sizer.minSize;
    }
    return;
  }

  // If the space is greater than the total max, maximize each sizer.
  if (space >= totalMax) {
    for (let i = 0; i < count; ++i) {
      let sizer = sizers.at(i);
      sizer.size = sizer.maxSize;
    }
    return;
  }

  // The loops below perform sub-pixel precision sizing. A near zero
  // value is used for compares instead of zero to ensure that the
  // loop terminates when the subdivided space is reasonably small.
  let nearZero = 0.01;

  // A counter which is decremented each time a sizer is resized to
  // its limit. This ensures the loops terminate even if there is
  // space remaining to distribute.
  let notDoneCount = count;

  // Distribute negative delta space.
  if (space < totalSize) {
    // Shrink each stretchable sizer by an amount proportional to its
    // stretch factor. If a sizer reaches its min size it's marked as
    // done. The loop progresses in phases where each sizer is given
    // a chance to consume its fair share for the pass, regardless of
    // whether a sizer before it reached its limit. This continues
    // until the stretchable sizers or the free space is exhausted.
    let freeSpace = totalSize - space;
    while (stretchCount > 0 && freeSpace > nearZero) {
      let distSpace = freeSpace;
      let distStretch = totalStretch;
      for (let i = 0; i < count; ++i) {
        let sizer = sizers.at(i);
        if (sizer.done || sizer.stretch === 0) {
          continue;
        }
        let amt = sizer.stretch * distSpace / distStretch;
        if (sizer.size - amt <= sizer.minSize) {
          freeSpace -= sizer.size - sizer.minSize;
          totalStretch -= sizer.stretch;
          sizer.size = sizer.minSize;
          sizer.done = true;
          notDoneCount--;
          stretchCount--;
        } else {
          freeSpace -= amt;
          sizer.size -= amt;
        }
      }
    }
    // Distribute any remaining space evenly among the non-stretchable
    // sizers. This progresses in phases in the same manner as above.
    while (notDoneCount > 0 && freeSpace > nearZero) {
      let amt = freeSpace / notDoneCount;
      for (let i = 0; i < count; ++i) {
        let sizer = sizers.at(i);
        if (sizer.done) {
          continue;
        }
        if (sizer.size - amt <= sizer.minSize) {
          freeSpace -= sizer.size - sizer.minSize;
          sizer.size = sizer.minSize;
          sizer.done = true;
          notDoneCount--;
        } else {
          freeSpace -= amt;
          sizer.size -= amt;
        }
      }
    }
  }
  // Distribute positive delta space.
  else {
    // Expand each stretchable sizer by an amount proportional to its
    // stretch factor. If a sizer reaches its max size it's marked as
    // done. The loop progresses in phases where each sizer is given
    // a chance to consume its fair share for the pass, regardless of
    // whether a sizer before it reached its limit. This continues
    // until the stretchable sizers or the free space is exhausted.
    let freeSpace = space - totalSize;
    while (stretchCount > 0 && freeSpace > nearZero) {
      let distSpace = freeSpace;
      let distStretch = totalStretch;
      for (let i = 0; i < count; ++i) {
        let sizer = sizers.at(i);
        if (sizer.done || sizer.stretch === 0) {
          continue;
        }
        let amt = sizer.stretch * distSpace / distStretch;
        if (sizer.size + amt >= sizer.maxSize) {
          freeSpace -= sizer.maxSize - sizer.size;
          totalStretch -= sizer.stretch;
          sizer.size = sizer.maxSize;
          sizer.done = true;
          notDoneCount--;
          stretchCount--;
        } else {
          freeSpace -= amt;
          sizer.size += amt;
        }
      }
    }
    // Distribute any remaining space evenly among the non-stretchable
    // sizers. This progresses in phases in the same manner as above.
    while (notDoneCount > 0 && freeSpace > nearZero) {
      let amt = freeSpace / notDoneCount;
      for (let i = 0; i < count; ++i) {
        let sizer = sizers.at(i);
        if (sizer.done) {
          continue;
        }
        if (sizer.size + amt >= sizer.maxSize) {
          freeSpace -= sizer.maxSize - sizer.size;
          sizer.size = sizer.maxSize;
          sizer.done = true;
          notDoneCount--;
        } else {
          freeSpace -= amt;
          sizer.size += amt;
        }
      }
    }
  }
}


/**
 * Adjust a sizer by a delta and adjust its neighbors accordingly.
 *
 * @param object - The sizers which should be adjusted.
 *
 * @param index - The index of the sizer to grow.
 *
 * @param delta - The amount to adjust the sizer, positive or negative.
 *
 * #### Notes
 * This will adjust the indicated sizer by the specified amount, along
 * with the sizes of the appropriate neighbors, subject to the limits
 * specified by each of the sizers.
 *
 * This is useful when implementing box layouts where the boundaries
 * between the sizers are interactively adjustable by the user.
 */
export
function adjustSizer(object: SequenceOrArrayLike<BoxSizer>, index: number, delta: number): void {
  // Bail early when there is nothing to do.
  if (object.length === 0 || delta === 0) {
    return;
  }

  // Dispatch to the proper implementation.
  if (delta > 0) {
    Private.growSizer(object, index, delta);
  } else {
    Private.shrinkSizer(object, index, -delta);
  }
}


/**
 * The namespace for the private module data.
 */
namespace Private {
  /**
   * Grow a sizer by a positive delta and adjust neighbors.
   */
  export
  function growSizer(object: SequenceOrArrayLike<BoxSizer>, index: number, delta: number): void {
    // Cast the object to a sequence of sizers.
    let sizers = asSequence(object);

    // Compute how much the items to the left can expand.
    let growLimit = 0;
    for (let i = 0; i <= index; ++i) {
      let sizer = sizers.at(i);
      growLimit += sizer.maxSize - sizer.size;
    }

    // Compute how much the items to the right can shrink.
    let shrinkLimit = 0;
    for (let i = index + 1, n = sizers.length; i < n; ++i) {
      let sizer = sizers.at(i);
      shrinkLimit += sizer.size - sizer.minSize;
    }

    // Clamp the delta adjustment to the limits.
    delta = Math.min(delta, growLimit, shrinkLimit);

    // Grow the sizers to the left by the delta.
    let grow = delta;
    for (let i = index; i >= 0 && grow > 0; --i) {
      let sizer = sizers.at(i);
      let limit = sizer.maxSize - sizer.size;
      if (limit >= grow) {
        sizer.sizeHint = sizer.size + grow;
        grow = 0;
      } else {
        sizer.sizeHint = sizer.size + limit;
        grow -= limit;
      }
    }

    // Shrink the sizers to the right by the delta.
    let shrink = delta;
    for (let i = index + 1, n = sizers.length; i < n && shrink > 0; ++i) {
      let sizer = sizers.at(i);
      let limit = sizer.size - sizer.minSize;
      if (limit >= shrink) {
        sizer.sizeHint = sizer.size - shrink;
        shrink = 0;
      } else {
        sizer.sizeHint = sizer.size - limit;
        shrink -= limit;
      }
    }
  }

  /**
   * Shrink a sizer by a positive delta and adjust neighbors.
   */
  export
  function shrinkSizer(object: SequenceOrArrayLike<BoxSizer>, index: number, delta: number): void {
    // Cast the object to a sequence of sizers.
    let sizers = asSequence(object);

    // Compute how much the items to the right can expand.
    let growLimit = 0;
    for (let i = index + 1, n = sizers.length; i < n; ++i) {
      let sizer = sizers.at(i);
      growLimit += sizer.maxSize - sizer.size;
    }

    // Compute how much the items to the left can shrink.
    let shrinkLimit = 0;
    for (let i = 0; i <= index; ++i) {
      let sizer = sizers.at(i);
      shrinkLimit += sizer.size - sizer.minSize;
    }

    // Clamp the delta adjustment to the limits.
    delta = Math.min(delta, growLimit, shrinkLimit);

    // Grow the sizers to the right by the delta.
    let grow = delta;
    for (let i = index + 1, n = sizers.length; i < n && grow > 0; ++i) {
      let sizer = sizers.at(i);
      let limit = sizer.maxSize - sizer.size;
      if (limit >= grow) {
        sizer.sizeHint = sizer.size + grow;
        grow = 0;
      } else {
        sizer.sizeHint = sizer.size + limit;
        grow -= limit;
      }
    }

    // Shrink the sizers to the left by the delta.
    let shrink = delta;
    for (let i = index; i >= 0 && shrink > 0; --i) {
      let sizer = sizers.at(i);
      let limit = sizer.size - sizer.minSize;
      if (limit >= shrink) {
        sizer.sizeHint = sizer.size - shrink;
        shrink = 0;
      } else {
        sizer.sizeHint = sizer.size - limit;
        shrink -= limit;
      }
    }
  }
}
