/*
 * File    : BoxBag.java
 * Created : 10-sep-2001 10:13
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details (see the LICENSE file).
 */

package edu.xtec.jclic.boxes;

import edu.xtec.jclic.Activity;
import edu.xtec.jclic.Constants;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ImageObserver;
import java.util.ArrayList;
import javax.swing.JComponent;

/**
 * A BoxBag is a class derived from {@link edu.xtec.jclic.boxes.AbstractBox}
 * that contains a collection of "boxes" (objects also derived from
 * AbstractBox). The boxes are stores into a protected
 * {@link java.util.ArrayList}. The class implements methods to add, remove and
 * retrieve boxes, and to manage some of its properties like visibility, status,
 * location and size.
 *
 * @author Francesc Busquets (fbusquets@xtec.cat)
 * @version 13.09.23
 */
public class BoxBag extends AbstractBox implements Cloneable, Resizable {

  protected ArrayList<AbstractBox> cells = new ArrayList<AbstractBox>(12);
  protected Rectangle2D preferredBounds = new Rectangle2D.Double();
  protected AbstractBox backgroundBox = null;

  /** Creates new ActiveBoxBag */
  public BoxBag(AbstractBox parent, JComponent container, BoxBase boxBase) {
    super(parent, container, boxBase);
    preferredBounds.setRect(getBounds());
  }

  @Override
  public Object clone() {
    BoxBag dBB = (BoxBag) super.clone();
    dBB.preferredBounds = (Rectangle2D) preferredBounds.clone();
    dBB.cells = new ArrayList<AbstractBox>();
    for (int i = 0; i < cells.size(); i++)
      dBB.cells.add((AbstractBox) getBox(i).clone());
    if (backgroundBox != null) {
      dBB.backgroundBox = (AbstractBox) backgroundBox.clone();
      dBB.backgroundBox.setParent(dBB);
    }
    return dBB;
  }

  @Override
  public void setContainer(JComponent newContainer) {
    super.setContainer(newContainer);
  }

  @Override
  public void end() {
    for (int i = 0; i < cells.size(); i++)
      getBox(i).end();
    if (backgroundBox != null) {
      backgroundBox.end();
      backgroundBox = null;
    }
    cells.clear();
    super.end();
  }

  public Dimension getPreferredSize() {
    return preferredBounds.getBounds().getSize();
  }

  public Dimension getMinimumSize() {
    Dimension d = getPreferredSize();
    return new Dimension(Math.max(Constants.MIN_CELL_SIZE, d.width), Math.max(Constants.MIN_CELL_SIZE, d.height));
  }

  public Dimension getScaledSize(double scale) {
    Dimension d = getPreferredSize();
    return new Dimension((int) (scale * d.width), (int) (scale * d.height));
  }

  public void ensureCapacity(int n) {
    cells.ensureCapacity(n);
  }

  public void addBox(AbstractBox bx) {
    cells.add(bx);
    bx.setParent(this);
    if (cells.size() == 1) {
      super.setBounds(bx);
    } else {
      add(bx);
    }
    preferredBounds.setRect(getBounds());
  }

  public int boxIndex(Object bx) {
    return bx == null ? -1 : cells.indexOf(bx);
  }

  public AbstractBox getBox(int id) {
    return (id < 0 || id >= cells.size()) ? null : (AbstractBox) cells.get(id);
  }

  public AbstractBox getBackgroundBox() {
    return backgroundBox;
  }

  public void setBackgroundBox(AbstractBox bx) {
    backgroundBox = bx;
    if (backgroundBox != null) {
      add(backgroundBox);
      backgroundBox.setParent(this);
    }
    preferredBounds.setRect(getBounds());
  }

  public void recalcSize() {
    Rectangle2D r = new Rectangle2D.Double(x, y, 0, 0);
    if (backgroundBox != null)
      r.add(backgroundBox);
    for (int i = 0; i < cells.size(); i++)
      r.add((AbstractBox) (cells.get(i)));
    preferredBounds.setRect(r);
    x = r.getX();
    y = r.getY();
    width = r.getWidth();
    height = r.getHeight();
  }

