/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.tools.pear.merger;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.jar.JarFile;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import org.apache.uima.analysis_engine.AnalysisEngineDescription;
import org.apache.uima.pear.tools.InstallationDescriptor;
import org.apache.uima.pear.tools.PackageBrowser;
import org.apache.uima.pear.util.FileUtil;

/**
 * The <code>PMController</code> class allows to merge several input PEAR files in one PEAR file and
 * generate an aggregate analysis engine from the components encapsulated in the input PEARs.
 * 
 * @see org.apache.uima.tools.pear.merger.PMControllerHelper
 * @see org.apache.uima.tools.pear.merger.PMUimaAgent
 */

public class PMController {
  // utility name
  static final String PEAR_MERGER = "PEAR Merger";

  // log file name
  static final String LOG_FILE = "pm.log";

  // command line argument names
  static final String AGGREGATE_NAME_ARG = "-n";

  static final String AGGREGATE_PEAR_FILE_ARG = "-f";

  // command line parameters
  private static File[] __pearFiles = null;

  private static String __aggregateName = null;

  private static File __aggregatePearFile = null;

  /**
   * The <code>PMLogFormatter</code> class formats messages for the log file.
   */
  static class PMLogFormatter extends SimpleFormatter {
    private boolean _firstTime = true;

    @Override
    public String format(LogRecord record) {
      if (_firstTime) {
        // 1st time - print with header
        _firstTime = false;
        record.setSourceMethodName("");
        return super.format(record);
      } else {
        // print level: message
        StringBuffer buffer = new StringBuffer();
        buffer.append(record.getLevel());
        String message = record.getMessage();
        // for multi-line msg - start msg in new line
        if (message.indexOf('\n') >= 0)
          buffer.append(": \n");
        else
          // for single line msg - start msg in the same line
          buffer.append(": ");
        buffer.append(record.getMessage());
        buffer.append('\n');
        return buffer.toString();
      }
    }
  }

  // static attributes
  private static Logger __logger = null;

  private static boolean __logFileEnabled = false;
  // static Logger initialization
  static {
    // create default logger
    __logger = Logger.getLogger(PMController.class.getName());
    __logger.setUseParentHandlers(false);
  }

  // internal attributes
  private File[] _inpPearFiles = null;

  private String _outAggCompName = null;

  private File _outAggPearFile = null;

  private File _tempWorkDir = null;

  private File _outAggRootDir = null;

  private File[] _outDlgRootDirs = null;

  private InstallationDescriptor[] _dlgInstDescs = null;

  private InstallationDescriptor _outAggInstDesc = null;

  /**
   * The command line application entry point. This method expects the following command line
   * arguments:
   * <ul>
   * <li>pear_file_1 ... pear_file_n - input PEAR files (required)</li>
   * <li>-n agg_name - a name of the output aggregate analysis engine (required)</li>
   * <li>-f agg_pear_file - output aggregate PEAR file (optional). <br>
   * If the output PEAR file is not specified, the default output PEAR file is created in the
   * current working directory.</li>
   * </ul>
   * 
   * @param args
   *          pear_file_1 ... pear_file_n -n agg_name [-f agg_pear_file]
   */
  public static void main(String[] args) {
    // enable log file
    setLogFileEnabled(true);
    // parse and validate command line
    if (!parseCommandLine(args)) {
      logErrorMessage(PEAR_MERGER + " terminated: command line error");
      return;
    }
    PMController controller = null;
    try {
      // create controller object
      controller = new PMController(__pearFiles, __aggregateName, __aggregatePearFile);
      // merge input PEARs and create merged aggregate PEAR file
      if (controller.mergePears()) {
        logInfoMessage("[" + PEAR_MERGER + "]: " + "operation completed successfully");
      } else
        logInfoMessage("[" + PEAR_MERGER + "]: " + "operation failed");
    } catch (Throwable err) {
      logErrorMessage("Error in " + PEAR_MERGER + ": " + err.toString());
    } finally {
      if (controller != null) {
        try {
          controller.cleanUp();
        } catch (Exception e) {
        }
      }
    }
  }

