/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org 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 version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/
package com.sun.star.wizards.web;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

import com.sun.star.lang.XMultiServiceFactory;
import com.sun.star.wizards.common.ConfigSet;
import com.sun.star.wizards.common.FileAccess;
import com.sun.star.wizards.common.UCB;
import com.sun.star.wizards.ui.event.Task;
import com.sun.star.wizards.web.data.CGContent;
import com.sun.star.wizards.web.data.CGDocument;
import com.sun.star.wizards.web.data.CGExporter;
import com.sun.star.wizards.web.data.CGLayout;
import com.sun.star.wizards.web.data.CGPublish;
import com.sun.star.wizards.web.data.CGSettings;
import com.sun.star.wizards.web.export.Exporter;

/**
 * @author rpiterman
 * This class is used to process a CGSession object
 * and generate a site. </br>
 * it does the following: <br/>
 * 1. create a temporary directory.<br/>
 * 2. export documents to the temporary directory.<br/>
 * 3. generate the TOC page, includes copying images from the
 * web wizard work directory and other layout files.<br/>
 * 4. publish, or copy, from the temporary directory to
 * different destinations.<br/>
 * 5. delete the temporary directory.<br/>
 * <br/>
 * to follow up the status/errors it uses a TaskListener object,
 * and an ErrorHandler. <br/>
 * in practice, the TaskListener is the status dialog,
 * and the Errorhandler does the interaction with the user,
 * if something goes wrong.<br/>
 * Note that this class takes it in count that
 * the given session object is prepared for it -
 * all preparations are done in WWD_Events.finishWizard methods.
 * <br/>
 * <br/>
 *
 * note on error handling: <br/>
 * on "catch" clauses I tries to decide whether the
 * exception is fatal or not. For fatal exception an error message
 * is displayed (or rather: the errorHandler is being called...)
 * and a false is returned.
 * In less-fatal errors, the errorHandler "should decide" which means,
 * the user is given the option to "OK" or to "Cancel" and depending
 * on that interaction I cary on.
 */
public class Process implements WebWizardConst, ProcessErrors
{

    private static final int TASKS_PER_DOC = 5;
    private static final int TASKS_PER_XSL = 2;
    private static final int TASKS_PER_PUBLISH = 2;
    private static final int TASKS_IN_PREPARE = 1;
    private static final int TASKS_IN_EXPORT = 2;
    private static final int TASKS_IN_GENERATE = 2;
    private static final int TASKS_IN_PUBLISH = 2;
    private static final int TASKS_IN_FINISHUP = 1;
    private CGSettings settings;
    private XMultiServiceFactory xmsf;
    private ErrorHandler errorHandler;
    private String tempDir;
    private FileAccess fileAccess;
    private UCB ucb;
    public Task myTask;
    /**
     * This is a cache for exporters, so I do not need to
     * instanciate the same exporter more than once.
     */
    private Map exporters = new Hashtable(3);
    private boolean result;

    public Process(
            CGSettings settings,
            XMultiServiceFactory xmsf,
            ErrorHandler er)
            throws Exception
    {
        this.xmsf = xmsf;
        this.settings = settings;
        fileAccess = new FileAccess(xmsf);
        errorHandler = er;

        ucb = new UCB(xmsf);

        int taskSteps = getTaskSteps();
        myTask = new Task(TASK, TASK_PREPARE, taskSteps);

    }

    /**
     * @return to how many destinations should the
     * generated site be published.
     */
    private int countPublish()
    {
        int count = 0;
        ConfigSet publishers = settings.cp_DefaultSession.cp_Publishing;
        for (int i = 0; i < publishers.getSize(); i++)
        {
            if (((CGPublish) publishers.getElementAt(i)).cp_Publish)
            {
                count++;
            }
        }
        return count;
    }

    /**
     * @return the number of task steps that this
     * session should have
     */
    private int getTaskSteps()
    {
        int docs = settings.cp_DefaultSession.cp_Content.cp_Documents.getSize();
        int xsl = 0;
        try
        {
            xsl = settings.cp_DefaultSession.getLayout().getTemplates(xmsf).size();
        }
        catch (Exception ex)
        {
        }
        int publish = countPublish();
        int taskSteps =
                TASKS_IN_PREPARE +
                TASKS_IN_EXPORT + docs * TASKS_PER_DOC +
                TASKS_IN_GENERATE + xsl * TASKS_PER_XSL +
                TASKS_IN_PUBLISH + publish * TASKS_PER_PUBLISH +
                TASKS_IN_FINISHUP;
        return taskSteps;
    }

