/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package dvi.ctx;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.WritableRaster;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

import dvi.DviException;
import dvi.DviObject;
import dvi.DviPaperSize;
import dvi.DviRect;
import dvi.DviResolution;
import dvi.api.DevicePainter;
import dvi.api.DviContextSupport;
import dvi.api.DviDocument;
import dvi.api.DviPage;
import dvi.api.Geometer;
import dvi.api.ImageDevice;
import dvi.gui.swing.ViewSpec;
import dvi.image.split.DviImage;
import dvi.image.split.SplitImage;
import dvi.image.split.SplitImageUtils;
import dvi.render.BasicGeometer;
import dvi.render.DviBoundingBoxPreparator;
import dvi.render.DviPagePreparator;
import dvi.render.IntRGBImage;
import dvi.render.RunLengthSampler;
import dvi.special.AnchorSet;
import dvi.special.EPS2ImagePreparator;
import dvi.special.EPS2SplitImagePreparator;
import dvi.special.EmbeddedPostScript;
import dvi.special.EmbeddedPostScriptPreparator;
import dvi.special.HtmlSpecialParser;
import dvi.special.SourceSpecialParser;
import dvi.util.DviUtils;
import dvi.util.concurrent.CacheEntry;
import dvi.util.concurrent.CachedComputer;
import dvi.util.concurrent.Computation;
import dvi.util.concurrent.ThreadedComputer;

