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

/* $Id: RenderPagesModel.java 1897194 2022-01-19 10:43:17Z ssteiner $ */

package org.apache.fop.area;

// Java
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.xml.sax.SAXException;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.render.Renderer;
import org.apache.fop.render.RendererEventProducer;

/**
 * This uses the AreaTreeModel to store the pages
 * Each page is either rendered if ready or prepared
 * for later rendering.
 * Once a page is rendered it is cleared to release the
 * contents but the PageViewport is retained. So even
 * though the pages are stored the contents are discarded.
 */
public class RenderPagesModel extends AreaTreeModel {
    /**
     * The renderer that will render the pages.
     */
    protected Renderer renderer;

    /**
     * Pages that have been prepared but not rendered yet.
     */
    protected List<PageViewport> prepared = new java.util.ArrayList<PageViewport>();

    private List<OffDocumentItem> pendingODI = new java.util.ArrayList<OffDocumentItem>();
    private List<OffDocumentItem> endDocODI = new java.util.ArrayList<OffDocumentItem>();

    /**
     * Create a new render pages model with the given renderer.
     * @param userAgent FOUserAgent object for process
     * @param outputFormat the MIME type of the output format to use (ex. "application/pdf").
     * @param fontInfo FontInfo object
     * @param stream OutputStream
     * @throws FOPException if the renderer cannot be properly initialized
     */
    public RenderPagesModel(FOUserAgent userAgent, String outputFormat,
        FontInfo fontInfo, OutputStream stream) throws FOPException {

        super();
        this.renderer = userAgent.getRendererFactory().createRenderer(
                userAgent, outputFormat);

        try {
            renderer.setupFontInfo(fontInfo);
            // check that the "any,normal,400" font exists
            if (!fontInfo.isSetupValid()) {
                throw new FOPException(
                    "No default font defined by OutputConverter");
            }
            renderer.startRenderer(stream);
        } catch (IOException e) {
            throw new FOPException(e);
        }
    }

    @Override
    public void setDocumentLocale(Locale locale) {
        renderer.setDocumentLocale(locale);
    }

    /** {@inheritDoc} */
    @Override
    public void startPageSequence(PageSequence pageSequence) {
        super.startPageSequence(pageSequence);
        if (renderer.supportsOutOfOrder()) {
            renderer.startPageSequence(getCurrentPageSequence());
        }
    }

    /**
     * Add a page to the render page model.
     * If the page is finished it can be rendered immediately.
     * If the page needs resolving then if the renderer supports
     * out of order rendering it can prepare the page. Otherwise
     * the page is added to a queue.
     * @param page the page to add to the model
     */
    @Override
    public void addPage(PageViewport page) {
        super.addPage(page);

        // for links the renderer needs to prepare the page
        // it is more appropriate to do this after queued pages but
        // it will mean that the renderer has not prepared a page that
        // could be referenced
        boolean ready = renderer.supportsOutOfOrder() && page.isResolved();
        if (ready) {
            if (!renderer.supportsOutOfOrder() && page.getPageSequence().isFirstPage(page)) {
                renderer.startPageSequence(getCurrentPageSequence());
            }
            try {
                renderer.renderPage(page);
            } catch (RuntimeException re) {
                String err = "Error while rendering page " + page.getPageNumberString();
                log.error(err, re);
                throw re;
            } catch (IOException ioe) {
                RendererEventProducer eventProducer = RendererEventProducer.Provider.get(
                        renderer.getUserAgent().getEventBroadcaster());
                eventProducer.ioError(this, ioe);
            } catch (FOPException e) {
                //TODO use error handler to handle this FOPException or propagate exception
                String err = "Error while rendering page " + page.getPageNumberString();
                log.error(err, e);
                throw new IllegalStateException("Fatal error occurred. Cannot continue. "
                        + e.getClass().getName() + ": " + err);
            }
            page.clear();
        } else {
            preparePage(page);
        }


        // check prepared pages
        boolean cont = checkPreparedPages(page, false);

        if (cont) {
            processOffDocumentItems(pendingODI);
            pendingODI.clear();
        }
    }

    /**
     * Check prepared pages
     *
     * @param newPageViewport the new page being added
     * @param renderUnresolved render pages with unresolved idref's
     *          (done at end-of-document processing)
     * @return true if the current page should be rendered
     *         false if the renderer doesn't support out of order
     *         rendering and there are pending pages
     */
    protected boolean checkPreparedPages(PageViewport newPageViewport,
                                         boolean renderUnresolved) {

        for (Iterator iter = prepared.iterator(); iter.hasNext();) {
            PageViewport pageViewport = (PageViewport)iter.next();
            if (pageViewport.isResolved() || renderUnresolved) {
                if (!renderer.supportsOutOfOrder()
                        && pageViewport.getPageSequence().isFirstPage(pageViewport)) {
                    renderer.startPageSequence(pageViewport.getPageSequence());
                }
                renderPage(pageViewport);
                pageViewport.clear();
                iter.remove();
            } else {
                // if keeping order then stop at first page not resolved
                if (!renderer.supportsOutOfOrder()) {
                    break;
                }
            }
        }
        return renderer.supportsOutOfOrder() || prepared.isEmpty();
    }

    /**
     * Renders the given page and notified about unresolved IDs if any.
     * @param pageViewport the page to be rendered.
     */
    protected void renderPage(PageViewport pageViewport) {
        try {
            renderer.renderPage(pageViewport);
        } catch (Exception e) {
            AreaEventProducer eventProducer = AreaEventProducer.Provider.get(
                    renderer.getUserAgent().getEventBroadcaster());
            eventProducer.pageRenderingError(this,
                    pageViewport.getPageNumberString(), e);
            if (e instanceof RuntimeException) {
                throw (RuntimeException)e;
            }
        }
    }

    /**
     * Prepare a page.
     * An unresolved page can be prepared if the renderer supports
     * it and the page will be rendered later.
     * @param page the page to prepare
     */
    protected void preparePage(PageViewport page) {
        if (renderer.supportsOutOfOrder()) {
            renderer.preparePage(page);
        }
        prepared.add(page);
    }

    /** {@inheritDoc} */
    @Override
    public void handleOffDocumentItem(OffDocumentItem oDI) {
        switch(oDI.getWhenToProcess()) {
            case OffDocumentItem.IMMEDIATELY:
                renderer.processOffDocumentItem(oDI);
                break;
            case OffDocumentItem.AFTER_PAGE:
                pendingODI.add(oDI);
                break;
            case OffDocumentItem.END_OF_DOC:
                endDocODI.add(oDI);
                break;
            default:
                throw new RuntimeException();
        }
    }

    private void processOffDocumentItems(List<OffDocumentItem> list) {
        for (OffDocumentItem oDI : list) {
            renderer.processOffDocumentItem(oDI);
        }
    }

    /**
     * End the document. Render any end document OffDocumentItems
     * {@inheritDoc}
     */
    @Override
    public void endDocument() throws SAXException {
        // render any pages that had unresolved ids
        checkPreparedPages(null, true);

        processOffDocumentItems(pendingODI);
        pendingODI.clear();
        processOffDocumentItems(endDocODI);

        try {
            renderer.stopRenderer();
        } catch (IOException ex) {
            throw new SAXException(ex);
        }
    }
}

