package org.apache.maven.doxia.docrenderer.pdf.itext;

/*
 * 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.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.maven.doxia.docrenderer.DocumentRendererContext;
import org.apache.maven.doxia.docrenderer.DocumentRendererException;
import org.apache.maven.doxia.docrenderer.pdf.AbstractPdfRenderer;
import org.apache.maven.doxia.docrenderer.pdf.PdfRenderer;
import org.apache.maven.doxia.document.DocumentCover;
import org.apache.maven.doxia.document.DocumentMeta;
import org.apache.maven.doxia.document.DocumentModel;
import org.apache.maven.doxia.document.DocumentTOCItem;
import org.apache.maven.doxia.module.itext.ITextSink;
import org.apache.maven.doxia.module.itext.ITextSinkFactory;
import org.apache.maven.doxia.module.itext.ITextUtil;
import org.apache.maven.doxia.parser.module.ParserModule;
import org.apache.xml.utils.DefaultErrorHandler;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.lowagie.text.ElementTags;

/**
 * Abstract <code>document</code> render with the <code>iText</code> framework
 *
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @author ltheussl
 * @since 1.1
 */
@Deprecated
@Component( role = PdfRenderer.class, hint = "itext" )
public class ITextPdfRenderer
    extends AbstractPdfRenderer
{
    /** The xslt style sheet used to transform a Document to an iText file. */
    private static final String XSLT_RESOURCE = "TOC.xslt";

    /** The TransformerFactory. */
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();

    /** The DocumentBuilderFactory. */
    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();

    /** The DocumentBuilder. */
    private static final DocumentBuilder DOCUMENT_BUILDER;

    static
    {
        TRANSFORMER_FACTORY.setErrorListener( new DefaultErrorHandler() );

        try
        {
            DOCUMENT_BUILDER = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
        }
        catch ( ParserConfigurationException e )
        {
            throw new RuntimeException( "Error building document :" + e.getMessage() );
        }
    }

    /** {@inheritDoc} */
    public void generatePdf( File inputFile, File pdfFile )
        throws DocumentRendererException
    {
        if ( getLogger().isDebugEnabled() )
        {
            getLogger().debug( "Generating : " + pdfFile );
        }

        try
        {
            ITextUtil.writePdf( new FileInputStream( inputFile ), new FileOutputStream( pdfFile ) );
        }
        catch ( IOException e )
        {
            throw new DocumentRendererException( "Cannot create PDF from " + inputFile + ": " + e.getMessage(), e );
        }
        catch ( RuntimeException e )
        {
            throw new DocumentRendererException( "Error creating PDF from " + inputFile + ": " + e.getMessage(), e );
        }
    }

    /** {@inheritDoc} */
    @Override
    public void render( Map<String, ParserModule> filesToProcess, File outputDirectory, DocumentModel documentModel )
        throws DocumentRendererException, IOException
    {
        render( filesToProcess, outputDirectory, documentModel, null );
    }

    /** {@inheritDoc} */
    @Override
    public void render( Map<String, ParserModule> filesToProcess, File outputDirectory, DocumentModel documentModel,
                        DocumentRendererContext context )
        throws DocumentRendererException, IOException
    {
        // copy resources, images, etc.
        copyResources( outputDirectory );

        if ( documentModel == null )
        {
            getLogger().debug( "No document model, generating all documents individually." );

            renderIndividual( filesToProcess, outputDirectory, context );
            return;
        }

        String outputName = getOutputName( documentModel );

        File outputITextFile = new File( outputDirectory, outputName + ".xml" );
        if ( !outputITextFile.getParentFile().exists() )
        {
            outputITextFile.getParentFile().mkdirs();
        }

        File pdfOutputFile = new File( outputDirectory, outputName + ".pdf" );
        if ( !pdfOutputFile.getParentFile().exists() )
        {
            pdfOutputFile.getParentFile().mkdirs();
        }

        List<File> iTextFiles;
        if ( ( documentModel.getToc() == null ) || ( documentModel.getToc().getItems() == null ) )
        {
            getLogger().info( "No TOC is defined in the document descriptor. Merging all documents." );

            iTextFiles = parseAllFiles( filesToProcess, outputDirectory, context );
        }
        else
        {
            getLogger().debug( "Using TOC defined in the document descriptor." );

            iTextFiles = parseTOCFiles( outputDirectory, documentModel, context );
        }

        String generateTOC =
            ( context != null && context.get( "generateTOC" ) != null ? context.get( "generateTOC" ).toString()
                            : "start" );

        File iTextFile = new File( outputDirectory, outputName + ".xml" );
        File iTextOutput = new File( outputDirectory, outputName + "." + getOutputExtension() );
        Document document = generateDocument( iTextFiles );
        transform( documentModel, document, iTextFile, generateTOC );
        generatePdf( iTextFile, iTextOutput );
    }

    /** {@inheritDoc} */
    @Override
    public void renderIndividual( Map<String, ParserModule> filesToProcess, File outputDirectory )
        throws DocumentRendererException, IOException
    {
        renderIndividual( filesToProcess, outputDirectory, null );
    }

    /** {@inheritDoc} */
    @Override
    public void renderIndividual( Map<String, ParserModule> filesToProcess, File outputDirectory,
                                  DocumentRendererContext context )
        throws DocumentRendererException, IOException
    {
        for ( Map.Entry<String, ParserModule> entry : filesToProcess.entrySet() )
        {
            String key = entry.getKey();
            ParserModule module = entry.getValue();
            File fullDoc = new File( getBaseDir(), module.getSourceDirectory() + File.separator + key );

            String output = key;
            for ( String extension : module.getExtensions() )
            {
                String lowerCaseExtension = extension.toLowerCase( Locale.ENGLISH );
                if ( output.toLowerCase( Locale.ENGLISH ).indexOf( "." + lowerCaseExtension ) != -1 )
                {
                    output =
                        output.substring( 0, output.toLowerCase( Locale.ENGLISH ).indexOf( "." + lowerCaseExtension ) );
                }
            }

            File outputITextFile = new File( outputDirectory, output + ".xml" );
            if ( !outputITextFile.getParentFile().exists() )
            {
                outputITextFile.getParentFile().mkdirs();
            }

            File pdfOutputFile = new File( outputDirectory, output + ".pdf" );
            if ( !pdfOutputFile.getParentFile().exists() )
            {
                pdfOutputFile.getParentFile().mkdirs();
            }

            parse( fullDoc, module, outputITextFile, context );

            generatePdf( outputITextFile, pdfOutputFile );
        }
    }

      //--------------------------------------------
     //
    //--------------------------------------------


    /**
     * Parse a source document and emit results into a sink.
     *
     * @param fullDocPath file to the source document.
     * @param module the site module associated with the source document (determines the parser to use).
     * @param iTextFile the resulting iText xml file.
     * @throws DocumentRendererException in case of a parsing problem.
     * @throws IOException if the source and/or target document cannot be opened.
     */
    private void parse( File fullDoc, ParserModule module, File iTextFile, DocumentRendererContext context )
        throws DocumentRendererException, IOException
    {
        if ( getLogger().isDebugEnabled() )
        {
            getLogger().debug( "Parsing file " + fullDoc.getAbsolutePath() );
        }

        System.setProperty( "itext.basedir", iTextFile.getParentFile().getAbsolutePath() );

        Writer writer = null;
        ITextSink sink = null;
        try
        {
            writer = WriterFactory.newXmlWriter( iTextFile );
            sink = (ITextSink) new ITextSinkFactory().createSink( writer );

            sink.setClassLoader( new URLClassLoader( new URL[] { iTextFile.getParentFile().toURI().toURL() } ) );

            parse( fullDoc.getAbsolutePath(), module.getParserId(), sink, context );
        }
        finally
        {
            if ( sink != null )
            {
                sink.flush();
                sink.close();
            }
            IOUtil.close( writer );
            System.getProperties().remove( "itext.basedir" );
        }
    }

    /**
     * Merge all iTextFiles to a single one.
     *
     * @param iTextFiles list of iText xml files.
     * @return Document.
     * @throws DocumentRendererException if any.
     * @throws IOException if any.
     */
    private Document generateDocument( List<File> iTextFiles )
        throws DocumentRendererException, IOException
    {
        Document document = DOCUMENT_BUILDER.newDocument();
        document.appendChild( document.createElement( ElementTags.ITEXT ) ); // Used only to set a root

        for ( File iTextFile : iTextFiles )
        {
            Document iTextDocument;

            try
            {
                iTextDocument = DOCUMENT_BUILDER.parse( iTextFile );
            }
            catch ( SAXException e )
            {
                throw new DocumentRendererException( "SAX Error : " + e.getMessage() );
            }

            // Only one chapter per doc
            Node chapter = iTextDocument.getElementsByTagName( ElementTags.CHAPTER ).item( 0 );

            try
            {
                document.getDocumentElement().appendChild( document.importNode( chapter, true ) );
            }
            catch ( DOMException e )
            {
                throw new DocumentRendererException( "Error appending chapter for "
                        + iTextFile + " : " + e.getMessage() );
            }
        }

        return document;
    }

    /**
     * Initialize the transformer object.
     *
     * @return an instance of a transformer object.
     * @throws DocumentRendererException if any.
     */
    private Transformer initTransformer()
        throws DocumentRendererException
    {
        try
        {
            Transformer transformer = TRANSFORMER_FACTORY.newTransformer( new StreamSource( ITextPdfRenderer.class
                .getResourceAsStream( XSLT_RESOURCE ) ) );

            transformer.setErrorListener( TRANSFORMER_FACTORY.getErrorListener() );

            transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "false" );

            transformer.setOutputProperty( OutputKeys.INDENT, "yes" );

            transformer.setOutputProperty( OutputKeys.METHOD, "xml" );

            transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" );

            // No doctype since itext doctype is not up to date!

            return transformer;
        }
        catch ( TransformerConfigurationException e )
        {
            throw new DocumentRendererException( "Error configuring Transformer for " + XSLT_RESOURCE + ": "
                + e.getMessage() );
        }
        catch ( IllegalArgumentException e )
        {
            throw new DocumentRendererException( "Error configuring Transformer for " + XSLT_RESOURCE + ": "
                + e.getMessage() );
        }
    }

    /**
     * Add transformer parameters from a DocumentModel.
     *
     * @param transformer the Transformer to set the parameters.
     * @param documentModel the DocumentModel to take the parameters from, could be null.
     * @param iTextFile the iTextFile not null for the relative paths.
     * @param generateTOC not null, possible values are: 'none', 'start' and 'end'.
     */
    private void addTransformerParameters( Transformer transformer, DocumentModel documentModel, File iTextFile,
                                           String generateTOC )
    {
        if ( documentModel == null )
        {
            return;
        }

        // TOC
        addTransformerParameter( transformer, "toc.position", generateTOC );

        // Meta parameters
        boolean hasNullMeta = false;
        if ( documentModel.getMeta() == null )
        {
            hasNullMeta = true;
            documentModel.setMeta( new DocumentMeta() );
        }
        addTransformerParameter( transformer, "meta.author", documentModel.getMeta().getAllAuthorNames(),
                                 System.getProperty( "user.name", "null" ) );
        addTransformerParameter( transformer, "meta.creator", documentModel.getMeta().getCreator(),
                                 System.getProperty( "user.name", "null" ) );
        // see com.lowagie.text.Document#addCreationDate()
        SimpleDateFormat sdf = new SimpleDateFormat( "EEE MMM dd HH:mm:ss zzz yyyy" );
        addTransformerParameter( transformer, "meta.creationdate", documentModel.getMeta().getCreationdate(),
                                 sdf.format( new Date() ) );
        addTransformerParameter( transformer, "meta.keywords", documentModel.getMeta().getAllKeyWords() );
        addTransformerParameter( transformer, "meta.pagesize", documentModel.getMeta().getPageSize(),
                                 ITextUtil.getPageSize( ITextUtil.getDefaultPageSize() ) );
        addTransformerParameter( transformer, "meta.producer", documentModel.getMeta().getGenerator(),
                                 "Apache Doxia iText" );
        addTransformerParameter( transformer, "meta.subject", documentModel.getMeta().getSubject(),
                                 ( documentModel.getMeta().getTitle() != null ? documentModel.getMeta().getTitle()
                                                 : "" ) );
        addTransformerParameter( transformer, "meta.title", documentModel.getMeta().getTitle() );
        if ( hasNullMeta )
        {
            documentModel.setMeta( null );
        }

        // cover parameter
        boolean hasNullCover = false;
        if ( documentModel.getCover() == null )
        {
            hasNullCover = true;
            documentModel.setCover( new DocumentCover() );
        }
        addTransformerParameter( transformer, "cover.author", documentModel.getCover().getAllAuthorNames(),
                                 System.getProperty( "user.name", "null" ) );
        String companyLogo = getLogoURL( documentModel.getCover().getCompanyLogo(), iTextFile.getParentFile() );
        addTransformerParameter( transformer, "cover.companyLogo", companyLogo );
        addTransformerParameter( transformer, "cover.companyName", documentModel.getCover().getCompanyName() );
        if ( documentModel.getCover().getCoverdate() == null )
        {
            documentModel.getCover().setCoverDate( new Date() );
            addTransformerParameter( transformer, "cover.date", documentModel.getCover().getCoverdate() );
            documentModel.getCover().setCoverDate( null );
        }
        else
        {
            addTransformerParameter( transformer, "cover.date", documentModel.getCover().getCoverdate() );
        }
        addTransformerParameter( transformer, "cover.subtitle", documentModel.getCover().getCoverSubTitle() );
        addTransformerParameter( transformer, "cover.title", documentModel.getCover().getCoverTitle() );
        addTransformerParameter( transformer, "cover.type", documentModel.getCover().getCoverType() );
        addTransformerParameter( transformer, "cover.version", documentModel.getCover().getCoverVersion() );
        String projectLogo = getLogoURL( documentModel.getCover().getProjectLogo(), iTextFile.getParentFile() );
        addTransformerParameter( transformer, "cover.projectLogo", projectLogo );
        addTransformerParameter( transformer, "cover.projectName", documentModel.getCover().getProjectName() );
        if ( hasNullCover )
        {
            documentModel.setCover( null );
        }
    }

    /**
     * @param transformer not null
     * @param name not null
     * @param value could be empty
     * @param defaultValue could be empty
     * @since 1.1.1
     */
    private void addTransformerParameter( Transformer transformer, String name, String value, String defaultValue )
    {
        if ( StringUtils.isEmpty( value ) )
        {
            addTransformerParameter( transformer, name, defaultValue );
        }
        else
        {
            addTransformerParameter( transformer, name, value );
        }
    }

    /**
     * @param transformer not null
     * @param name not null
     * @param value could be empty
     * @since 1.1.1
     */
    private void addTransformerParameter( Transformer transformer, String name, String value )
    {
        if ( StringUtils.isEmpty( value ) )
        {
            return;
        }

        transformer.setParameter( name, value );
    }

    /**
     * Transform a document to an iTextFile.
     *
     * @param documentModel the DocumentModel to take the parameters from, could be null.
     * @param document the Document to transform.
     * @param iTextFile the resulting iText xml file.
     * @param generateTOC not null, possible values are: 'none', 'start' and 'end'.
     * @throws DocumentRendererException in case of a transformation error.
     */
    private void transform( DocumentModel documentModel, Document document, File iTextFile, String generateTOC )
        throws DocumentRendererException
    {
        Transformer transformer = initTransformer();

        addTransformerParameters( transformer, documentModel, iTextFile, generateTOC );

        // need a writer for StreamResult to prevent FileNotFoundException when iTextFile contains spaces
        Writer writer = null;
        try
        {
            writer = WriterFactory.newXmlWriter( iTextFile );
            transformer.transform( new DOMSource( document ), new StreamResult( writer ) );
        }
        catch ( TransformerException e )
        {
            throw new DocumentRendererException(
                                                 "Error transforming Document " + document + ": " + e.getMessage(),
                                                 e );
        }
        catch ( IOException e )
        {
            throw new DocumentRendererException(
                                                 "Error transforming Document " + document + ": " + e.getMessage(),
                                                 e );
        }
        finally
        {
            IOUtil.close( writer );
        }
    }

    /**
     * @param filesToProcess not null
     * @param outputDirectory not null
     * @return a list of all parsed files.
     * @throws DocumentRendererException if any
     * @throws IOException if any
     * @since 1.1.1
     */
    private List<File> parseAllFiles( Map<String, ParserModule> filesToProcess, File outputDirectory,
                                      DocumentRendererContext context )
        throws DocumentRendererException, IOException
    {
        List<File> iTextFiles = new LinkedList<File>();
        for ( Map.Entry<String, ParserModule> entry : filesToProcess.entrySet() )
        {
            String key = entry.getKey();
            ParserModule module = entry.getValue();
            File fullDoc = new File( getBaseDir(), module.getSourceDirectory() + File.separator + key );

            String outputITextName = key.substring( 0, key.lastIndexOf( '.' ) + 1 ) + "xml";
            File outputITextFileTmp = new File( outputDirectory, outputITextName );
            outputITextFileTmp.deleteOnExit();
            if ( !outputITextFileTmp.getParentFile().exists() )
            {
                outputITextFileTmp.getParentFile().mkdirs();
            }

            iTextFiles.add( outputITextFileTmp );
            parse( fullDoc, module, outputITextFileTmp, context );
        }

        return iTextFiles;
    }

    /**
     * @param filesToProcess not null
     * @param outputDirectory not null
     * @return a list of all parsed files.
     * @throws DocumentRendererException if any
     * @throws IOException if any
     * @since 1.1.1
     */
    private List<File> parseTOCFiles( File outputDirectory, DocumentModel documentModel,
                                      DocumentRendererContext context )
        throws DocumentRendererException, IOException
    {
        List<File> iTextFiles = new LinkedList<File>();
        for ( Iterator<DocumentTOCItem> it = documentModel.getToc().getItems().iterator(); it.hasNext(); )
        {
            DocumentTOCItem tocItem = it.next();

            if ( tocItem.getRef() == null )
            {
                getLogger().debug(
                                   "No ref defined for the tocItem '" + tocItem.getName()
                                       + "' in the document descriptor. IGNORING" );
                continue;
            }

            String href = StringUtils.replace( tocItem.getRef(), "\\", "/" );
            if ( href.lastIndexOf( '.' ) != -1 )
            {
                href = href.substring( 0, href.lastIndexOf( '.' ) );
            }

            Collection<ParserModule> modules = parserModuleManager.getParserModules();
            for ( ParserModule module : modules )
            {
                File moduleBasedir = new File( getBaseDir(), module.getSourceDirectory() );

                if ( moduleBasedir.exists() )
                {
                    for ( String extension : module.getExtensions() )
                    {
                        String doc = href + "." + extension;
                        File source = new File( moduleBasedir, doc );

                        // Velocity file?
                        if ( !source.exists() )
                        {
                            if ( href.indexOf( "." + extension ) != -1 )
                            {
                                doc = href + ".vm";
                            }
                            else
                            {
                                doc = href + "." + extension + ".vm";
                            }
                            source = new File( moduleBasedir, doc );
                        }

                        if ( source.exists() )
                        {
                            String outputITextName = doc.substring( 0, doc.lastIndexOf( '.' ) + 1 ) + "xml";
                            File outputITextFileTmp = new File( outputDirectory, outputITextName );
                            outputITextFileTmp.deleteOnExit();
                            if ( !outputITextFileTmp.getParentFile().exists() )
                            {
                                outputITextFileTmp.getParentFile().mkdirs();
                            }

                            iTextFiles.add( outputITextFileTmp );
                            parse( source, module, outputITextFileTmp, context );
                        }
                    }
                }
            }
        }

        return iTextFiles;
    }

    /**
     * @param logo
     * @param parentFile
     * @return the logo url or null if unable to create it.
     * @since 1.1.1
     */
    private String getLogoURL( String logo, File parentFile )
    {
        if ( logo == null )
        {
            return null;
        }

        try
        {
            return new URL( logo ).toString();
        }
        catch ( MalformedURLException e )
        {
            try
            {
                File f = new File( parentFile, logo );
                if ( !f.exists() )
                {
                    getLogger().warn( "The logo " + f.getAbsolutePath() + " doesnt exist. IGNORING" );
                }
                else
                {
                    return f.toURI().toURL().toString();
                }
            }
            catch ( MalformedURLException e1 )
            {
                getLogger().debug( "Failed to convert to URL: " + logo, e1 );
            }
        }

        return null;
    }
}