  public int getNumCells() {
    return cells.size();
  }

  @Override
  public void setBorder(boolean newVal) {
    for (int i = 0; i < cells.size(); i++)
      getBox(i).setBorder(newVal);
  }

  @Override
  public void setVisible(boolean newVal) {
    for (int i = 0; i < cells.size(); i++)
      getBox(i).setVisible(newVal);
  }

  @Override
  public void setAlternative(boolean newVal) {
    super.setAlternative(newVal);
    for (int i = 0; i < cells.size(); i++)
      getBox(i).setAlternative(newVal);
  }

  @Override
  public void setBoxBase(BoxBase setBb) {
    super.setBoxBase(setBb);
  }

  @Override
  public void setBounds(Rectangle2D r) {
    if (!r.isEmpty() && !r.equals(this)) {
      double scaleW = r.getWidth() / width;
      double scaleH = r.getHeight() / height;
      double dx = r.getX() - x;
      double dy = r.getY() - y;
      for (int i = 0; i < cells.size(); i++) {
        AbstractBox bx = getBox(i);
        Point2D.Double p = new Point2D.Double(bx.x - x, bx.y - y);
        bx.setBounds(dx + x + scaleW * p.x, dy + y + scaleH * p.y, scaleW * bx.width, scaleH * bx.height);
      }
      if (backgroundBox != null) {
        AbstractBox bx = backgroundBox;
        Point2D.Double p = new Point2D.Double(bx.x - x, bx.y - y);
        bx.setBounds(dx + x + scaleW * p.x, dy + y + scaleH * p.y, scaleW * bx.width, scaleH * bx.height);
      }
    }
    super.setBounds(r);
  }

  @Override
  public boolean update(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io) {

    if (isEmpty() || !isVisible() || isTemporaryHidden())
      return false;
    if (!intersects(dirtyRegion))
      return false;

    if (backgroundBox != null)
      backgroundBox.update(g2, dirtyRegion, io);

    AbstractBox bx;
    for (int i = 0; i < cells.size(); i++)
      if (!((bx = getBox(i)).isMarked()))
        bx.update(g2, dirtyRegion, io);
    for (int i = 0; i < cells.size(); i++)
      if ((bx = getBox(i)).isMarked())
        bx.update(g2, dirtyRegion, io);
    return true;
  }

  public boolean updateContent(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io) {
    return true;
  }

  public AbstractBox findBox(Point2D p) {
    for (int i = cells.size() - 1; i >= 0; i--) {
      AbstractBox bx = getBox(i);
      if (bx.isVisible() && bx.contains(p))
        return bx;
    }
    return null;
  }

  public int countInactiveCells() {
    int n = 0;
    for (int i = 0; i < cells.size(); i++) {
      if (getBox(i).isInactive())
        n++;
    }
    return n;
  }

  public static Dimension layoutSingle(Dimension preferredMaxSize, Resizable rs, int margin) {

    // Avoid exceptions when rs is null
    if (rs == null)
      return preferredMaxSize;

    // optimal dimension
    Dimension d = rs.getPreferredSize();
    // minimal dimension
    Dimension minSize = rs.getMinimumSize();
    // maximal dimension
    Dimension maxSize = preferredMaxSize;
    // remove margins
    maxSize.width -= 2 * margin;
    maxSize.height -= 2 * margin;
    // correct maxSize if less than minSize
    if (minSize.width > maxSize.width || minSize.height > maxSize.height) {
      maxSize = minSize;
    }
    // compute scale factor
    double scale = 1;
    if (d.width > maxSize.width) {
      scale = (double) maxSize.width / d.width;
    }
    if ((scale * d.height) > maxSize.height) {
      scale = (double) maxSize.height / d.height;
    }
    // resize bg
    d = rs.getScaledSize(scale);
    rs.setBounds(margin, margin, d.width, d.height);
    // restore margins
    d.width += 2 * margin;
    d.height += 2 * margin;

    return d;
  }

