include plain_scaling;

// After a transformation, produce new coordinate bounds.  For paths that
// have been added, this is only an approximation since it takes the bounds of
// their transformed bounding box.
private void addTransformedCoords(coords2 dest, transform t,
                                  coords2 point, coords2 min, coords2 max)
{
  dest.push(t, point, point);

  // Add in all 4 corner coords, to properly size rectangular pictures.
  dest.push(t,min,min);
  dest.push(t,min,max);
  dest.push(t,max,min);
  dest.push(t,max,max);
}

// Adds another sizing restriction to the coordinates, but only if it is
// maximal, that is, if under some scaling, this coordinate could be the
// largest.
private void addIfMaximal(coord[] coords, real user, real truesize) {
  // TODO: Test promoting coordinates for efficiency.

  for (coord c : coords)
    if (user <= c.user && truesize <= c.truesize)
      // Not maximal.
      return;

  // The coordinate is not dominated by any existing extreme, so it is
  // maximal and will be added, but first remove any coords it now dominates.
  int i = 0;
  while (i < coords.length) {
    coord c = coords[i];
    if (c.user <= user && c.truesize <= truesize)
      coords.delete(i);
    else
      ++i;
  }

  // Add the coordinate to the extremes.
  coords.push(coord.build(user, truesize));
}

private void addIfMaximal(coord[] dest, coord[] src)
{
  // This may be inefficient, as it rebuilds the coord struct when adding it.
  for (coord c : src)
    addIfMaximal(dest, c.user, c.truesize);
}

// Same as addIfMaximal, but testing for minimal coords.
private void addIfMinimal(coord[] coords, real user, real truesize) {
  for (coord c : coords)
    if (user >= c.user && truesize >= c.truesize)
      return;

  int i = 0;
  while (i < coords.length) {
    coord c = coords[i];
    if (c.user >= user && c.truesize >= truesize)
      coords.delete(i);
    else
      ++i;
  }

  coords.push(coord.build(user, truesize));
}

private void addIfMinimal(coord[] dest, coord[] src)
{
  for (coord c : src)
    addIfMinimal(dest, c.user, c.truesize);
}

// This stores a list of sizing bounds for picture data.  If the object is
// frozen, then it cannot be modified further, and therefore can be safely
// passed by reference and stored in the sizing data for multiple pictures.
private struct freezableBounds {
  restricted bool frozen = false;
  void freeze() {
    frozen = true;
  }

  // Optional links to further (frozen) sizing data.
  private freezableBounds[] links;

  // Links to (frozen) sizing data that is transformed when added here.
  private static struct transformedBounds {
    transform t;
    freezableBounds link;
  };
  private transformedBounds[] tlinks;

  // The sizing data.  It cannot be modified once this object is frozen.
  private coords2 point, min, max;

  // A bound represented by a path.  Using the path instead of the bounding
  // box means it will be accurate after a transformation by coordinates.
  private path[] pathBounds;

  // A bound represented by a path and a pen.
  // As often many paths use the same pen, we store an array of paths.
  private static struct pathpen {
    path[] g; pen p;

    void operator init(path g, pen p) {
      this.g.push(g);
      this.p = p;
    }
  }
  private static pathpen operator *(transform t, pathpen pp) {
    // Should the pen be transformed?
    pathpen newpp;
    for (path g : pp.g)
      newpp.g.push(t*g);
    newpp.p = pp.p;
    return newpp;
  }

  // WARNING: Due to crazy optimizations, if this array is changed between an
  // empty and non-empty state, the assignment of a method to
  // addPath(path,pen) must also change.
  private pathpen[] pathpenBounds;

  // Once frozen, the sizing is immutable, and therefore we can compute and
  // store the extremal coordinates.
  public static struct extremes {
    coord[] left, bottom, right, top;

    void operator init(coord[] left, coord[] bottom,
                       coord[] right, coord[] top) {
      this.left = left;
      this.bottom = bottom;
      this.right = right;
      this.top = top;
    }

  }
  private static void addMaxToExtremes(extremes e, pair user, pair truesize) {
    addIfMaximal(e.right, user.x, truesize.x);
    addIfMaximal(e.top, user.y, truesize.y);
  }
  private static void addMinToExtremes(extremes e, pair user, pair truesize) {
    addIfMinimal(e.left, user.x, truesize.x);
    addIfMinimal(e.bottom, user.y, truesize.y);
  }
  private static void addMaxToExtremes(extremes e, coords2 coords) {
    addIfMaximal(e.right, coords.x);
    addIfMaximal(e.top, coords.y);
  }
  private static void addMinToExtremes(extremes e, coords2 coords) {
    addIfMinimal(e.left, coords.x);
    addIfMinimal(e.bottom, coords.y);
  }

