/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2000 - 2009 Pentaho Corporation, Object Refinery Limited and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.pixie.wmf;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.pixie.wmf.records.CommandFactory;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmd;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmdSetWindowExt;
import org.pentaho.reporting.libraries.pixie.wmf.records.MfCmdSetWindowOrg;
import org.pentaho.reporting.libraries.base.util.FastStack;

/**
 * Parses and replays the WmfFile.
 */
public class WmfFile
{
  private static final Log logger = LogFactory.getLog(WmfFile.class);

  public static final int QUALITY_NO = 0;    // Can't convert.
  public static final int QUALITY_MAYBE = 1; // Might be able to convert.
  public static final int QUALITY_YES = 2;   // Can convert.

  // Maximal picture size is 1200x1200. A average wmf file scales easily
  // to 20000 and more, so we have to limit the pixel image's size.

  private static final int MAX_PICTURE_SIZE = getMaxPictureSize();

  private static int getMaxPictureSize()
  {
    return 1200;
  }

  private WmfObject[] objects;
  private FastStack dcStack;
  private MfPalette palette;

  //private String inName;
  private InputStream in;
  private MfHeader header;
  private int fileSize;
  private int filePos;

  private ArrayList records;
  private Graphics2D graphics;

  private int maxWidth;
  private int maxHeight;
  private int imageWidth;
  private int imageHeight;

  private int minX;
  private int minY;
  private int imageX;
  private int imageY;

  /**
   * Initialize metafile for reading from an URL. Width and height will be computed automatically.
   *
   * @param input the URL from where to read.
   * @throws IOException if any other error occured.
   */
  public WmfFile(final URL input)
      throws IOException
  {
    this(input, -1, -1);
  }

  /**
   * Initialize metafile for reading from file. Width and height will be computed automatically.
   *
   * @param input the name of the file from where to read.
   * @throws IOException if any other error occured.
   */
  public WmfFile(final String input)
      throws IOException
  {
    this(input, -1, -1);
  }

  /**
   * Initialize metafile for reading from an URL.
   *
   * @param imageWidth  the target width of the image or -1 for automatic mode.
   * @param imageHeight the target height of the image or -1 for automatic mode.
   * @param input       the URL from where to read.
   * @throws IOException if any other error occured.
   */
  public WmfFile(final URL input, final int imageWidth, final int imageHeight)
      throws IOException
  {
    this(new BufferedInputStream(input.openStream()), imageWidth, imageHeight);
  }

  /**
   * Initialize metafile for reading from filename.
   *
   * @param imageWidth  the target width of the image or -1 for automatic mode.
   * @param imageHeight the target height of the image or -1 for automatic mode.
   * @param inName      the file name from where to read.
   * @throws FileNotFoundException if the file was not found.
   * @throws IOException           if any other error occured.
   */
  public WmfFile(final String inName, final int imageWidth, final int imageHeight)
      throws FileNotFoundException, IOException
  {
    this(new BufferedInputStream(new FileInputStream(inName)), imageWidth, imageHeight);
  }

  /**
   * Initialize metafile for reading from the given input stream.
   *
   * @param imageWidth  the target width of the image or -1 for automatic mode.
   * @param imageHeight the target height of the image or -1 for automatic mode.
   * @param in          the stream from where to read.
   * @throws IOException if any other error occured.
   */
  public WmfFile(final InputStream in, final int imageWidth,
                 final int imageHeight)
      throws IOException
  {
    this.in = in;
    this.imageWidth = imageWidth;
    this.imageHeight = imageHeight;
    records = new ArrayList();
    dcStack = new FastStack(100);
    palette = new MfPalette();
    readHeader();
    parseRecords();
    resetStates();
  }

  public Dimension getImageSize()
  {
    return new Dimension(maxWidth, maxHeight);
  }

  private void resetStates()
  {
    Arrays.fill(objects, null);
    dcStack.clear();
    dcStack.push(new MfDcState(this));
  }

  public MfPalette getPalette()
  {
    return palette;
  }

  /**
   * Return Placeable and Windows headers that were read earlier.
   *
   * @return the meta-file header.
   */
  public MfHeader getHeader()
  {
    return header;
  }

  public Graphics2D getGraphics2D()
  {
    return graphics;
  }

  /**
   * Check class invariant.
   */
  private void assertValid()
  {
    if (filePos < 0 || filePos > fileSize)
    {
      throw new IllegalStateException("WmfFile is not valid");
    }
  }