    /**
     * does the job
     */
    public void runProcess()
    {
        myTask.start();
        try
        {
            try
            {
                /*
                 * I use here '&&' so if one of the
                 * methods returns false, the next
                 * will not be called.
                 */
                result = createTempDir(myTask) && export(myTask) && generate(tempDir, myTask) && publish(tempDir, myTask);

            }
            finally
            {
                //cleanup must be called.
                result = result & cleanup(myTask);
            }
        }
        catch (Exception ex)
        {
            result = false;
        }

        if (!result)
        {
            myTask.fail();        //this is a bug protection.
        }
        while (myTask.getStatus() < myTask.getMax())
        {
            myTask.advance(true);
        }
    }

    /**
     * creates a temporary directory.
     * @param task
     * @return true should continue
     */
    private boolean createTempDir(Task task)
    {

        tempDir = fileAccess.createNewDir(getSOTempDir(xmsf), "wwiztemp");
        if (tempDir == null)
        {
            error(null, null, ERROR_MKDIR, ErrorHandler.ERROR_PROCESS_FATAL);
            return false;
        }
        else
        {
            task.advance(true);
            return true;
        }
    }

    /**
     * @param xmsf
     * @return the staroffice /openoffice temporary directory
     */
    static String getSOTempDir(XMultiServiceFactory xmsf)
    {
        try
        {
            String s = FileAccess.getOfficePath(xmsf, "Temp", "", "");
            return s;
        }
        catch (Exception e)
        {
        }
        return null;
    }

    // CLEANUP
    /**
     * delete the temporary directory
     * @return true should continue
     */
    private boolean cleanup(Task task)
    {

        task.setSubtaskName(TASK_FINISH);
        boolean b = fileAccess.delete(tempDir);
        if (!b)
        {
            error(null, null, ERROR_CLEANUP, ErrorHandler.ERROR_WARNING);
        }
        task.advance(b);
        return b;
    }

//  /**
//   * deletes the given directory
//   * @param dir the directory to delete
//   * @return true if should continue
//   */
//  private boolean cleanup(String dir) {
//
//      boolean success = true;
//
//      if (dir != null && fileAccess.exists(dir,false)) {
//
//          String[] files = fileAccess.listFiles(dir,true);
//
//          for (int i = 0; i < files.length; i++) {
//              if (fileAccess.isDirectory(files[i]))
//                  success = success && cleanup(files[i]);
//              else
//                  success = success && fileAccess.delete(files[i]);
//
//          }
//      }
//      return success && fileAccess.delete(dir);
//  }
    /**
     * This method is used to copy style files to a target
     * Directory: css and background.
     * Note that this method is static since it is
     * also used when displaying a "preview"
     */
    public static void copyMedia(UCB copy, CGSettings settings, String targetDir, Task task) throws Exception
    {

        //1. .css
        String sourceDir = FileAccess.connectURLs(settings.workPath, "styles");
        String filename = settings.cp_DefaultSession.getStyle().cp_CssHref;
        copy.copy(sourceDir, filename, targetDir, "style.css");

        task.advance(true);

        //2. background image
        String background = settings.cp_DefaultSession.cp_Design.cp_BackgroundImage;
        if (background != null && !background.equals(""))
        {
            sourceDir = FileAccess.getParentDir(background);
            filename = background.substring(sourceDir.length());
            copy.copy(sourceDir, filename, targetDir + "/images", "background.gif");
        }

        task.advance(true);
    }

    /**
     * Copy "static" files (which are always the same,
     * thus not user-input-dependant) to a target directory.
     * Note that this method is static since it is
     * also used when displaying a "preview"
     * @param copy
     * @param settings
     * @param targetDir
     * @throws Exception
     */
    public static void copyStaticImages(UCB copy, CGSettings settings, String targetDir)
            throws Exception
    {
        copy.copy(FileAccess.connectURLs(settings.workPath, "images"), targetDir + "/images");
    }