  /**
   * Returns the instance of the class-specific <code>Logger</code> object.
   * 
   * @return The instance of the class-specific <code>Logger</code> object.
   */
  public static Logger getLogger() {
    return __logger;
  }

  /**
   * Logs a given error message to the log file and prints it to the standard error console stream.
   * 
   * @param message
   *          The given error message.
   */
  public static void logErrorMessage(String message) {
    if (__logFileEnabled)
      getLogger().severe(message);
    System.err.println(message);
  }

  /**
   * Logs a given info message to the log file and prints it to the standard output console stream.
   * 
   * @param message
   *          The given info message.
   */
  public static void logInfoMessage(String message) {
    if (__logFileEnabled)
      getLogger().info(message);
    System.out.println(message);
  }

  /**
   * Logs a given warning message to the log file and prints it to the standard error console
   * stream.
   * 
   * @param message
   *          The given warning message.
   */
  public static void logWarningMessage(String message) {
    if (__logFileEnabled)
      getLogger().warning(message);
    System.err.println(message);
  }

  /**
   * Parses and validates the command line and initializes static attributes.
   * 
   * @param args
   *          The given command line arguments.
   * @return <code>true</code>, if the command line arguments are valid, <code>false</code>
   *         otherwise.
   */
  private static boolean parseCommandLine(String[] args) {
    // verify command line args (min 4 args: p_1 p_2 -n name)
    if (args.length < 4) {
      logErrorMessage(PEAR_MERGER + " args: " + "pear_file_1 ... pear_file_n -n agg_name "
              + "[-f agg_pear_file]");
      return false;
    }
    ArrayList listOfPears = new ArrayList();
    // parse command line
    for (int i = 0; i < args.length; i++) {
      if (args[i].equals(AGGREGATE_NAME_ARG)) {
        // set aggregate name
        if (++i < args.length)
          __aggregateName = args[i];
      } else if (args[i].equals(AGGREGATE_PEAR_FILE_ARG)) {
        // set aggregate PEAR file
        if (++i < args.length)
          __aggregatePearFile = new File(args[i]);
      } else if (args[i].startsWith("-")) {
        logErrorMessage(PEAR_MERGER + " error: " + "unknown flag '" + args[i] + "'");
        return false;
      } else {
        // add next input PEAR file
        File inPear = new File(args[i]);
        if (inPear.isFile()) {
          // make sure this is not duplicated input file
          for (int n = 0; n < listOfPears.size(); n++) {
            try {
              String fPath = ((File) listOfPears.get(n)).getCanonicalPath();
              String inPath = inPear.getCanonicalPath();
              if (fPath.equals(inPath)) {
                logErrorMessage(PEAR_MERGER + " error: " + "duplicated input file " + args[i]);
                return false;
              }
            } catch (IOException e) {
              logErrorMessage(PEAR_MERGER + " error: " + e.toString());
              return false;
            }
          }
          listOfPears.add(inPear);
        } else {
          logErrorMessage(PEAR_MERGER + " error: " + "cannot find input file " + args[i]);
          return false;
        }
      }
    }
    // validate command line parameters and set input files
    if (listOfPears.size() < 2) {
      logErrorMessage(PEAR_MERGER + " error: " + "input PEAR files not specified");
      return false;
    } else {
      __pearFiles = new File[listOfPears.size()];
      listOfPears.toArray(__pearFiles);
    }
    if (__aggregateName == null) {
      logErrorMessage(PEAR_MERGER + " error: " + "output aggregate name not specified");
      return false;
    }
    return true;
  }

