// Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/**
 * Implements a grid-based layout system based on:
 * [http://dev.w3.org/csswg/css3-grid-align/]
 *
 * This layout is designed to support animations and work on browsers that
 * don't support grid natively. As such, we implement it on top of absolute
 * positioning.
 */
// TODO(jmesserly): the DOM integration still needs work:
//  - The grid assumes it is absolutely positioned in its container.
//    Becasue of that, the grid doesn't work right unless it has at least one
//    fractional size in each dimension. In other words, only "top down" grids
//    work at the moment, because the grid can't determine its own size.
//    The core algorithm supports computing min breadth; the issue is about how
//    to integrate it into our View layer.
//  - Unless a child element is "display: inline-block" we can't get its
//    horizontal content size.
//  - Once we set an element's size to "position: absolute", we lose the
//    ability to get its original content size. If the width or height gets
//    set to something other than the content size, we can't recover the
//    original content size.
//  - There's some rounding to ints when we want to set the positions of our
//    tracks. I don't think we necessarily need to do that.
//
// TODO(jmesserly): Some features of the spec are unimplemented:
//  - grid-flow & items that have row and column set to 'auto'.
//  - Grid writing modes (right to left languages, etc)
//  - We don't do a second calculation pass if min content size of a grid-item
//    changes due to column width.
//  - The CSS parsing is not 100% complete, see the parser TODOs.
//  - We don't implement error recovery for invalid combinations of CSS
//    properties, or invalid CSS property values. Instead we throw an error.
//
// TODO(jmesserly): high level performance optimizations we could do:
//  - Optimize for the common case of spanCount = 1
//  - Optimize for the vbox/hbox case (1 row or 1 column)
//  - Optimize for the case of no content sized tracks
//  - Optimize for the "incremental update" cases
class GridLayout extends ViewLayout {

  /** Configuration parameters defined in CSS. */
  final GridTrackList rows;
  final GridTrackList columns;
  final GridTemplate template;

  /** The default sizing for new rows. */
  final TrackSizing rowSizing;

  /** The default sizing for new columns. */
  final TrackSizing columnSizing;

  /**
   * This stores the grid's size during a layout.
   * Used for rows/columns with % or fr units.
   */
  int _gridWidth, _gridHeight;

  /**
   * During a layout, this stores all row/column size information.
   * Because grid-items can implicitly specify their own rows/columns, we can't
   * compute this until we know the set of items.
   */
  List<GridTrack> _rowTracks, _columnTracks;

  /** During a layout, tracks which dimension we're processing. */
  Dimension _dimension;

  GridLayout(Positionable view)
    : super(view),
      rows = _GridTrackParser.parse(view.customStyle['grid-rows']),
      columns = _GridTrackParser.parse(view.customStyle['grid-columns']),
      template = _GridTemplateParser.parse(view.customStyle['grid-template']),

      rowSizing = _GridTrackParser.parseTrackSizing(
          view.customStyle['grid-row-sizing']),

      columnSizing = _GridTrackParser.parseTrackSizing(
          view.customStyle['grid-column-sizing']) {

    _rowTracks = rows != null ? rows.tracks : new List<GridTrack>();
    _columnTracks = columns != null ? columns.tracks : new List<GridTrack>();
  }

  int get currentWidth() => _gridWidth;
  int get currentHeight() => _gridHeight;

  /** The main entry point for layout computation. */
  bool measureLayout(int width, int height) {
    _gridWidth = width;
    _gridHeight = height;

    _ensureAllTracks();

    if (_rowTracks.length < 1 || _columnTracks.length < 1) {
      return false; // nothing to do
    }

    _measureTracks();
    _setBoundsOfChildren();
    return true;
  }

  /**
   * The top level measurement function.
   * [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks]
   */
  void _measureTracks() {
    // Resolve logical width, then height. Width comes first so we can use
    // the width when determining the content-sized height.
    try {
      _dimension = Dimension.WIDTH;
      _computeUsedBreadthOfTracks(_columnTracks);
      _dimension = Dimension.HEIGHT;
      _computeUsedBreadthOfTracks(_rowTracks);
    } finally {
      _dimension = null;
    }

    // TODO(jmesserly): we're supposed to detect a min-content size change
    // due to our computed width and trigger a new layout.
    // How do we implement that?
  }

  num _getRemainingSpace(List<GridTrack> tracks) {
    num remaining = _getGridContentSize();
    remaining -= CollectionUtils.sum(tracks, (t) => t.usedBreadth);
    return Math.max(0, remaining);
  }

  /**
   * This is the core Grid Track sizing algorithm. It is run for Grid columns
   * and Grid rows. The goal of the function is to ensure:
   *   1. That each Grid Track satisfies its minSizing
   *   2. That each Grid Track grows from the breadth which satisfied its
   *      minSizing to a breadth which satifies its
   *      maxSizing, subject to RemainingSpace.
   */
  // Note: spec does not correctly doc all the parameters to this function.
  void _computeUsedBreadthOfTracks(List<GridTrack> tracks) {

    // TODO(jmesserly): as a performance optimization we could cache this
    final items = CollectionUtils.map(view.childViews, (view) => view.layout);
    CollectionUtils.sortBy(items, (item) => _getSpanCount(item));

    // 1. Initialize per Grid Track variables
    for (final t in tracks) {
      // percentage or length sizing functions will return a value
      // min-content, max-content, or a fraction will be set to 0
      t.usedBreadth = t.minSizing.resolveLength(_getGridContentSize());
      t.maxBreadth = t.maxSizing.resolveLength(_getGridContentSize());
      t.updatedBreadth = 0;
    }

    // 2. Resolve content-based MinTrackSizingFunctions
    final USED_BREADTH = const _UsedBreadthAccumulator();
    final MAX_BREADTH = const _MaxBreadthAccumulator();

    _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH);

    _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH);

    // 3. Ensure that maxBreadth is as big as usedBreadth for each track
    for (final t in tracks) {
      if (t.maxBreadth < t.usedBreadth) {
        t.maxBreadth = t.usedBreadth;
      }
    }

    // 4. Resolve content-based MaxTrackSizingFunctions
    _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH);

    _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH);

    // 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their
    //    maxBreadth value until RemainingSpace is exhausted.
    // Note: it's not spec'd what to pass as the accumulator, but usedBreadth
    // seems right.
    _distributeSpaceToTracks(tracks, _getRemainingSpace(tracks),
        USED_BREADTH, false);

    // Spec wording is confusing about which direction this assignment happens,
    // but this is the way that makes sense.
    for (final t in tracks) {
      t.usedBreadth = t.updatedBreadth;
    }

    // 6. Grow all Grid Tracks having a fraction as their maxSizing
    final tempBreadth = _calcNormalizedFractionBreadth(tracks);
    for (final t in tracks) {
      t.usedBreadth = Math.max(t.usedBreadth,
          tempBreadth * t.maxSizing.fractionValue);
    }

    _computeTrackPositions(tracks);
  }

  /**
   * Final steps to finish positioning tracks. Takes the track size and uses
   * it to get start and end positions. Also rounds the positions to integers.
   */
  void _computeTrackPositions(List<GridTrack> tracks) {
    // Compute start positions of tracks, as well as the final position

    num position = 0;
    for (final t in tracks) {
      t.start = position;
      position += t.usedBreadth;
    }

    // Now, go through and round each position to an integer. Then
    // compute the sizes based on those integers.
    num finalPosition = position;

    for (int i = 0; i < tracks.length; i++) {
      int startEdge = tracks[i].start;
      int endEdge;
      if (i < tracks.length - 1) {
        endEdge = tracks[i + 1].start.round().toInt();
        tracks[i + 1].start = endEdge;
      } else {
        endEdge = finalPosition.round().toInt();
      }
      int breadth = endEdge - startEdge;

      // check that we're not off by >= 1px.
      assert((endEdge - startEdge - tracks[i].usedBreadth).abs() < 1);

      tracks[i].usedBreadth = breadth;
    }
  }

  /**
   * This method computes a '1fr' value, referred to as the
   * tempBreadth, for a set of Grid Tracks. The value computed
   * will ensure that when the tempBreadth is multiplied by the
   * fractions associated with tracks, that the UsedBreadths of tracks
   * will increase by an amount equal to the maximum of zero and the specified
   * freeSpace less the sum of the current UsedBreadths.
   */
  num _calcNormalizedFractionBreadth(List<GridTrack> tracks) {

    final fractionTracks = tracks.filter((t) => t.maxSizing.isFraction);

    // Note: the spec has various bugs in this function, such as mismatched
    // identifiers and names that aren't defined. For the most part it's
    // possible to figure out the meaning. It's also a bit confused about
    // how to compute spaceNeededFromFractionTracks, but that should just be the
    // set to the remaining free space after usedBreadth is accounted for.

    // We use the tempBreadth field to store the normalized fraction breadth
    for (final t in fractionTracks) {
      t.tempBreadth = t.usedBreadth / t.maxSizing.fractionValue;
    }

    CollectionUtils.sortBy(fractionTracks, (t) => t.tempBreadth);

    num spaceNeededFromFractionTracks = _getRemainingSpace(tracks);
    num currentBandFractionBreadth = 0;
    num accumulatedFractions = 0;
    for (final t in fractionTracks) {
      if (t.tempBreadth != currentBandFractionBreadth) {
        if (t.tempBreadth * accumulatedFractions >
            spaceNeededFromFractionTracks) {
          break;
        }
        currentBandFractionBreadth = t.tempBreadth;
      }
      accumulatedFractions += t.maxSizing.fractionValue;
      spaceNeededFromFractionTracks += t.usedBreadth;
    }
    return spaceNeededFromFractionTracks / accumulatedFractions;
  }

  /**
   * Ensures that for each Grid Track in tracks, a value will be
   * computed, updatedBreadth, that represents the Grid Track's share of
   * freeSpace.
   */
  void _distributeSpaceToTracks(List<GridTrack> tracks, num freeSpace,
      _BreadthAccumulator breadth, bool ignoreMaxBreadth) {

    // TODO(jmesserly): in some cases it would be safe to sort the passed in
    // list in place. Not always though.
    tracks = CollectionUtils.orderBy(tracks,
        (t) => t.maxBreadth - breadth.getSize(t));

    // Give each Grid Track an equal share of the space, but without exceeding
    // their maxBreadth values. Because there are different MaxBreadths
    // assigned to the different Grid Tracks, this can result in uneven growth.
    for (int i = 0; i < tracks.length; i++) {
      num share = freeSpace / (tracks.length - i);
      share = Math.min(share, tracks[i].maxBreadth);
      tracks[i].tempBreadth = share;
      freeSpace -= share;
    }

    // If the first loop completed having grown every Grid Track to its
    // maxBreadth, and there is still freeSpace, then divide that space
    // evenly and assign it to each Grid Track without regard for its
    // maxBreadth. This phase of growth will always be even, but only occurs
    // when the ignoreMaxBreadth flag is true.
    if (freeSpace > 0 && ignoreMaxBreadth) {
      for (int i = 0; i < tracks.length; i++) {
        final share = freeSpace / (tracks.length - i);
        tracks[i].tempBreadth += share;
        freeSpace -= share;
      }
    }

    // Note: the spec has us updating all grid tracks, not just the passed in
    // tracks, but I think that's a spec bug.
    for (final t in tracks) {
      t.updatedBreadth = Math.max(t.updatedBreadth, t.tempBreadth);
    }
  }

  /**
   * This function prioritizes the distribution of space driven by Grid Items
   * in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid
   * Items having a lower spanCount have an opportunity to increase the size of
   * the Grid Tracks they cover before those with larger SpanCounts.
   *
   * Note: items are assumed to be already sorted in increasing span count
   */
  void _distributeSpaceBySpanCount(List<ViewLayout> items,
      ContentSizeMode sizeMode, _BreadthAccumulator breadth) {

    items = items.filter((item) =>
        _hasContentSizedTracks(_getTracks(item), sizeMode, breadth));

    var tracks = [];

    for (int i = 0; i < items.length; i++) {
      final item = items[i];

      final itemTargetSize = item.measureContent(this, _dimension, sizeMode);

      final spannedTracks = _getTracks(item);
      _distributeSpaceToTracks(spannedTracks, itemTargetSize, breadth, true);

      // Remember that we need to update the sizes on these tracks
      tracks.addAll(spannedTracks);

      // Each time we transition to a new spanCount, update any modified tracks
      bool spanCountFinished = false;
      if (i + 1 == items.length) {
        spanCountFinished = true;
      } else if (_getSpanCount(item) != _getSpanCount(items[i + 1])) {
        spanCountFinished = true;
      }

      if (spanCountFinished) {
        for (final t in tracks) {
          breadth.setSize(t,
            Math.max(breadth.getSize(t), t.updatedBreadth));
        }
        tracks = [];
      }
    }
  }

  /**
   * Returns true if we have an appropriate content sized dimension, and don't
   * cross a fractional track.
   */
  static bool _hasContentSizedTracks(Collection<GridTrack> tracks,
      ContentSizeMode sizeMode, _BreadthAccumulator breadth) {

    for (final t in tracks) {
      final fn = breadth.getSizingFunction(t);
      if (sizeMode == ContentSizeMode.MAX && fn.isMaxContentSized ||
          sizeMode == ContentSizeMode.MIN && fn.isContentSized) {

        // Make sure we don't cross a fractional track
        return tracks.length == 1 || !tracks.some((t) => t.isFractional);
      }
    }
    return false;
  }

  /** Ensures that the numbered track exists. */
  void _ensureTrack(List<GridTrack> tracks, TrackSizing sizing,
      int start, int span) {
    // Start is 1-based. Make it 0-based.
    start -= 1;

    // Grow the list if needed
    int length = start + span;
    int first = Math.min(start, tracks.length);
    tracks.length = Math.max(tracks.length, length);

    // Fill in tracks
    for (int i = first; i < length; i++) {
      if (tracks[i] == null) {
        tracks[i] = new GridTrack(sizing);
      }
    }
  }

  /**
   * Scans children creating GridLayoutParams as needed, and creates all of the
   * rows and columns that we will need.
   *
   * Note: this can potentially create new rows/columns, so this needs to be
   * run before the track sizing algorithm.
   */
  void _ensureAllTracks() {
    final items = CollectionUtils.map(view.childViews, (view) => view.layout);

    for (final child in items) {
      if (child.layoutParams == null) {
        final p = new GridLayoutParams(child.view, this);
        _ensureTrack(_rowTracks, rowSizing, p.row, p.rowSpan);
        _ensureTrack(_columnTracks, columnSizing, p.column, p.columnSpan);
        child.layoutParams = p;
      }
    }
  }

  /**
   * Given the track sizes that were computed, position children in the grid.
   */
  void _setBoundsOfChildren() {
    final items = CollectionUtils.map(view.childViews, (view) => view.layout);

    for (final item in items) {
      GridLayoutParams childLayout = item.layoutParams;
      var xPos = _getTrackLocationX(childLayout);
      var yPos = _getTrackLocationY(childLayout);

      int left = xPos.start, width = xPos.length;
      int top = yPos.start, height = yPos.length;

      // Somewhat counterintuitively (at least to me):
      //   grid-col-align is the horizontal alignment
      //   grid-row-align is the vertical alignment
      xPos = childLayout.columnAlign.align(xPos, item.currentWidth);
      yPos = childLayout.rowAlign.align(yPos, item.currentHeight);

      item.setBounds(xPos.start, yPos.start, xPos.length, yPos.length);
    }
  }

  num _getGridContentSize() {
    switch (_dimension) {
      case Dimension.WIDTH:
        return _gridWidth;
      case Dimension.HEIGHT:
        return _gridHeight;
    }
  }

  _GridLocation _getTrackLocationX(GridLayoutParams childLayout) {
    int start = childLayout.column - 1;
    int end = start + childLayout.columnSpan - 1;

    start = _columnTracks[start].start;
    end = _columnTracks[end].end;

    return new _GridLocation(start, end - start);
  }

  _GridLocation _getTrackLocationY(GridLayoutParams childLayout) {
    int start = childLayout.row - 1;
    int end = start + childLayout.rowSpan - 1;

    start = _rowTracks[start].start;
    end = _rowTracks[end].end;

    return new _GridLocation(start, end - start);
  }

  /** Gets the tracks that this item crosses. */
  // TODO(jmesserly): might be better to return an iterable
  List<GridTrack> _getTracks(ViewLayout item) {
    GridLayoutParams childLayout = item.layoutParams;

    int start, span;
    List<GridTrack> tracks;
    switch (_dimension) {
      case Dimension.WIDTH:
        start = childLayout.column - 1;
        span = childLayout.columnSpan;
        tracks = _columnTracks;
        break;
      case Dimension.HEIGHT:
        start = childLayout.row - 1;
        span = childLayout.rowSpan;
        tracks = _rowTracks;
    }

    assert(start >= 0 && span >= 1);

    final result = new List<GridTrack>(span);
    for (int i = 0; i < span; i++) {
      result[i] = tracks[start + i];
    }
    return result;
  }

  int _getSpanCount(ViewLayout item) {
    GridLayoutParams childLayout = item.layoutParams;
    return (_dimension == Dimension.WIDTH ?
        childLayout.columnSpan : childLayout.rowSpan);
  }
}