  public static Dimension layoutDouble(Dimension desiredMaxSize, Resizable rsA, Resizable rsB, int boxGridPos,
      int margin) {
    // number of horizontally and vertically grids
    boolean isHLayout = false;
    int nbh = 1, nbv = 1;
    switch (boxGridPos) {
    case Activity.AB:
    case Activity.BA:
      nbh = 2;
      nbv = 1;
      isHLayout = true;
      break;
    case Activity.AUB:
    case Activity.BUA:
      nbh = 1;
      nbv = 2;
      isHLayout = false;
      break;
    }
    Rectangle2D ra = rsA.getBounds2D();
    Rectangle2D rb = rsB.getBounds2D();
    // optimal dimensions
    Dimension da = rsA.getPreferredSize();
    Dimension db = rsB.getPreferredSize();
    Dimension d = new Dimension(isHLayout ? da.width + db.width : Math.max(da.width, db.width),
        isHLayout ? Math.max(da.height, db.height) : da.height + db.height);
    // minimal dimensions
    Dimension minSizeA = rsA.getMinimumSize();
    Dimension minSizeB = rsB.getMinimumSize();
    Dimension minSize = new Dimension(
        isHLayout ? minSizeA.width + minSizeB.width : Math.max(minSizeA.width, minSizeB.width),
        isHLayout ? Math.max(minSizeA.height, minSizeB.height) : minSizeA.height + minSizeB.height);
    // maximal dimension
    Dimension maxSize = desiredMaxSize;
    // remove margins
    maxSize.width -= (1 + nbh) * margin;
    maxSize.height -= (1 + nbv) * margin;
    // correct maxSize if less than minSize
    if (minSize.width > maxSize.width || minSize.height > maxSize.height) {
      maxSize.setSize(minSize);
    }
    // compute scale factor
    double scale = 1;
    if (d.width > maxSize.width) {
      scale = (double) maxSize.width / d.width;
    }
    if ((scale * d.height) > maxSize.height) {
      scale = (double) maxSize.height / d.height;
    }
    // correct possible minimal infractions
    // ...
    // resize
    da = rsA.getScaledSize(scale);
    db = rsB.getScaledSize(scale);

    // margins to center one box relatove to the other
    int dah, dav, dbh, dbv;
    dah = db.width > da.width ? (db.width - da.width) / 2 : 0;
    dbh = da.width > db.width ? (da.width - db.width) / 2 : 0;
    dav = db.height > da.height ? (db.height - da.height) / 2 : 0;
    dbv = da.height > db.height ? (da.height - db.height) / 2 : 0;

    switch (boxGridPos) {
    case Activity.AB:
      rsA.setBounds(margin, margin + dav, da.width, da.height);
      rsB.setBounds(2 * margin + da.width, margin + dbv, db.width, db.height);
      break;
    case Activity.BA:
      rsB.setBounds(margin, margin + dbv, db.width, db.height);
      rsA.setBounds(2 * margin + db.width, margin + dav, da.width, da.height);
      break;
    case Activity.AUB:
      rsA.setBounds(margin + dah, margin, da.width, da.height);
      rsB.setBounds(margin + dbh, 2 * margin + da.height, db.width, db.height);
      break;
    case Activity.BUA:
      rsB.setBounds(margin + dbh, margin, db.width, db.height);
      rsA.setBounds(margin + dah, 2 * margin + db.height, da.width, da.height);
      break;
    default:
      rsA.setBounds((int) (margin + scale * ra.getX()), (int) (margin + scale * ra.getY()), da.width, da.height);
      rsB.setBounds((int) (margin + scale * rb.getX()), (int) (margin + scale * rb.getY()), da.width, da.height);
      break;
    }

    // recompute d adding margins
    Rectangle r = new Rectangle(rsA.getBounds());
    r.add(rsB.getBounds());
    d.width = r.width + 2 * margin;
    d.height = r.height + 2 * margin;

    return d;
  }
}
