/*
 * 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 jp.sourceforge.dvibrowser.dvicore.ctx;

import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.security.AccessControlException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import jp.sourceforge.dvibrowser.dvicore.DviException;
import jp.sourceforge.dvibrowser.dvicore.DviFontSpec;
import jp.sourceforge.dvibrowser.dvicore.DviPaperSize;
import jp.sourceforge.dvibrowser.dvicore.DviResolution;
import jp.sourceforge.dvibrowser.dvicore.MetafontMode;
import jp.sourceforge.dvibrowser.dvicore.api.CharacterCodeMapper;
import jp.sourceforge.dvibrowser.dvicore.api.DevicePainter;
import jp.sourceforge.dvibrowser.dvicore.api.DviContext;
import jp.sourceforge.dvibrowser.dvicore.api.DviData;
import jp.sourceforge.dvibrowser.dvicore.api.DviDocument;
import jp.sourceforge.dvibrowser.dvicore.api.DviExecutor;
import jp.sourceforge.dvibrowser.dvicore.api.DviExecutorHandler;
import jp.sourceforge.dvibrowser.dvicore.api.DviFont;
import jp.sourceforge.dvibrowser.dvicore.api.FullMetrics;
import jp.sourceforge.dvibrowser.dvicore.api.Glyph;
import jp.sourceforge.dvibrowser.dvicore.api.SimpleMetrics;
import jp.sourceforge.dvibrowser.dvicore.doc.DirectFileDviDocument;
import jp.sourceforge.dvibrowser.dvicore.doc.StreamDviDocument;
import jp.sourceforge.dvibrowser.dvicore.doc.URLDviDocument;
import jp.sourceforge.dvibrowser.dvicore.font.DviFontResolver;
import jp.sourceforge.dvibrowser.dvicore.font.FullMetricsResolver;
import jp.sourceforge.dvibrowser.dvicore.font.LogicalFont;
import jp.sourceforge.dvibrowser.dvicore.gs.GhostscriptUtils;
import jp.sourceforge.dvibrowser.dvicore.render.BasicExecutor;
import jp.sourceforge.dvibrowser.dvicore.render.DefaultDevicePainter;
import jp.sourceforge.dvibrowser.dvicore.util.DviCache;
import jp.sourceforge.dvibrowser.dvicore.util.DviUtils;
import jp.sourceforge.dvibrowser.dvicore.util.concurrent.CacheEntry;
import jp.sourceforge.dvibrowser.dvicore.util.concurrent.CachedComputer;
import jp.sourceforge.dvibrowser.dvicore.util.concurrent.Computation;
import jp.sourceforge.dvibrowser.dvicore.util.concurrent.ThreadedComputer;
import jp.sourceforge.dvibrowser.dvicore.util.progress.ManagedProgressItem;
import jp.sourceforge.dvibrowser.dvicore.util.progress.ProgressRecorder;


public class DefaultDviContext
implements DviContext
{
  private static final Logger LOGGER = Logger.getLogger(DefaultDviContext.class.getName());
  
  private static final String DVICORE_INTERNAL_FILENAME_PREFIX = "dvicore-";
  
  private final Map<String, Glyph> glyphCache = new DviCache<String, Glyph>(16384);
  private final CharacterCodeMapper characterCodeMapper
    = new SimpleJisToUnicodeMapper();
  
  private boolean recordResources = false;
  private final Set<URL> resources = new TreeSet<URL>(new Comparator<URL>() {
	public int compare(URL arg0, URL arg1) {
		return arg0.toString().compareTo(arg1.toString());
	}
  });
  
  private final ProgressRecorder recorder = new ProgressRecorder(this) {
    @Override
    protected boolean removeEldestElement(ManagedProgressItem item)
    {
      List<ManagedProgressItem> list = getProgressItems();
      if (list.size() > 10) {
        return true;
      }
      return false;
    }
  };


  public DefaultDviContext()
  throws DviException
  {
    this(null);
  }

  public DefaultDviContext(Properties prop)
  throws DviException
  {
    if (prop == null)
      prop = getDefaultProperties();
    this.prop = prop;
    initializeFontMapper();
    populatePaperSizes();
    initializeDirs();
  }
  
  protected void initializeDirs()
  throws DviException
  {
    File cacheDir = getCacheDirectory();
    if (cacheDir != null && !cacheDir.exists()) {
      if (!cacheDir.mkdirs()) {
        throw new DviException("Failed to create cache directory: " + cacheDir);
      }
    }
    File tmpDir = getTemporaryDirectory();
    if (tmpDir != null && !tmpDir.exists()) {
      if (!tmpDir.mkdirs()) {
        throw new DviException("Failed to create temporary directory: " + tmpDir);
      }
    }
  }

  protected Properties getDefaultProperties()
  throws DviException
  {
    try {
      URL url = DefaultDviContext.class.getResource("default-context.properties");
      Properties prop = new Properties();
      if (null != url) {
          prop.load(url.openStream());
      } else {
    	  LOGGER.warning("Failed to load default-context.properties");
      }
      return prop;
    } catch (Exception e) {
      throw new DviException(e);
    }
  }
  
  private final Properties prop;

  public Properties getProperties()
  {
    return prop;
  }
  
  public void execute(DviData data, DviExecutorHandler handler)
      throws DviException
  {
    newDviExecutor().execute(data, handler);
  }

  private static final CachedComputer<String, Collection<DviFont>> dviFontComputer
  = new CachedComputer<String, Collection<DviFont>>
    (new ThreadedComputer<String, Collection<DviFont>>(1)) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<String, Collection<DviFont>>> entry)
    {
      boolean remove = getCache().size() > 64;
      return remove;
    }
  };

  public DviFont findDviFont(LogicalFont logicalFont) throws DviException
  {
    Computation<String, Collection<DviFont>> computation
      = new DviFontResolver(this, logicalFont);
    try {
      Collection<DviFont> fonts = dviFontComputer.compute(computation).get();
      for (DviFont font : fonts) {
        return font;
      }
      return null;
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
  }
  
  public SimpleMetrics findDviSimpleMetrics(DviFontSpec fs) throws DviException
  {
    return findDviFullMetrics(fs);
  }

  private static final CachedComputer<String, Collection<FullMetrics>> fullMetricsComputer
  = new CachedComputer<String, Collection<FullMetrics>>
    (new ThreadedComputer<String, Collection<FullMetrics>>(1));

  public FullMetrics findDviFullMetrics(DviFontSpec fs) throws DviException
  {
    Computation<String, Collection<FullMetrics>> computation = new FullMetricsResolver(
        this, fs);
    try {
      Collection<FullMetrics> fonts = fullMetricsComputer.compute(computation)
          .get();
      if (fonts != null) {
        for (FullMetrics font : fonts) {
          return font;
        }
      }
      return null;
    } catch (InterruptedException e) {
      throw new DviException(e);
    } catch (ExecutionException e) {
      throw new DviException(e);
    }
  }

  public CharacterCodeMapper getCharacterCodeMapper(LogicalFont logicalFont) throws DviException
  {
    return characterCodeMapper;
  }
  
  public DviContext getDviContext() { return this; }
  
  public Map<String, Glyph> getGlyphCache() throws DviException
  {
    return Collections.synchronizedMap(glyphCache);
  }

  protected void initializeFontMapper()
  {
  }
  
  public DevicePainter newDevicePainter() throws DviException
  {
    return new DefaultDevicePainter(this);
  }

  public DviExecutor newDviExecutor() throws DviException
  {
    return new BasicExecutor(this);
  }
  
  public DviDocument openDviDocument(File file) throws DviException
  {
    return new DirectFileDviDocument(this, file);
  }

  public DviDocument openDviDocument(InputStream is) throws DviException
  {
    return new StreamDviDocument(this, is);
  }

  // TODO: implement async resource loading
  public DviDocument openDviDocument(URL url) throws DviException
  {
    return new URLDviDocument(this, url);
  }

  public ProgressRecorder getProgressRecorder()
  {
    return recorder;
  }

  private static final Map<String, DviPaperSize> paperSizes = new TreeMap<String, DviPaperSize>(); 
  
  protected void populatePaperSizes()
  {
    // ISO 216 sizes
    // TODO: implement B and C serieses. A4 Japanese, too.
    // TOOD: outsource the configuration.
    addPaperSize(new DviPaperSize(841.0, 1189.0, "A0"));
    addPaperSize(new DviPaperSize(594.0, 841.0, "A1"));
    addPaperSize(new DviPaperSize(420.0, 594.0, "A2"));
    addPaperSize(new DviPaperSize(297.0, 420.0, "A3"));
    addPaperSize(new DviPaperSize(210.0, 297.0, "A4"));
    addPaperSize(new DviPaperSize(148.0, 210.0, "A5"));
    addPaperSize(new DviPaperSize(105.0, 148.0, "A6"));
    addPaperSize(new DviPaperSize(74.0, 105.0, "A7"));
    addPaperSize(new DviPaperSize(52.0, 74.0, "A8"));
    addPaperSize(new DviPaperSize(37.0, 52.0, "A9"));
    addPaperSize(new DviPaperSize(26.0, 37.0, "A10"));
  }

  protected void addPaperSize(DviPaperSize dviPaperSize)
  {
    if (dviPaperSize == null) return;
    paperSizes.put(dviPaperSize.description().toLowerCase(), dviPaperSize);
  }

  public DviPaperSize findPaperSizeByName(String name) throws DviException
  {
    if (name == null) return null;
    return paperSizes.get(name.toLowerCase());
  }

  public DviPaperSize getDefaultPaperSize() throws DviException
  {
    return findPaperSizeByName("A4");
  }

  public DviPaperSize [] listPaperSizes() throws DviException
  {
    return paperSizes.values().toArray(new DviPaperSize[0]);
  }

  private static final DviResolution defaultResolution = new DviResolution(2400, 20);
  public DviResolution getDefaultResolution() throws DviException
  {
    return defaultResolution;
  }
  
  private static final CachedComputer<String, Collection<URL>> dviResourceComputer
    = new CachedComputer<String, Collection<URL>>
      (new ThreadedComputer<String, Collection<URL>>(1));
  
  public URL getDviResource(String filename) throws DviException
  {
    if (filename.startsWith(DVICORE_INTERNAL_FILENAME_PREFIX)) {
      // The resource name starting with "dvicore-" are only for internal use.
      // So there is no local file corresponding to such a filename.
      // We answer null.
      return null;
    }
    
    Computation<String, Collection<URL>> c
      = new FileLocationResolver(this, "/dvi/builtin", filename);
    final Future<Collection<URL>> future = dviResourceComputer.compute(c);
    try {
      final Collection<URL> list = future.get();
      for (URL url : list) {
        if (recordResources) {
          LOGGER.info("resolved resource: filename=" + filename + " url=" + url);
          resources.add(url);
        } else {
          LOGGER.finest("resolved resource: filename=" + filename + " url=" + url);
        }
        return url;
      }
      return null;
    } catch (InterruptedException e) {
      LOGGER.warning(e.toString());
      throw new DviException(e);
    } catch (ExecutionException e) {
      LOGGER.warning(e.toString());
      throw new DviException(e);
    }
  }

  public LogicalFont mapLogicalFont(LogicalFont logicalFont)
      throws DviException
  {
	LogicalFont mapped = logicalFont;
	if (logicalFont != null) {
	    String prefix = getClass().getName();
	    String faceKey = prefix + ".fontMap." + logicalFont.fontSpec().name();
        LOGGER.info("Face key: " + faceKey);
	    String face = getProperties().getProperty(faceKey);
        LOGGER.info("Properties: " + getProperties());
	    if (face != null) {
		    mapped = logicalFont.renameTo(face);
	        LOGGER.info("Rename logical font: " + logicalFont + " => " + mapped);
	    }
	}
    LOGGER.info("Map logical font: " + logicalFont + " => " + mapped);
    return mapped;
  }
  
  // N.B. System.getProperty() throws an exception when invoked
  // from inside an applet.
  private static String getSystemProperty(String key) {
	try {
	    return System.getProperty(key);
	} catch (AccessControlException ex) {
		return null;
	}
  }

  private static final String userDir = getSystemProperty("user.dir");
  private static final String ioTmpDir = getSystemProperty("java.io.tmpdir");
  
  // TODO: externalize the string "dvibrowser.jar".
  public File getApplicationHomeDir()
  throws DviException
  {
	if (userDir != null) {
	    File home = new File(userDir);
	    File markFile = new File(home, "dvibrowser.jar");
	    if (markFile.exists()) {
	      // It seems that we are using DviContext from within dvibrowser.
	      return home;
	    }
	}
    return null;
  }

  public File getCacheDirectory() throws DviException
  {
    File appHome = getApplicationHomeDir();
    if (appHome == null) {
      File tmpDir = getTemporaryDirectory();
      if (tmpDir != null) {
          return new File(tmpDir, "cache");
      }
    } else {
    	if (userDir != null) {
    	      File var = new File(userDir, "var");
    	      return new File(var, "cache");
    	}
    }
    return null;
  }

  public File getTemporaryDirectory() throws DviException
  {
    File appHome = getApplicationHomeDir();
    if (appHome == null) {
    	if (ioTmpDir != null) {
    	      return new File(ioTmpDir, "dvicontext");
    	}
    } else {
      return new File(appHome, "tmp");
    }
    return null;
  }

  private volatile String [] ghostscriptExecutables = null;
  
  public String getExecutableName(String name) throws DviException
  {
    if ("gs".equals(name)) {
      if (ghostscriptExecutables == null) {
        // The following code might be called in pararell when we invoke
        // this method from within multiple threads.
        // It might be better to use some atomic classes.
        ghostscriptExecutables = GhostscriptUtils.listGhostscriptExecutables();
        if (ghostscriptExecutables.length == 0) {
          LOGGER.warning("You don't seem to have a Ghostscript installed.  dvibrowser needs it to render PS, EPS, and PDF.");
        } else {
          LOGGER.info("Ghostscript executables found: " + DviUtils.join(" ", ghostscriptExecutables));
        }
      }
      if (ghostscriptExecutables.length > 0) {
        return ghostscriptExecutables[0];
      }
    }
    
    return name;
  }

  private final DviToolkit dviToolkit = new DviToolkit(this);
  public DviToolkit getDviToolkit()
  {
    return dviToolkit;
  }

  private final MetafontMode libraryDefaultMetafontMode = MetafontMode.FALLBACK;
  public MetafontMode getDefaultMetafontMode() throws DviException {
    return libraryDefaultMetafontMode;
  }

public void setRecordResources(boolean recordResources) {
	this.recordResources = recordResources;
}

public boolean wantRecordResources() {
	return recordResources;
}

public Set<URL> getRecordedResources() {
	return resources;
}
}
