/*
 * $Id: AbstractTiledOpImpl.java 4359 2008-10-28 22:55:06Z uckelman $
 *
 * Copyright (c) 2007 by Joel Uckelman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */

package VASSAL.tools.imageop;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * The abstract base class for {@link ImageOp}s which support multiple tiles.
 *
 * @since 3.1.0
 * @author Joel Uckelman
 */
public abstract class AbstractTiledOpImpl extends AbstractOpImpl {
  private static final Dimension DEFAULT_TILE_SIZE = new Dimension(256, 256);

  /** The standard size of this <code>ImageOp</code>s tiles. */
  protected Dimension tileSize;

  /** The number of tiles along the x-axis. */
  protected int numXTiles;

  /** The number of tiles along the y-axis. */
  protected int numYTiles;

  /** The tiles already created, stored as <code>y*numXTiles + x</code>. */
  protected ImageOp[] tiles; 

  /**
   * Sets the <code>tileSize</code> which is used by {@link getTileSize},
   * {@link getTileHeight}, {@link getTileWidth}, {@link getNumXTiles},
   * {@link getNumYTiles}, and all other tile methods.
   */
  protected void fixTileSize() {
    if (size == null) fixSize();

    tileSize = DEFAULT_TILE_SIZE;

    numXTiles = (int) Math.ceil((double)size.width/tileSize.width);
    numYTiles = (int) Math.ceil((double)size.height/tileSize.height);

    tiles = new ImageOp[numXTiles*numYTiles];
  }

  /** {@inheritDoc} */
  public Dimension getTileSize() {
    if (tileSize == null) fixTileSize();
    return new Dimension(tileSize);
  }

  /** {@inheritDoc} */
  public int getTileHeight() {
    if (tileSize == null) fixTileSize();
    return tileSize.height;
  }
    
  /** {@inheritDoc} */
  public int getTileWidth() {
    if (tileSize == null) fixTileSize();
    return tileSize.width;
  }

  /** {@inheritDoc} */
  public int getNumXTiles() {
    if (tileSize == null) fixTileSize();
    return numXTiles;
  }
  
  /** {@inheritDoc} */
  public int getNumYTiles() {
    if (tileSize == null) fixTileSize();
    return numYTiles;
  }

  /**
   * Returns the <code>ImageOp</code> which produces tile
   * <code>(tileX,tileY)</code>, creating it if necessary.
   *
   * @return the <code>ImageOp</code> for tile <code>(tileX,tileY)</code>
   */
  public ImageOp getTileOp(int tileX, int tileY) {
    ImageOp top = tiles[tileY*numXTiles + tileX];
    if (top == null) {
      top = tiles[tileY*numXTiles + tileX] = createTileOp(tileX, tileY);
    }
  
    return top;
  }
  
  protected abstract ImageOp createTileOp(int tileX, int tileY);

  /**
   * {@inheritDoc}
   *
   * @throws IndexOutOfBoundsException unless {@code 0 <= tileX < numXTiles}
   * and {@code 0 <= tileY < numYTiles}.
   */
  public BufferedImage getTile(int tileX, int tileY, ImageOpObserver obs)
    throws CancellationException, InterruptedException, ExecutionException
  {
    if (tileX < 0 || tileX >= numXTiles ||
        tileY < 0 || tileY >= numYTiles)
      throw new IndexOutOfBoundsException();

    return getTileOp(tileX, tileY).getImage(obs);
  }

  /**
   * {@inheritDoc}
   *
   * @throws IndexOutOfBoundsException unless {@code 0 <= tileX < numXTiles}
   * and {@code 0 <= tileY < numYTiles}.
   */
  public Future<BufferedImage> getFutureTile(
    int tileX, int tileY, ImageOpObserver obs) throws ExecutionException
  { 
    if (tileX < 0 || tileX >= numXTiles ||
        tileY < 0 || tileY >= numYTiles)
      throw new IndexOutOfBoundsException();

    return getTileOp(tileX, tileY).getFutureImage(obs);
  }

  /**
   * {@inheritDoc}
   *
   * @throws IllegalArgumentException if <code>rect == null</code>.
   */
  public Point[] getTileIndices(Rectangle rect) {
    if (rect == null) throw new IllegalArgumentException();

    if (size == null || tileSize == null) fixTileSize();

// FIXME: maybe do this without creating new Rectangles
    rect = rect.intersection(new Rectangle(size));
    if (rect.isEmpty()) {
      return new Point[0];
    }

    final int minTileX = rect.x/tileSize.width;
    final int minTileY = rect.y/tileSize.height;
    final int maxTileX = (rect.x + rect.width - 1)/tileSize.width;
    final int maxTileY = (rect.y + rect.height - 1)/tileSize.height;
   
    final Point[] tilesInRect =
      new Point[(maxTileX-minTileX+1)*(maxTileY-minTileY+1)];

// FIXME: Maybe do this by keeping a MRU cache of Points.
// Maybe not, profiling shows that this isn't causing the gc to run much.
    int offset = 0;
    for (int ty = minTileY; ty <= maxTileY; ++ty) {
      for (int tx = minTileX; tx <= maxTileX; ++tx) {
        tilesInRect[offset++] = new Point(tx,ty);
      }
    }

    return tilesInRect;
  }
}