    /**
     * publish the given directory.
     * @param dir the source directory to publish from
     * @param task task tracking.
     * @return true if should continue
     */
    private boolean publish(String dir, Task task)
    {
        task.setSubtaskName(TASK_PUBLISH_PREPARE);
        ConfigSet set = settings.cp_DefaultSession.cp_Publishing;
        try
        {

            copyMedia(ucb, settings, dir, task);
            copyStaticImages(ucb, settings, dir);
            task.advance(true);
        }
        catch (Exception ex)
        {
            //error in copying media
            error(ex, "", ERROR_PUBLISH_MEDIA, ErrorHandler.ERROR_PROCESS_FATAL);
            return false;
        }

        boolean result = true;

        for (int i = 0; i < set.getSize(); i++)
        {

            CGPublish p = (CGPublish) set.getElementAt(i);

            if (p.cp_Publish)
            {

                String key = (String) set.getKey(p);
                task.setSubtaskName(key);

                if (key.equals(ZIP_PUBLISHER))
                {
                    fileAccess.delete(p.cp_URL);
                }
                if (!publish(dir, p, ucb, task))
                {
                    return false;
                }

            }
        }

        return result;
    }

    /**
     * publish the given directory to the
     * given target CGPublish.
     * @param dir the dir to copy from
     * @param publish the object that specifies the target
     * @param copy ucb encapsulation
     * @param task task tracking
     * @return true if should continue
     */
    private boolean publish(String dir, CGPublish publish, UCB copy, Task task)
    {
        try
        {
            //copy.deleteDirContent(publish.url);
            task.advance(true);
            copy.copy(dir, publish.url);
            task.advance(true);
            return true;
        }
        catch (Exception e)
        {
            task.advance(false);
            return error(e, publish, ERROR_PUBLISH, ErrorHandler.ERROR_NORMAL_IGNORE);
        }
    }
    //GENERATING METHODS
    /**
     * Generates the TOC pages for the current session.
     * @param targetDir generating to this directory.
     */
    public boolean generate(String targetDir, Task task)
    {
        boolean result = false;
        task.setSubtaskName(TASK_GENERATE_PREPARE);


        CGLayout layout = settings.cp_DefaultSession.getLayout();

        try
        {
            /*
             * here I create the DOM of the TOC to pass to the XSL
             */
            Document doc = (Document) settings.cp_DefaultSession.createDOM();
            generate(xmsf, layout, doc, fileAccess, targetDir, task);

        }
        catch (Exception ex)
        {
            error(ex, "", ERROR_GENERATE_XSLT, ErrorHandler.ERROR_PROCESS_FATAL);
            return false;
        }

        /* copy files which are not xsl from layout directory to
         * website root.
         */
        try
        {

            task.setSubtaskName(TASK_GENERATE_COPY);

            copyLayoutFiles(ucb, fileAccess, settings, layout, targetDir);

            task.advance(true);

            result = true;
        }
        catch (Exception ex)
        {
            task.advance(false);
            return error(ex, null, ERROR_GENERATE_COPY, ErrorHandler.ERROR_NORMAL_ABORT);
        }



        return result;

    }

    /**
     * copies layout files which are not .xsl files
     * to the target directory.
     * @param ucb UCB encapsulatzion object
     * @param fileAccess filaAccess encapsulation object
     * @param settings web wizard settings
     * @param layout the layout object
     * @param targetDir the target directory to copy to
     * @throws Exception
     */
    public static void copyLayoutFiles(UCB ucb, FileAccess fileAccess, CGSettings settings, CGLayout layout, String targetDir)
            throws Exception
    {
        String filesPath = fileAccess.getURL(
                FileAccess.connectURLs(settings.workPath, "layouts/"), layout.cp_FSName);
        ucb.copy(filesPath, targetDir, new ExtensionVerifier("xsl"));

    }