  private extremes cachedExtremes = null;

  // Once frozen, getMutable returns a new object based on this one, which can
  // be modified.
  freezableBounds getMutable() {
    assert(frozen);
    var f = new freezableBounds;
    f.links.push(this);
    return f;
  }

  freezableBounds transformed(transform t) {
    // Freeze these bounds, as we are storing a reference to them.
    freeze();

    var tlink = new transformedBounds;
    tlink.t = t;
    tlink.link = this;

    var b = new freezableBounds;
    b.tlinks.push(tlink);

    return b;
  }

  void append(freezableBounds b) {
    // Check that we can modify the object.
    assert(!frozen);

    //TODO: If b is "small", ie. a single tlink or cliplink, just copy the
    //link.

    // As we only reference b, we must freeze it to ensure it does not change.
    b.freeze();
    links.push(b);
  }

  void addPoint(pair user, pair truesize) {
    assert(!frozen);
    point.push(user, truesize);
  }

  void addBox(pair userMin, pair userMax, pair trueMin, pair trueMax) {
    assert(!frozen);
    this.min.push(userMin, trueMin);
    this.max.push(userMax, trueMax);
  }

  void addPath(path g) {
    // This, and other asserts have been removed to speed things up slightly.
    //assert(!frozen);
    this.pathBounds.push(g);
  }

  void addPath(path[] g) {
    //assert(!frozen);
    this.pathBounds.append(g);
  }

  // To squeeze out a bit more performance, this method is either assigned
  // addPathToNonEmptyArray or addPathToEmptyArray depending on the state of
  // the pathpenBounds array.
  void addPath(path g, pen p);

  private void addPathToNonEmptyArray(path g, pen p) {
    //assert(!frozen);
    //assert(!pathpenBounds.empty());
    var pp = pathpenBounds[0];

    // Test if the pens are equal or have the same bounds.
    if (pp.p == p || (min(pp.p) == min(p) && max(pp.p) == max(p))) {
      // If this path has the same pen as the last one, just add it to the
      // array corresponding to that pen.
      pp.g.push(g);
    }
    else {
      // A different pen.  Start a new bound and put it on the front.  Put
      // the old bound at the end of the array.
      pathpenBounds[0] = pathpen(g,p);
      pathpenBounds.push(pp);
    }
  }
  void addPathToEmptyArray(path g, pen p) {
    //assert(!frozen);
    //assert(pathpenBounds.empty());

    pathpenBounds.push(pathpen(g,p));
    addPath = addPathToNonEmptyArray;
  }

  // Initial setting for addPath.
  addPath = addPathToEmptyArray;

  // Transform the sizing info by t then add the result to the coords
  // structure.
  private void accumulateCoords(transform t, coords2 coords) {
    for (var link : links)
      link.accumulateCoords(t, coords);

    for (var tlink : tlinks)
      tlink.link.accumulateCoords(t*tlink.t, coords);

    addTransformedCoords(coords, t, this.point, this.min, this.max);

    for (var g : pathBounds) {
      g = t*g;
      coords.push(min(g), (0,0));
      coords.push(max(g), (0,0));
    }

    for (var pp: pathpenBounds) {
      pair pm = min(pp.p), pM = max(pp.p);
      for (var g : pp.g) {
        g = t*g;
        coords.push(min(g), pm);
        coords.push(max(g), pM);
      }
    }
  }

  // Add all of the sizing info to the given coords structure.
  private void accumulateCoords(coords2 coords) {
    for (var link : links)
      link.accumulateCoords(coords);

    for (var tlink : tlinks)
      tlink.link.accumulateCoords(tlink.t, coords);

    coords.append(this.point);
    coords.append(this.min);
    coords.append(this.max);

    for (var g : pathBounds) {
      coords.push(min(g), (0,0));
      coords.push(max(g), (0,0));
    }

    for (var pp: pathpenBounds) {
      pair pm = min(pp.p), pM = max(pp.p);
      for (var g : pp.g) {
        coords.push(min(g), pm);
        coords.push(max(g), pM);
      }
    }
  }