  /**
   * Read Placeable and Windows headers.
   */
  private MfHeader readHeader()
      throws IOException
  {
    header = new MfHeader();
    header.read(in);
    if (header.isValid())
    {
      fileSize = header.getFileSize();
      objects = new WmfObject[header.getObjectsSize()];
      filePos = header.getHeaderSize();
      return header;
    }
    else
    {
      throw new IOException("The given file is not a real metafile");
    }
  }

  /**
   * Fetch a record.
   *
   * @return the next record read or null, if the end-of-file has been reached.
   * @throws IOException if an IO-Error occurs.
   */
  private MfRecord readNextRecord()
      throws IOException
  {
    if (filePos >= fileSize)
    {
      return null;
    }

    assertValid();

    final MfRecord record = new MfRecord(in);
    filePos += record.getLength();
    return record;
  }

  /**
   * Read and interpret the body of the metafile.
   *
   * @throws IOException if an IO-Error occurs.
   */
  private void parseRecords() throws IOException
  {
    minX = Integer.MAX_VALUE;
    minY = Integer.MAX_VALUE;
    maxWidth = 0;
    maxHeight = 0;

    final CommandFactory cmdFactory = CommandFactory.getInstance();
    MfRecord mf;
    while ((mf = readNextRecord()) != null)
    {
      final MfCmd cmd = cmdFactory.getCommand(mf.getType());
      if (cmd == null)
      {
        logger.info("Failed to parse record " + mf.getType());
      }
      else
      {
        cmd.setRecord(mf);

        if (cmd.getFunction() == MfType.SET_WINDOW_ORG)
        {
          final MfCmdSetWindowOrg worg = (MfCmdSetWindowOrg) cmd;
          final Point p = worg.getTarget();
          minX = Math.min(p.x, minX);
          minY = Math.min(p.y, minY);
        }
        else if (cmd.getFunction() == MfType.SET_WINDOW_EXT)
        {
          final MfCmdSetWindowExt worg = (MfCmdSetWindowExt) cmd;
          final Dimension d = worg.getDimension();
          maxWidth = Math.max(maxWidth, d.width);
          maxHeight = Math.max(maxHeight, d.height);
        }
        records.add(cmd);
      }
    }
    in.close();
    in = null;

    // make sure that we don't have invalid values in case no
    // setWindow records were found ...
    if (minX == Integer.MAX_VALUE)
    {
      minX = 0;
    }
    if (minY == Integer.MAX_VALUE)
    {
      minY = 0;
    }

    //System.out.println(records.size() + " records read");
    //System.out.println("Image Extends: " + maxWidth + " " + maxHeight);
    if (imageWidth < 1 || imageHeight < 1)
    {
      scaleToFit(MAX_PICTURE_SIZE, MAX_PICTURE_SIZE);
    }
    else
    {
      scaleToFit(imageWidth, imageHeight);
    }
  }

  /**
   * Scales the WMF-image to the given width and height while preserving the aspect ration.
   *
   * @param fitWidth  the target width.
   * @param fitHeight the target height.
   */
  public void scaleToFit(final float fitWidth, final float fitHeight)
  {
    final float percentX = (fitWidth * 100) / maxWidth;
    final float percentY = (fitHeight * 100) / maxHeight;
    scalePercent(percentX < percentY ? percentX : percentY);
  }

  /**
   * Scale the image to a certain percentage.
   *
   * @param percent the scaling percentage <!-- Yes, this is from iText lib -->
   */
  public void scalePercent(final float percent)
  {
    scalePercent(percent, percent);
  }

  /**
   * Scale the width and height of an image to a certain percentage.
   *
   * @param percentX the scaling percentage of the width
   * @param percentY the scaling percentage of the height <!-- Yes, this is from iText
   *                 lib -->
   */
  public void scalePercent(final float percentX, final float percentY)
  {
    imageWidth = (int) ((maxWidth * percentX) / 100f);
    imageHeight = (int) ((maxHeight * percentY) / 100f);
    imageX = (int) ((minX * percentX) / 100f);
    imageY = (int) ((minY * percentY) / 100f);
  }

  public static void main(final String[] args)
      throws Exception
  {
    final WmfFile wmf = new WmfFile("./head/pixie/res/a0.wmf", 800, 600);
    wmf.replay();
    // System.out.println(wmf.imageWidth + ", " + wmf.imageHeight);
  }

  public MfDcState getCurrentState()
  {
    return (MfDcState) dcStack.peek();
  }

  // pushes a state on the stack
  public void saveDCState()
  {
    final MfDcState currentState = getCurrentState();
    dcStack.push(new MfDcState(currentState));

  }

  public int getStateCount()
  {
    return dcStack.size();
  }