    /**
     * generates the TOC page for the given layout.
     * This method might generate more than one file, depending
     * on how many .xsl files are in the
     * directory specifies by the given layout object.
     * @param xmsf
     * @param layout specifies the layout to use.
     * @param doc the DOM representation of the web wizard session
     * @param fileAccess encapsulation of FileAccess
     * @param targetPath target directory
     * @param task
     * @throws Exception
     */
    public static void generate(
            XMultiServiceFactory xmsf,
            CGLayout layout,
            Document doc,
            FileAccess fileAccess,
            String targetPath,
            Task task)
            throws Exception
    {
        /*
         * a map that contains xsl templates. the keys are the xsl file names.
         */
        Map templates = layout.getTemplates(xmsf);

        task.advance(true, TASK_GENERATE_XSL);

        /*
         * each template generates a page.
         */
        for (Iterator i = templates.keySet().iterator(); i.hasNext();)
        {

            String key = "";

            key = (String) i.next();

            Transformer transformer = ((Templates) templates.get(key)).newTransformer();

            doc.normalize();
            task.advance(true);

            /*
             * The target file name is like the xsl template filename
             * without the .xsl extension.
             */
            String fn = fileAccess.getPath(targetPath, key.substring(0, key.length() - 4));
            File f = new File(fn);
            FileOutputStream oStream = new FileOutputStream(f);
            // Due to a problem occuring when using Xalan-Java 2.6.0 and
            // Java 1.5.0, wrap f in a FileOutputStream here (otherwise, the
            // StreamResult's getSystemId would return a "file:/..." URL while
            // the Xalan code expects a "file:///..." URL):
            transformer.transform(
                    new DOMSource(doc), new StreamResult(oStream));
            oStream.close();
            task.advance(true);
        }
    }

    /**
     * I broke the export method to two methods
     * in a time where a tree with more than one contents was planned.
     * I left it that way, because it may be used in the future.
     * @param task
     * @return
     */
    private boolean export(Task task)
    {

        return export(settings.cp_DefaultSession.cp_Content, tempDir, task);

    }

    /**
     * This method could actually, with light modification, use recursion.
     * In the present situation, where we only use a "flat" list of
     * documents, instead of the original plan to use a tree,
     * the recursion is not implemented.
     * @param content the content ( directory-like, contains documents)
     * @param dir (target directory for exporting this content.
     * @param task
     * @return true if should continue
     */
    private boolean export(CGContent content, String dir, Task task)
    {
        int toPerform = 1;
        String contentDir = dir;

        try
        {

            task.setSubtaskName(TASK_EXPORT_PREPARE);

            /* 1. create a content directory.
             * each content (at the moment there is only one :-( )
             * is created in its own directory.
             * faileure here is fatal.
             */
            contentDir = fileAccess.createNewDir(dir, content.cp_Name);
            if (contentDir == null || contentDir.equals(""))
            {
                throw new IOException("Directory " + dir + " could not be created.");
            }
            content.dirName = FileAccess.getFilename(contentDir);

            task.advance(true, TASK_EXPORT_DOCUMENTS);
            toPerform--;

            /*2. export all documents and sub contents.
             * (at the moment, only documents, no subcontents)
             */
            Object item = null;
            for (int i = 0; i < content.cp_Documents.getSize(); i++)
            {
                try
                {
                    item = content.cp_Documents.getElementAt(i);
                    /*
                     * In present this is always the case.
                     * may be in the future, when
                     * a tree is used, it will be abit different.
                     */
                    if (item instanceof CGDocument)
                    {
                        if (!export((CGDocument) item, contentDir, task))
                        {
                            return false;
                        }
                    }
                    else /*
                     * we never get here since we
                     * did not implement sub-contents.
                     */ if (!export((CGContent) item, contentDir, task))
                    {
                        return false;
                    }
                }
                catch (SecurityException sx)
                {
                    // nonfatal
                    if (!error(sx, item, ERROR_EXPORT_SECURITY, ErrorHandler.ERROR_NORMAL_IGNORE))
                    {
                        return false;
                    }
                    result = false;
                }
            }
        }
        catch (IOException iox)
        {
            //nonfatal
            return error(iox, content, ERROR_EXPORT_IO, ErrorHandler.ERROR_NORMAL_IGNORE);

        }
        catch (SecurityException se)
        {
            //nonfatal
            return error(se, content, ERROR_EXPORT_SECURITY, ErrorHandler.ERROR_NORMAL_IGNORE);
        }
        failTask(task, toPerform);
        return true;

    }