  // Returns all of the coords that this sizing data represents.
  private coords2 allCoords() {
    coords2 coords;
    accumulateCoords(coords);
    return coords;
  }

  private void addLocalsToExtremes(transform t, extremes e) {
    coords2 coords;
    addTransformedCoords(coords, t, this.point, this.min, this.max);
    addMinToExtremes(e, coords);
    addMaxToExtremes(e, coords);

    if (pathBounds.length > 0) {
      addMinToExtremes(e, minAfterTransform(t, pathBounds), (0,0));
      addMaxToExtremes(e, maxAfterTransform(t, pathBounds), (0,0));
    }

    for (var pp : pathpenBounds) {
      if (pp.g.length > 0) {
        addMinToExtremes(e, minAfterTransform(t, pp.g), min(pp.p));
        addMaxToExtremes(e, maxAfterTransform(t, pp.g), max(pp.p));
      }
    }
  }

  private void addToExtremes(transform t, extremes e) {
    for (var link : links)
      link.addToExtremes(t, e);

    for (var tlink : tlinks)
      tlink.link.addToExtremes(t*tlink.t, e);

    addLocalsToExtremes(t, e);
  }

  private void addLocalsToExtremes(extremes e) {
    addMinToExtremes(e, point);
    addMaxToExtremes(e, point);
    addMinToExtremes(e, min);
    addMaxToExtremes(e, max);

    if (pathBounds.length > 0) {
      addMinToExtremes(e, min(pathBounds), (0,0));
      addMaxToExtremes(e, max(pathBounds), (0,0));
    }

    for(var pp : pathpenBounds) {
      pair m=min(pp.p);
      pair M=max(pp.p);
      for(path gg : pp.g) {
        if (size(gg) > 0) {
          addMinToExtremes(e,min(gg),m);
          addMaxToExtremes(e,max(gg),M);
        }
      }
    }
  }

  private void addToExtremes(extremes e) {
    for (var link : links)
      link.addToExtremes(e);

    for (var tlink : tlinks)
      tlink.link.addToExtremes(tlink.t, e);

    addLocalsToExtremes(e);
  }

  private static void write(extremes e) {
    static void write(coord[] coords) {
      for (coord c : coords)
        write("  " + (string)c.user + " u + " + (string)c.truesize);
    }
    write("left:");
    write(e.left);
    write("bottom:");
    write(e.bottom);
    write("right:");
    write(e.right);
    write("top:");
    write(e.top);
  }

  // Returns the extremal coordinates of the sizing data.
  public extremes extremes() {
    if (cachedExtremes == null) {
      freeze();

      extremes e;
      addToExtremes(e);
      cachedExtremes = e;
    }

    return cachedExtremes;
  }

  // Helper functions for computing the usersize bounds.  usermin and usermax
  // would be easily computable from extremes, except that the picture
  // interface actually allows calls that manually change the usermin and
  // usermax values.  Therefore, we have to compute these values separately.
  private static struct userbounds {
    bool areSet=false;
    pair min;
    pair max;
  }
  private static struct boundsAccumulator {
    pair[] mins;
    pair[] maxs;

    void push(pair m, pair M) {
      mins.push(m);
      maxs.push(M);
    }

    void push(userbounds b) {
      if (b.areSet)
        push(b.min, b.max);
    }

    void push(transform t, userbounds b) {
      if (b.areSet) {
        pair[] box = { t*(b.min.x,b.max.y), t*b.max,
                       t*b.min,             t*(b.max.x,b.min.y) };
        for (var z : box)
          push(z,z);
      }
    }

    void pushUserCoords(coords2 min, coords2 max) {
      int n = min.x.length;
      assert(min.y.length == n);
      assert(max.x.length == n);
      assert(max.y.length == n);

      for (int i = 0; i < n; ++i)
        push((min.x[i].user, min.y[i].user),
             (max.x[i].user, max.y[i].user));
    }

    userbounds collapse() {
      userbounds b;
      if (mins.length > 0) {
        b.areSet = true;
        b.min = minbound(mins);
        b.max = maxbound(maxs);
      }
      else {
        b.areSet = false;
      }
      return b;
    }
  }

  // The user bounds already calculated for this data.
  private userbounds storedUserBounds = null;