  /**
   * Restores a state. The stateCount specifies the number of states to discard
   * to find the correct one.
   *
   * @param stateCount the state count.
   */
  public void restoreDCState(final int stateCount)
  {
    if ((stateCount > 0) == false)
    {
      throw new IllegalArgumentException();
    }
    // this is contrary to Caolans description of the WMF file format, but
    // Batik also ignores the stateCount parameter.
    dcStack.pop();
    getCurrentState().restoredState();
  }

  /**
   * Return the next free slot from the objects table.
   *
   * @return the next new free slot in the objects-registry
   */
  protected int findFreeSlot()
  {
    for (int slot = 0; slot < objects.length; slot++)
    {
      if (objects[slot] == null)
      {
        return slot;
      }
    }

    throw new IllegalStateException("No free slot");
  }


  public void storeObject(final WmfObject o)
  {
    final int idx = findFreeSlot();
    objects[idx] = o;
  }

  public void deleteObject(final int slot)
  {
    if ((slot < 0) || (slot >= objects.length))
    {
      throw new IllegalArgumentException("Range violation");
    }

    objects[slot] = null;
  }

  public WmfObject getObject(final int slot)
  {
    if ((slot < 0) || (slot >= objects.length))
    {
      throw new IllegalStateException("Range violation");
    }

    return objects[slot];
  }

  public MfLogBrush getBrushObject(final int slot)
  {
    final WmfObject obj = getObject(slot);
    if (obj.getType() == WmfObject.OBJ_BRUSH)
    {
      return (MfLogBrush) obj;
    }
    throw new IllegalStateException("Object " + slot + " was no brush");
  }

  public MfLogPen getPenObject(final int slot)
  {
    final WmfObject obj = getObject(slot);
    if (obj.getType() == WmfObject.OBJ_PEN)
    {
      return (MfLogPen) obj;
    }
    throw new IllegalStateException("Object " + slot + " was no pen");
  }

  public MfLogRegion getRegionObject(final int slot)
  {
    final WmfObject obj = getObject(slot);
    if (obj.getType() == WmfObject.OBJ_REGION)
    {
      return (MfLogRegion) obj;
    }
    throw new IllegalStateException("Object " + slot + " was no region");
  }

  public synchronized BufferedImage replay()
  {
    return replay(imageWidth, imageHeight);
  }


  public synchronized BufferedImage replay(final int imageX, final int imageY)
  {
    final BufferedImage image = new BufferedImage(imageX, imageY, BufferedImage.TYPE_INT_ARGB);
    final Graphics2D graphics = image.createGraphics();

    // clear the image area ...
    graphics.setPaint(new Color(0, 0, 0, 0));
    graphics.fill(new Rectangle(0, 0, imageX, imageY));

    draw(graphics, new Rectangle2D.Float(0, 0, imageX, imageY));
    graphics.dispose();
    return image;
  }

  public synchronized void draw(final Graphics2D graphics, final Rectangle2D bounds)
  {

    // this adjusts imageWidth and imageHeight
    scaleToFit((float) bounds.getWidth(), (float) bounds.getHeight());
    // adjust translation if needed ...
    graphics.translate(bounds.getX(), bounds.getY());
    // adjust to the image origin
    graphics.translate(-imageX, -imageY);

    this.graphics = graphics;

    for (int i = 0; i < records.size(); i++)
    {
      try
      {
        final MfCmd command = (MfCmd) records.get(i);
        command.setScale((float) imageWidth / (float) maxWidth, (float) imageHeight / (float) maxHeight);
        command.replay(this);
      }
      catch (Exception e)
      {
        logger.warn("Error while processing image record #" + i, e);
      }
    }
    resetStates();
  }

  /**
   * Returns the preferred size of the drawable. If the drawable is aspect ratio aware, these bounds should be used to
   * compute the preferred aspect ratio for this drawable.
   *
   * @return the preferred size.
   */
  public Dimension getPreferredSize()
  {
    return new Dimension(imageWidth, imageHeight);
  }

  /**
   * Returns true, if this drawable will preserve an aspect ratio during the drawing.
   *
   * @return true, if an aspect ratio is preserved, false otherwise.
   */
  public boolean isPreserveAspectRatio()
  {
    return true;
  }

  public String toString()
  {
    final StringBuffer bo = new StringBuffer(500);
    bo.append("WmfFile={width=");
    bo.append(imageWidth);
    bo.append(", height=");
    bo.append(imageHeight);
    bo.append(", recordCount=");
    bo.append(records.size());
    bo.append(", records={\n");
    for (int i = 0; i < records.size(); i++)
    {
      final MfCmd cmd = (MfCmd) records.get(i);
      bo.append(i);
      bo.append(',');
      bo.append(cmd.toString());
      bo.append('\n');
    }
    bo.append("}\n");
    return bo.toString();
  }
}
