/*
 * 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.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dvi.DviException;
import dvi.DviObject;
import dvi.MetafontMode;
import dvi.api.DviContext;
import dvi.api.DviContextSupport;
import dvi.plat.cygwin.CygwinUtils;
import dvi.util.CommandShell;
import dvi.util.CommandShellHandler;
import dvi.util.DviUtils;

public class KpseWhich
extends DviObject
{
  public static final int KPSEWHICH_TYPE_UNKNOWN = -1;
  public static final int KPSEWHICH_TYPE_DEFAULT = 0;
  public static final int KPSEWHICH_TYPE_WINDOWS_SLASH = 1;
  public static final int KPSEWHICH_TYPE_CYGWIN = 2;
  
  private static final Logger LOGGER = Logger.getLogger(KpseWhich.class.getName());
  private static boolean available = false;
  private static volatile boolean availabilityChecked = false;
  
  private static int type = -1;
  private boolean useMktexCommand = true;

  public KpseWhich(DviContextSupport dcs)
  {
    super(dcs);
    if (!availabilityChecked) {
      checkAvailability();
    }
  }
  
  private static void setAvailability(boolean isAvailable)
  {
    available = isAvailable;
    availabilityChecked = true;
  }

  // TODO: outsource the test filename.
  private synchronized void checkAvailability() {
    boolean success = true;
    try {
      String filename = "cmr10.mf";
      String result = findURLInternal(filename, true);
      if (result == null) {
        LOGGER.warning("Unable to locate by kpsewhich: " + filename);
        success = false;
      } else {
        LOGGER.info("kpsewhich answers: filename=" + filename + " result=" + result);
        File f = new File(result);
        if (!f.exists()) {
          if (DviUtils.isWindows()) {
            LOGGER.info("Trying cygpath to map " + result + " to system path.");
            {
              File f2 = replaceSlashInWindows(result);
              if (f2 != null) {
                if (f2.exists()) {
                  LOGGER.info("The mapped path seems to work: " + f2.getAbsolutePath());
                  LOGGER.info("kpsewhich seems to encode path with /.");
                  type = KPSEWHICH_TYPE_WINDOWS_SLASH;
                }
              }
            }
            
            if (type < 0) {
              File f2 = fromPosixPathCygwin(result);
              if (f2.exists()) {
                LOGGER.info("The mapped path seems to work: " + f2.getAbsolutePath());
                LOGGER.info("kpsewhich seems to use the cygwin style path.");
                type = KPSEWHICH_TYPE_CYGWIN;
              }
            }
            
            if (type >= 0) {
              LOGGER.warning("kpsewhich type resolved: " + type);
              success = true;
            } else {
              LOGGER.warning("Unrecognized output by kpsewhich: " + result);
              success = false;
            }
          } else {
            LOGGER.warning("Unable to find file: " + result);
            success = false;
          }
        } else {
          LOGGER.info("Using default kpsewhich.");
          type = KPSEWHICH_TYPE_DEFAULT;
          success = true;
        }
      }
    } catch (MalformedURLException ex) {
      success = false;
    } catch (DviException ex) {
      success = false;
    } catch (RuntimeException ex) {
      success = false;
    }
    setAvailability(success);
    if (!success) {
      LOGGER.info("kpsewhich is not available");
    }
  }
  
  private static final Pattern patSlashInWindows = Pattern.compile("^[A-Za-z]:.*$");
  private File replaceSlashInWindows(String path) {
    Matcher mat = patSlashInWindows.matcher(path);
    if (mat.matches()) {
      return new File(path.replaceAll("/", "\\"));
    }
    return null;
  }

  private File fromPosixPathCygwin(String posixPath) {
    try {
      String windowsPath = CygwinUtils.posixPathToJavaPath(posixPath);
      return new File(windowsPath);
    } catch (InterruptedException e) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, e);
      return null;
    } catch (IOException e) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, e);
      return null;
    } catch (DviException e) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, e);
      return null;
    }
  }

  public URL findURL(String name) throws MalformedURLException, DviException
  {
    return findURL(name, false);
  }
  
  private String findURLInternal(String name, boolean mustExist) throws MalformedURLException, DviException
  {
    String result = null;
    try {
      DviContext ctx = getDviContext();
      MetafontMode mfmode = getMetafontMode();
      Vector<String> cmdLine = new Vector<String>();
      cmdLine.add(ctx.getExecutableName("kpsewhich"));
      if (mustExist) {
        cmdLine.add("-must-exist");
      }
      
      if (useMktexThroughKpsewhich()) {
        cmdLine.add("-mktex=pk");
        cmdLine.add("-mktex=tex");
        cmdLine.add("-mktex=mf");
        cmdLine.add("-mktex=tfm");
      }
      if (mfmode != null) {
        cmdLine.add("-dpi=" + mfmode.getBdpi());
        cmdLine.add("-mode=" + mfmode.getMode());
      }
      cmdLine.add(name);
      
      final ArrayList<String> stderrData = new ArrayList<String>();
      final ArrayList<String> stdoutData = new ArrayList<String>();
      CommandShell cs = new CommandShell();
      cs.setCommandLine(cmdLine);
      cs.setHandler(new CommandShellHandler() {
        public void handleStderr(InputStream in) throws IOException {
          DviUtils.addLinesFromStream(stderrData, in, LOGGER, Level.FINE, System.err);
        }
        public void handleStdout(InputStream in) throws IOException {
          DviUtils.addLinesFromStream(stdoutData, in);
        }
        public void handleStdin(OutputStream out) throws IOException {
          out.close();
        }
      });
      int ret = cs.execute();
      if (ret != 0) {
        LOGGER.fine("kpsewhich command failed with retcode=" + ret + " cmdline=" +
            DviUtils.join(" ", cmdLine) + " stderr=" + DviUtils.join("\n", stderrData));
      } else {
        if (stdoutData.size() > 0) {
          result = stdoutData.get(0);
        }
      }
    } catch (Exception e) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, e);
    }

    LOGGER.fine("kpsewhich result: name=" + name + " result=" + result);
    
    return result;
  }


  protected MetafontMode getMetafontMode()
  throws DviException
  {
    MetafontMode mfmode = getDviContext().getDefaultMetafontMode();
    if (mfmode == null) {
      mfmode = MetafontMode.FALLBACK;
    }
    return mfmode;
  }

  public URL findURL(String name, boolean mustExist) throws MalformedURLException, DviException
  {
    if (!available) return null;
    String path = findURLInternal(name, mustExist);
    
    if (path == null) return null;
    
    // The result kpsewhich returns is a POSIX absolute path to the resource in cygwin environment.
    // Such a path does not work with the File class.  So we have to convert it by
    // running the command: cygpath -w <path-output-by-kpsewhich>

    File file = null;
    switch (type) {
    case KPSEWHICH_TYPE_DEFAULT:
      file = new File(path);
      break;
    case KPSEWHICH_TYPE_CYGWIN:
      file = fromPosixPathCygwin(path);
      break;
    case KPSEWHICH_TYPE_WINDOWS_SLASH:
      file = replaceSlashInWindows(path);
      break;
    default:
      throw new InternalError("unknown kpsewhich type: " + type);
    }
    
    if (mustExist) {
      if (!file.exists()) {
        file = null;
      }
    }
    
    if (file != null) {
      return file.toURL();
    }
    
    return null;
  }

  public void setUseMktexThroughKpsewhich(boolean useMktexCommand) {
    this.useMktexCommand = useMktexCommand;
  }

  public boolean useMktexThroughKpsewhich() {
    return useMktexCommand;
  }
}