  private void accumulateUserBounds(boundsAccumulator acc)
  {
    if (storedUserBounds != null) {
      assert(frozen);
      acc.push(storedUserBounds);
    } else {
      acc.pushUserCoords(point, point);
      acc.pushUserCoords(min, max);
      if (pathBounds.length > 0)
        acc.push(min(pathBounds), max(pathBounds));
      for (var pp : pathpenBounds)
        if(size(pp.g) > 0)
          acc.push(min(pp.g), max(pp.g));
      for (var link : links)
        link.accumulateUserBounds(acc);

      // Transforms are handled as they were in the old system.
      for (var tlink : tlinks) {
        boundsAccumulator tacc;
        tlink.link.accumulateUserBounds(tacc);
        acc.push(tlink.t, tacc.collapse());
      }
    }
  }

  private void computeUserBounds() {
    freeze();
    boundsAccumulator acc;
    accumulateUserBounds(acc);
    storedUserBounds = acc.collapse();
  }

  private userbounds userBounds() {
    if (storedUserBounds == null)
      computeUserBounds();

    assert(storedUserBounds != null);
    return storedUserBounds;
  }

  // userMin/userMax returns the minimal/maximal userspace coordinate of the
  // sizing data.  As coordinates for objects such as labels can have
  // significant truesize dimensions, this userMin/userMax values may not
  // correspond closely to the end of the screen, and are of limited use.
  // userSetx and userSety determine if there is sizing data in order to even
  // have userMin/userMax defined.
  public bool userBoundsAreSet() {
    return userBounds().areSet;
  }

  public pair userMin() {
    return userBounds().min;
  }
  public pair userMax() {
    return userBounds().max;
  }

  // To override the true userMin and userMax bounds, first compute the
  // userBounds as they should be at this point, then change the values.
  public void alterUserBound(string which, real val) {
    // We are changing the bounds data, so it cannot be frozen yet.  After the
    // user bounds are set, however, the sizing data cannot change, so it will
    // be frozen.
    assert(!frozen);
    computeUserBounds();
    assert(frozen);

    var b = storedUserBounds;
    if (which == "minx")
      b.min = (val, b.min.y);
    else if (which == "miny")
      b.min = (b.min.x, val);
    else if (which == "maxx")
      b.max = (val, b.max.y);
    else {
      assert(which == "maxy");
      b.max = (b.max.x, val);
    }
  }

  // A temporary measure.  Stuffs all of the data from the links and paths
  // into the coords.
  private void flatten() {
    assert(!frozen);

    // First, compute the user bounds, taking into account any manual
    // alterations.
    computeUserBounds();

    // Calculate all coordinates.
    coords2 coords = allCoords();

    // Erase all the old data.
    point.erase();
    min.erase();
    max.erase();
    pathBounds.delete();
    pathpenBounds.delete();
    addPath = addPathToEmptyArray;
    links.delete();
    tlinks.delete();

    // Put all of the coordinates into point.
    point = coords;
  }

  void xclip(real Min, real Max) {
    assert(!frozen);
    flatten();
    point.xclip(Min,Max);
    min.xclip(Min,Max);
    max.xclip(Min,Max);

    // Cap the userBounds.
    userbounds b = storedUserBounds;
    b.min = (max(Min, b.min.x), b.min.y);
    b.max = (min(Max, b.max.x), b.max.y);
  }

  void yclip(real Min, real Max) {
    assert(!frozen);
    flatten();
    point.yclip(Min,Max);
    min.yclip(Min,Max);
    max.yclip(Min,Max);

    // Cap the userBounds.
    userbounds b = storedUserBounds;
    b.min = (b.min.x, max(Min, b.min.y));
    b.max = (b.max.x, min(Max, b.max.y));
  }

  // Calculate the min for the final frame, given the coordinate transform.
  pair min(transform t) {
    extremes e = extremes();
    if (e.left.length == 0)
      return 0;

    pair a=t*(1,1)-t*(0,0), b=t*(0,0);
    scaling xs=scaling.build(a.x,b.x);
    scaling ys=scaling.build(a.y,b.y);

    return (min(infinity, xs, e.left), min(infinity, ys, e.bottom));
  }

  // Calculate the max for the final frame, given the coordinate transform.
  pair max(transform t) {
    extremes e = extremes();
    if (e.right.length == 0)
      return 0;

    pair a=t*(1,1)-t*(0,0), b=t*(0,0);
    scaling xs=scaling.build(a.x,b.x);
    scaling ys=scaling.build(a.y,b.y);

    return (max(-infinity, xs, e.right), max(-infinity, ys, e.top));
  }