  /**
   * Enables/disables PM log file. By default, the log file is disabled.
   * 
   * @param enable
   *          if <code>true</code>, the log file is enabled, otherwise it is disabled.
   */
  public static void setLogFileEnabled(boolean enable) {
    if (enable) {
      Handler[] handlers = getLogger().getHandlers();
      if (handlers == null || handlers.length == 0) {
        // add default file handler
        try {
          FileHandler fileHandler = new FileHandler(LOG_FILE, false);
          fileHandler.setLevel(Level.ALL);
          fileHandler.setFormatter(new PMLogFormatter());
          getLogger().addHandler(fileHandler);
        } catch (Throwable err) {
          System.err.println("Error initializing log file " + PMController.class.getName() + ": "
                  + err.toString());
        }
      }
    }
    __logFileEnabled = enable;
  }

  /**
   * Constructor that takes a given array of input PEAR files, a given output component name (ID)
   * and a given output PEAR file.
   * 
   * @param inpPearFiles
   *          The given array of input PEAR files.
   * @param outCompName
   *          The given output component name (ID).
   * @param outPearFile
   *          The given output PEAR file.
   * @exception IOException
   *              If any I/O exception occurred during initialization.
   */
  public PMController(File[] inpPearFiles, String outCompName, File outPearFile)
          throws IOException {
    // initialize task attributes
    _inpPearFiles = inpPearFiles;
    _outAggCompName = outCompName;
    _outAggPearFile = outPearFile;
    initializeTaskAttributes();
    // log PEAR Merger task parameters
    logInfoMessage("[" + PEAR_MERGER + "]: task parameters =>");
    logInfoMessage("> Input PEARs =>");
    for (int i = 0; i < _inpPearFiles.length; i++)
      logInfoMessage(">> " + _inpPearFiles[i].getAbsolutePath());
    logInfoMessage("> Output PEAR =>");
    logInfoMessage(">> Name = " + _outAggCompName);
    logInfoMessage(">> File = " + _outAggPearFile.getAbsolutePath());
    logInfoMessage("> Output root dir: " + _outAggRootDir.getAbsolutePath());
  }

  /**
   * Deletes all temporary directories and files after the merging operation is completed.
   * 
   * @throws IOException
   *           If an I/O exception occurred.
   */
  public void cleanUp() throws IOException {
    logInfoMessage("[" + PEAR_MERGER + "]: " + "deleting temporary files");
    FileUtil.deleteDirectory(_outAggRootDir);
  }

  /**
   * Extracts all specified input PEARs to their corresponding folders inside the output root
   * folder. Returns the total size (bytes) of extracted files.
   * 
   * @return The total size of extracted files.
   * @throws IOException
   *           If any I/O exception occurred during extraction.
   */
  private long extractInputPears() throws IOException {
    long totalSize = 0;
    for (int i = 0; i < _inpPearFiles.length; i++) {
      JarFile pearFile = new JarFile(_inpPearFiles[i]);
      totalSize += FileUtil.extractFilesFromJar(pearFile, _outDlgRootDirs[i]);
    }
    return totalSize;
  }

  /**
   * Intializes the 'PEAR Merger' task attributes, based on the specified input.
   * 
   * @exception IOException
   *              If any I/O exception occurred during initialization.
   */
  private void initializeTaskAttributes() throws IOException {
    if (_outAggPearFile == null) // set default output file
      _outAggPearFile = new File(_outAggCompName + ".pear");
    // set temporary working directory
    String userHomePath = System.getProperty("user.home");
    _tempWorkDir = new File(userHomePath);
    if (!_tempWorkDir.isDirectory())
      throw new IOException(userHomePath + " directory not found");
    // set output aggregate root directory
    _outAggRootDir = new File(_tempWorkDir, _outAggCompName);
    // if output aggregate root directory exists, remove it
    if (_outAggRootDir.isDirectory()) {
      if (!FileUtil.deleteDirectory(_outAggRootDir))
        throw new IOException("cannot delete existing folder " + _outAggRootDir.getAbsolutePath());
    } else if (_outAggRootDir.isFile()) {
      if (!_outAggRootDir.delete())
        throw new IOException("cannot delete existing file " + _outAggRootDir.getAbsolutePath());
    }
    // set output delegate root directories
    _outDlgRootDirs = new File[_inpPearFiles.length];
    for (int i = 0; i < _outDlgRootDirs.length; i++) {
      String fileName = _inpPearFiles[i].getName();
      String sdirName = fileName.substring(0, fileName.lastIndexOf('.'));
      _outDlgRootDirs[i] = new File(_outAggRootDir, sdirName);
    }
    // prepare array for delegate installation descriptors
    _dlgInstDescs = new InstallationDescriptor[_inpPearFiles.length];
  }