// TODO: Make this an interface
public class DviToolkit
extends DviObject
{
  private static final Logger LOGGER = Logger.getLogger(DviToolkit.class.getName());

  private static final Map<String, AnchorSet> anchorSetCache
  = Collections.synchronizedMap(new LinkedHashMap<String, AnchorSet>() {
    private static final long serialVersionUID = 3603340224879882990L;
    protected boolean removeEldestEntry(Map.Entry<String, AnchorSet> entry) {
        return size() > 1024;
      }
    });

  
  public DviToolkit(DviContextSupport dcs)
  {
    super(dcs);
  }
  
  private static final CachedComputer<String, DviRect>
  computerForBoundingBoxPreparator = new CachedComputer<String, DviRect>
    (new ThreadedComputer<String, DviRect>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, DviRect>> entry) {
      return getCache().size() > 1024;
    }
  };

  public DviRect computeRawBoundingBox(DviPage page, DviResolution res)
      throws DviException
  {
    if (page == null)
    return null;
    
    Computation<String, DviRect> computation
      = new DviBoundingBoxPreparator(this, page, res);
    
    Future<DviRect> future = computerForBoundingBoxPreparator.compute(computation);
    DviRect bbox;
    try {
      bbox = future.get();
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
    return bbox;
  }

  public DviRect computeBoundingBox(DviPage page, DviResolution res)
      throws DviException
  {
    if (page == null)
      return null;
    
    DviRect bbox = computeRawBoundingBox(page, res);
    if (bbox == null) return bbox;
    return bbox.shrink(res.shrinkFactor());
  }
  
  public DviRect [] computeRawBoundingBoxes(DviDocument doc, DviResolution res)
      throws DviException
  {
    if (doc == null)
      return null;
    
    List<DviRect> bbox = new ArrayList<DviRect>();

    for (DviPage page : doc.getPages()) {
      bbox.add(computeRawBoundingBox(page, res));
    }
    
    return bbox.toArray(new DviRect[0]);
  }

  public DviRect[] computeBoundingBoxes(DviDocument doc, DviResolution res) throws DviException
  {
    if (doc == null)
      return null;
    
    DviRect [] raw = computeRawBoundingBoxes(doc, res);
    DviRect [] ret = new DviRect[raw.length];
    for (int i=0; i<ret.length; i++) {
      ret[i] = raw[i].shrink(res.shrinkFactor());
    }
    return ret;
  }
  
  private void drawNamedFrame(Graphics gg, String string, Color color, int x, int y, int width, int height)
  {
    gg.setColor(color);
    gg.drawString(string, x, y + 32);
    gg.drawRect(x, y, width-1, height-1);
  }
  
  private static final boolean renderDebugInfo = false;

  /**
   * Render the specified page to a BufferedImage.
   * @param page page to render
   * @param viewSpec used to specify the view configuration.
   * @param rect the bounding box in the paper coordinate with resolution specified by viewSpec.
   * @return BufferedImage INT_RGB image
   * @throws DviException
   */
  public BufferedImage renderToBufferedImage(BufferedImage img, int x, int y, DviPage page, DviRect rect,
      ViewSpec viewSpec) throws DviException
  {
    if (img == null)
      throw new NullPointerException("img");
    if (page == null)
      throw new NullPointerException("page");
    if (viewSpec == null)
      throw new NullPointerException("view spec");
    
    final DviResolution res = viewSpec.getResolution();

    DviRect paper = computePageBoundingBox(viewSpec.getPaperSize(), res);

    if (rect == null) {
      rect = paper;
    }

    // A point (bbox.x + a, bbox.y + b) in the paper coordinate corresponds to
    // (x + a, y + b) == (paper2dispX + a, paper2dispY + b) in the image coordinate.
    final int paper2dispX = x - rect.x();
    final int paper2dispY = y - rect.y();

    WritableRaster raster = img.getRaster();
    DataBufferInt data = (DataBufferInt) (raster.getDataBuffer());

    IntRGBImage rgbImage = new IntRGBImage(data.getData(), raster.getWidth(),
        raster.getHeight());
    rgbImage.fill(viewSpec.getBackgroundColor().toIntRGB());

    if (viewSpec.isPostScriptEnabled()) {
      try {
        SplitImage dviImage = getEmbeddedPostScriptAsSplitImage(page, viewSpec);
        if (dviImage != null) {
          Graphics2D gg = img.createGraphics();
          try {
            gg.setClip(x, y, rect.width(), rect.height());
            double factor = res.actualDpi() / dviImage.getResolution().actualDpi();
            DviRect r0 = dviImage.getRect();
            DviRect r = r0.magnify(factor);
            DviRect psRect = r
               .moveTo(paper.bottomLeft().translate(0, - r.bottom()))
               .translate(paper2dispX, paper2dispY);
            SplitImageUtils.renderToGraphics(gg, dviImage, psRect.x(), psRect.y(), factor);
            if (renderDebugInfo) {
              drawNamedFrame(gg, "PS", Color.black, psRect.x(), psRect.y(), psRect.width(), psRect.height());
            }
          } finally {
            gg.dispose();
          }
        }
      } catch (OutOfMemoryError e) {
        DviUtils.logStackTrace(LOGGER, Level.SEVERE, e);
      }
    }
    if (renderDebugInfo) {
      Graphics gg = img.getGraphics();
      try {
        int a = (int) res.actualDpi();
        DviRect inchBox = paper.crop(a, a, a, a);
        drawNamedFrame(gg, "Paper", Color.blue, paper2dispX + paper.x(),
            paper2dispY + paper.y(), paper.width(), paper.height());
        drawNamedFrame(gg, "InchBox", Color.red, paper2dispX + inchBox.x(),
            paper2dispY + inchBox.y(), inchBox.width(), inchBox.height());
        DviRect bbox = computeBoundingBox(page, res);
        drawNamedFrame(gg, "BBOX", Color.cyan, paper2dispX + bbox.x(),
            paper2dispY + bbox.y(), bbox.width(), bbox.height());
      } finally {
        gg.dispose();
      }
    }

    ImageDevice out = rgbImage
        .getImageDevice(res, viewSpec.getGammaCorrector());
    out.setColor(viewSpec.getForegroundColor());
    out.translate(paper2dispX, paper2dispY);
    DevicePainter dp = getDviContext().newDevicePainter();
    dp.setOutput(new RunLengthSampler(out));
    Geometer geometer = new BasicGeometer(this);
    geometer.setPainter(dp);
    getDviContext().execute(page, geometer);

    return img;
  }
  
  public BufferedImage createCompatibleBufferedImage(int width, int height)
  {
    final BufferedImage img = new BufferedImage
      (width, height, BufferedImage.TYPE_INT_RGB);
    return img;
  }

  public BufferedImage renderToBufferedImage(DviPage page, DviRect bbox,
    ViewSpec viewSpec) throws DviException
  {
    final DviResolution res = viewSpec.getResolution();

    DviRect paper = computePageBoundingBox(viewSpec.getPaperSize(), res);

    if (bbox == null) {
      bbox = paper;
    }
    
    final BufferedImage img = createCompatibleBufferedImage(bbox.width(), bbox.height());
    return renderToBufferedImage(img, 0, 0, page, bbox, viewSpec);
  }
  
  public void prepareForRendering(DviDocument doc, ViewSpec viewSpec)
  throws DviException
  {
    if (doc == null) return;
    if (doc.getTotalPages() > 0) {
      DviPage page = doc.getPage(0);
      prepareForRendering(page, viewSpec);
      computeBoundingBoxes(doc, viewSpec.getResolution());
    }
  }

  private AnchorSet buildAnchorSetForDocument(DviDocument doc)
  {
    AnchorSet as = new AnchorSet();
    {
      try {
        HtmlSpecialParser hse = new HtmlSpecialParser(this);
        hse.execute(doc);
        LOGGER.fine("Extracted Html tags from " + doc);
        as.addAll(hse.getAnchorSet());
      } catch (DviException e) {
        LOGGER.warning(e.toString());
      }
    }
    {
      try {
        SourceSpecialParser hse = new SourceSpecialParser(this);
        hse.execute(doc);
        LOGGER.fine("Extracted Source specials from " + doc);
        as.addAll(hse.getAnchorSet());
      } catch (DviException e) {
        LOGGER.warning(e.toString());
      }
    }
    return as;
  }

  public AnchorSet getAnchorSet(DviPage page)
  throws DviException
  {
    if (page == null) return null;
    DviDocument doc = page.getDocument();
    String key = doc.getCacheKey();
    AnchorSet as = anchorSetCache.get(key);
    if (as == null) {
      as = buildAnchorSetForDocument(doc);
      if (as != null) {
        anchorSetCache.put(key, as);
        LOGGER.fine("Cached AnchorSet with key " + key);
      }
    }
    
    if (as == null) {
      return null;
    }

    AnchorSet pageAnchorSet = new AnchorSet();
    pageAnchorSet.addAll(as.intersect(page.range()));
    return pageAnchorSet;
  }

  public DviRect computePageBoundingBox(DviPaperSize paperSize, DviResolution res)
  {
    DviRect bbox = null;
    try {
      paperSize = getDviContext().getDefaultPaperSize();
    } catch (DviException e) {
      LOGGER.warning(e.toString());
    }
    if (paperSize == null) {
      LOGGER.warning("The default paper size is undefined. We use A4.");
      paperSize = new DviPaperSize(210.0, 297.0, "A4 internal");
    }

    bbox = paperSize.toBoundingBox(res);
    
    return bbox;
  }

  private static final CachedComputer<String, Long>
    computerForPreparator = new CachedComputer<String, Long>
      (new ThreadedComputer<String, Long>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, Long>> entry) {
      return getCache().size() > 1024;
    }
  };


  public void prepareForRendering(DviPage page, ViewSpec viewSpec)
  {
    Computation<String, Long> computation = new DviPagePreparator(this, page, viewSpec);
    try {
      computerForPreparator.compute(computation).get();
    } catch (InterruptedException e) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, e);
    } catch (ExecutionException e) {
      DviUtils.logStackTrace(LOGGER, Level.SEVERE, e);
    }
  }

  public boolean canRenderPageImmediately(DviPage page, ViewSpec viewSpec)
  {
    Computation<String, Long> computation = new DviPagePreparator(this, page, viewSpec);
    Future<Long> future = computerForPreparator.getCachedResult(computation);
    return future != null && future.isDone();
  }
  
  

  
  private static final CachedComputer<String, EmbeddedPostScript>
  computerForEmbeddedPostScriptPreparator = new CachedComputer<String, EmbeddedPostScript>
    (new ThreadedComputer<String, EmbeddedPostScript>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, EmbeddedPostScript>> entry) {
      return getCache().size() > 1024;
    }
  };

  public EmbeddedPostScript getEmbeddedPostScript(DviDocument doc, ViewSpec viewSpec)
      throws DviException
  {
    Computation<String, EmbeddedPostScript> computation
      = new EmbeddedPostScriptPreparator(this, doc, viewSpec);
    Future<EmbeddedPostScript> future = computerForEmbeddedPostScriptPreparator.compute(computation);
    try {
      return future.get();
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
  }

  public String getEmbeddedPostScript(DviPage page, ViewSpec viewSpec)
  throws DviException
  {
    if (page == null) return null;
    EmbeddedPostScript eps = getEmbeddedPostScript(page.getDocument(), viewSpec);
    return eps.toPostScript(page.getPageNumber(), viewSpec.getEpsResolutionDpi());
  }

  private static final CachedComputer<String, DviImage>
  computerForEPS2Image = new CachedComputer<String, DviImage>
    (new ThreadedComputer<String, DviImage>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, DviImage>> entry) {
      return getCache().size() > 10;
    }
  };

  public DviImage getEmbeddedPostScriptAsImage(DviPage page, ViewSpec viewSpec)
  throws DviException
  {
    Computation<String, DviImage> computation = new EPS2ImagePreparator(this,
        page, viewSpec);
    Future<DviImage> future = computerForEPS2Image.compute(computation);
    try {
      return future.get();
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
  }
  
  private static final CachedComputer<String, SplitImage>
  computerForEPS2SplitImage = new CachedComputer<String, SplitImage>
    (new ThreadedComputer<String, SplitImage>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, SplitImage>> entry) {
      return getCache().size() > 10;
    }
  };

  
  public SplitImage getEmbeddedPostScriptAsSplitImage(DviPage page, ViewSpec viewSpec)
  throws DviException
  {
    Computation<String, SplitImage> computation = new EPS2SplitImagePreparator(this,
        page, viewSpec);
    Future<SplitImage> future = computerForEPS2SplitImage.compute(computation);
    try {
      return future.get();
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
  }

  
  // This is slow.
  public BufferedImage getScaledImage(int width, int height, BufferedImage image) {
    if (image == null) return null;
    int origWidth = image.getWidth();
    int origHeight = image.getHeight();
    if (width < 0) {
      if (origHeight > 0) {
        width = origWidth * height / origHeight;
      } else {
        width = 0;
      }
    } else if (height < 0) {
      if (origWidth > 0) {
        height = origHeight * width / origWidth;
      } else {
        height = 0;
      }
    }
    BufferedImage out = createCompatibleBufferedImage(width, height);
    Graphics2D g = out.createGraphics();
    g.drawImage(image.getScaledInstance(width, height, Image.SCALE_SMOOTH), null, null);
    g.dispose();
    return out;
  }

  
}