  // Returns the transform for turning user-space pairs into true-space pairs.
  transform scaling(real xsize, real ysize,
                    real xunitsize, real yunitsize,
                    bool keepAspect, bool warn) {
    if(xsize == 0 && xunitsize == 0 && ysize == 0 && yunitsize == 0)
      return identity();

    // Get the extremal coordinates.
    extremes e = extremes();

    real sx;
    if(xunitsize == 0) {
      if(xsize != 0) sx=calculateScaling("x",e.left,e.right,xsize,warn);
    } else sx=xunitsize;

    /* Possible alternative code :
       real sx = xunitsize != 0 ? xunitsize :
       xsize != 0     ? calculateScaling("x", Coords.x, xsize, warn) :
       0; */

    real sy;
    if(yunitsize == 0) {
      if(ysize != 0) sy=calculateScaling("y",e.bottom,e.top,ysize,warn);
    } else sy=yunitsize;

    if(sx == 0) {
      sx=sy;
      if(sx == 0)
        return identity();
    } else if(sy == 0) sy=sx;


    if(keepAspect && (xunitsize == 0 || yunitsize == 0))
      return scale(min(sx,sy));
    else
      return scale(sx,sy);
  }
}

struct bounds {
  private var base = new freezableBounds;

  // We should probably put this back into picture.
  bool exact = true;

  // Called just before modifying the sizing data.  It ensures base is
  // non-frozen.
  // Note that this is manually inlined for speed reasons in a couple often
  // called methods below.
  private void makeMutable() {
    if (base.frozen)
      base = base.getMutable();
    //assert(!base.frozen); // Disabled for speed reasons.
  }

  void erase() {
    // Just discard the old bounds.
    base = new freezableBounds;

    // We don't reset the 'exact' field, for backward compatibility.
  }

  bounds copy() {
    // Freeze the underlying bounds and make a shallow copy.
    base.freeze();

    var b = new bounds;
    b.base = this.base;
    b.exact = this.exact;
    return b;
  }

  bounds transformed(transform t) {
    var b = new bounds;
    b.base = base.transformed(t);
    b.exact = this.exact;
    return b;
  }

  void append(bounds b) {
    makeMutable();
    base.append(b.base);
  }

  void append(transform t, bounds b) {
    // makeMutable will be called by append.
    if (t == identity())
      append(b);
    else
      append(b.transformed(t));
  }

  void addPoint(pair user, pair truesize) {
    makeMutable();
    base.addPoint(user, truesize);
  }

  void addBox(pair userMin, pair userMax, pair trueMin, pair trueMax) {
    makeMutable();
    base.addBox(userMin, userMax, trueMin, trueMax);
  }

  void addPath(path g) {
    //makeMutable(); // Manually inlined here for speed reasons.
    if (base.frozen)
      base = base.getMutable();
    base.addPath(g);
  }

  void addPath(path[] g) {
    //makeMutable(); // Manually inlined here for speed reasons.
    if (base.frozen)
      base = base.getMutable();
    base.addPath(g);
  }

  void addPath(path g, pen p) {
    //makeMutable(); // Manually inlined here for speed reasons.
    if (base.frozen)
      base = base.getMutable();
    base.addPath(g, p);
  }

  public bool userBoundsAreSet() {
    return base.userBoundsAreSet();
  }
  public pair userMin() {
    return base.userMin();
  }
  public pair userMax() {
    return base.userMax();
  }
  public void alterUserBound(string which, real val) {
    makeMutable();
    base.alterUserBound(which, val);
  }

  void xclip(real Min, real Max) {
    makeMutable();
    base.xclip(Min,Max);
  }

  void yclip(real Min, real Max) {
    makeMutable();
    base.yclip(Min,Max);
  }

  void clip(pair Min, pair Max) {
    // TODO: If the user bounds have been manually altered, they may be
    // incorrect after the clip.
    xclip(Min.x,Max.x);
    yclip(Min.y,Max.y);
  }

  pair min(transform t) {
    return base.min(t);
  }

  pair max(transform t) {
    return base.max(t);
  }

  transform scaling(real xsize, real ysize,
                    real xunitsize, real yunitsize,
                    bool keepAspect, bool warn) {
    return base.scaling(xsize, ysize, xunitsize, yunitsize, keepAspect, warn);
  }
}

bounds operator *(transform t, bounds b) {
  return b.transformed(t);
}