  /**
   * Merges specified input PEARs into one PEAR, which encapsulates aggregate AE that refers to
   * input components, as delegates. Returns <code>true</code>, if the merging operation completed
   * successfully, <code>false</code> otherwise.
   * 
   * @return <code>true</code>, if the merge operation completed successfully, <code>false</code>
   *         otherwise.
   * @throws IOException
   *           If an I/O exception occurred during the merging operation.
   */
  public boolean mergePears() throws IOException {
    boolean done = false;
    // 1st step: extract delegate PEARs to their target dirs
    logInfoMessage("[" + PEAR_MERGER + "]: " + "extracting delegate PEARs ...");
    long totalSize = extractInputPears();
    logInfoMessage("[" + PEAR_MERGER + "]: " + totalSize + " bytes extracted successfully");
    // 2nd step: process files in delegate packages and create InsD objs
    for (int i = 0; i < _outDlgRootDirs.length; i++) {
      _dlgInstDescs[i] = PMControllerHelper.processDescriptors(_outDlgRootDirs[i]);
      if (_dlgInstDescs[i] == null) {
        logErrorMessage("[" + PEAR_MERGER + "]: " + "failed to process input package in "
                + _outDlgRootDirs[i] + "directory");
        break;
      }
    }
    // 3rd step: generate merged package directory structure
    File pkgDescDir = new File(_outAggRootDir, PackageBrowser.DESCRIPTORS_DIR);
    File pkgMetadataDir = new File(_outAggRootDir, PackageBrowser.METADATA_DIR);
    if (pkgDescDir.mkdirs() && pkgMetadataDir.mkdirs())
      logInfoMessage("[" + PEAR_MERGER + "]: " + "created merged package directory structure");
    else
      throw new IOException("cannot create merged package folders");
    // 4th step: generate aggregate component descriptor
    File aggDescFile = new File(pkgDescDir, _outAggCompName + ".xml");
    AnalysisEngineDescription aggDescription = PMUimaAgent
            .createAggregateDescription(_outAggCompName, _outAggRootDir, _dlgInstDescs);
    if (aggDescription != null) {
      PMUimaAgent.saveAggregateDescription(aggDescription, aggDescFile);
      logInfoMessage("[" + PEAR_MERGER + "]: " + "generated aggregate component descriptor");
      if (System.getProperty("DEBUG") != null)
        logInfoMessage(PMUimaAgent.toXmlString(aggDescription));
    } else
      throw new IOException("cannot generate aggregate component descriptor");
    // 5th step: generate merged installation descriptor
    _outAggInstDesc = PMControllerHelper.generateMergedInstallationDescriptor(_outAggRootDir,
            _outAggCompName, aggDescFile, _dlgInstDescs, _outDlgRootDirs);
    if (_outAggInstDesc != null) {
      logInfoMessage(
              "[" + PEAR_MERGER + "]: " + "generated aggregate package installation descriptor");
      if (System.getProperty("DEBUG") != null)
        logInfoMessage(_outAggInstDesc.toString());
      // 6th step: create merged PEAR file
      File outPearFile = FileUtil.zipDirectory(_outAggRootDir, _outAggPearFile);
      logInfoMessage("[" + PEAR_MERGER + "]: " + "created output aggregate PEAR file - "
              + outPearFile.getAbsolutePath());
      done = true;
    }
    return done;
  }
}