    /**
     * exports a single document
     * @param doc the document to export
     * @param dir the target directory
     * @param task task tracking
     * @return true if should continue
     */
    private boolean export(CGDocument doc, String dir, Task task)
    {

        //first I check if the document was already validated...
        if (!doc.valid)
        {
            try
            {
                doc.validate(xmsf, null);
            }
            catch (Exception ex)
            {
                //fatal
                error(ex, doc, ERROR_DOC_VALIDATE, ErrorHandler.ERROR_PROCESS_FATAL);
                return false;
            }
        //get the exporter specified for this document
        }
        CGExporter exporter = (CGExporter) settings.cp_Exporters.getElement(doc.cp_Exporter);


        try
        {

            /*
             * here I calculate the destination filename.
             * I take the original filename (docFilename), substract the extension, (docExt) -> (fn)
             * and find an available filename which starts with
             * this filename, but with the new extension. (destExt)
             */
            String docFilename = FileAccess.getFilename(doc.cp_URL);

            String docExt = FileAccess.getExtension(docFilename);
            String fn = doc.localFilename.substring(0, doc.localFilename.length() - docExt.length() - 1); //filename without extension

            /*
             * the copyExporter does not change
             * the extension of the target...
             */
            String destExt = (exporter.cp_Extension.equals("")
                    ? FileAccess.getExtension(docFilename)
                    : exporter.cp_Extension);

            /* if this filter needs to export to its own directory...
             * this is the case in, for example, impress html export
             */
            if (exporter.cp_OwnDirectory)
            { //+++
                dir = fileAccess.createNewDir(dir, fn);
                doc.dirName = FileAccess.getFilename(dir);
            }

            /*
             * if two files with the same name
             * need to be exported ? So here
             * i get a new filename, so I do not
             * overwrite files...
             */
            String file = fileAccess.getNewFile(dir, fn, destExt);


            /* set filename with extension.
             * this will be used by the exporter,
             * and to generate the TOC.
             */
            doc.urlFilename = FileAccess.getFilename(file);

            task.advance(true);

            try
            {
                //export
                getExporter(exporter).export(doc, file, xmsf, task);
                task.advance(true);
            }
            /*
             * getExporter(..) throws
             * IllegalAccessException, InstantiationException, ClassNotFoundException
             * export() throws Exception
             */
            catch (Exception ex)
            {
                //nonfatal
                if (!error(ex, doc, ERROR_EXPORT, ErrorHandler.ERROR_NORMAL_IGNORE))
                {
                    return false;
                }
            }
        }
        catch (Exception ex)
        {
            //nonfatal
            if (!error(ex, doc, ERROR_EXPORT_MKDIR, ErrorHandler.ERROR_NORMAL_ABORT))
            {
                return false;
            }
        }

        return true;

    }

    /**
     * submit an error.
     * @param ex the exception
     * @param arg1 error argument
     * @param arg2 error argument 2
     * @param errType error type
     * @return the interaction result
     */
    private boolean error(Exception ex, Object arg1, int arg2, int errType)
    {
        result = false;
        return errorHandler.error(ex, arg1, arg2, errType);
    }

    /**
     * advances the given task in the given count of steps,
     * marked as failed.
     * @param task the task to advance
     * @param count the number of steps to advance
     */
    private void failTask(Task task, int count)
    {
        while (count-- > 0)
        {
            task.advance(false);
        }
    }

    /**
     * creates an instance of the exporter class
     * as specified by the
     * exporter object.
     * @param export specifies the exporter to be created
     * @return the Exporter instance
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private Exporter createExporter(CGExporter export)
            throws ClassNotFoundException,
            IllegalAccessException,
            InstantiationException
    {
        Exporter e = (Exporter) Class.forName(export.cp_ExporterClass).newInstance();
        e.init(export);
        return e;
    }

    /**
     * searches the an exporter for the given CGExporter object
     * in the cache.
     * If its not there, creates it, stores it in the cache and
     * returns it.
     * @param export specifies the needed exporter.
     * @return an Exporter instance
     * @throws ClassNotFoundException thrown when using Class.forName(string)
     * @throws IllegalAccessException thrown when using Class.forName(string)
     * @throws InstantiationException thrown when using Class.forName(string)
     */
    private Exporter getExporter(CGExporter export)
            throws ClassNotFoundException,
            IllegalAccessException,
            InstantiationException
    {
        Exporter exp = (Exporter) exporters.get(export);
        if (exp == null)
        {
            exp = createExporter(export);
            exporters.put(export, exp);
        }
        return exp;
    }

    /**
     * @return tru if everything went smooth, false
     * if error(s) accured.
     */
    public boolean getResult()
    {
        return (myTask.getFailed() == 0) && result;
    }
}
