/*
 * 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: PDFFilterList.java 1761026 2016-09-16 12:50:43Z ssteiner $ */

package org.apache.fop.pdf;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * This class represents a list of PDF filters to be applied when serializing
 * the output of a PDF object.
 */
public class PDFFilterList {

    /** Key for the default filter */
    public static final String DEFAULT_FILTER = "default";
    /** Key for the filter used for normal content*/
    public static final String CONTENT_FILTER = "content";
    /** Key for the filter used for precompressed content */
    public static final String PRECOMPRESSED_FILTER = "precompressed";
    /** Key for the filter used for images */
    public static final String IMAGE_FILTER = "image";
    /** Key for the filter used for JPEG images */
    public static final String JPEG_FILTER = "jpeg";
    /** Key for the filter used for TIFF images */
    public static final String TIFF_FILTER = "tiff";
    /** Key for the filter used for fonts */
    public static final String FONT_FILTER = "font";
    /** Key for the filter used for metadata */
    public static final String METADATA_FILTER = "metadata";

    private List<PDFFilter> filters = new java.util.ArrayList<PDFFilter>();

    private boolean ignoreASCIIFilters;

    private boolean disableAllFilters;

    /**
     * Default constructor.
     * <p>
     * The flag for ignoring ASCII filters defaults to false.
     */
    public PDFFilterList() {
        //nop
    }

    /**
     * Use this descriptor if you want to have ASCII filters (such as ASCIIHex
     * and ASCII85) ignored, for example, when encryption is active.
     * @param ignoreASCIIFilters true if ASCII filters should be ignored
     */
    public PDFFilterList(boolean ignoreASCIIFilters) {
        this.ignoreASCIIFilters = ignoreASCIIFilters;
    }

    /**
     * Used to disable all filters.
     * @param value true if all filters shall be disabled
     */
    public void setDisableAllFilters(boolean value) {
        this.disableAllFilters = value;
    }

    /**
     * Returns true if all filters are disabled.
     * @return true if all filters are disabled
     */
    public boolean isDisableAllFilters() {
        return this.disableAllFilters;
    }

    /**
     * Indicates whether the filter list is already initialized.
     * @return true if more there are filters present
     */
    public boolean isInitialized() {
        return this.filters.size() > 0;
    }

    /**
     * Add a filter for compression of the stream. Filters are
     * applied in the order they are added. This should always be a
     * new instance of the particular filter of choice. The applied
     * flag in the filter is marked true after it has been applied to the
     * data.
     * @param filter filter to add
     */
    public void addFilter(PDFFilter filter) {
        if (filter != null) {
            if (this.ignoreASCIIFilters && filter.isASCIIFilter()) {
                return; //ignore ASCII filter
            }
            filters.add(filter);
        }
    }

    /**
     * Add a filter for compression of the stream by name.
     * @param filterType name of the filter to add
     */
    public void addFilter(String filterType) {
        if (filterType == null) {
            return;
        }
        if (filterType.equals("flate")) {
            addFilter(new FlateFilter());
        } else if (filterType.equals("null")) {
            addFilter(new NullFilter());
        } else if (filterType.equals("ascii-85")) {
            if (this.ignoreASCIIFilters) {
                return; //ignore ASCII filter
            }
            addFilter(new ASCII85Filter());
        } else if (filterType.equals("ascii-hex")) {
            if (this.ignoreASCIIFilters) {
                return; //ignore ASCII filter
            }
            addFilter(new ASCIIHexFilter());
        } else if (filterType.equals("")) {
            return;
        } else {
            throw new IllegalArgumentException(
                "Unsupported filter type in stream-filter-list: " + filterType);
        }
    }

    /**
     * Checks the filter list for the filter and adds it in the correct
     * place if necessary.
     * @param pdfFilter the filter to check / add
     */
    public void ensureFilterInPlace(PDFFilter pdfFilter) {
        if (this.filters.size() == 0) {
            addFilter(pdfFilter);
        } else {
            if (!(this.filters.get(0).equals(pdfFilter))) {
                this.filters.add(0, pdfFilter);
            }
        }
    }

    /**
     * Adds the default filters to this stream.
     * @param filters Map of filters
     * @param type which filter list to modify
     */
    public void addDefaultFilters(Map filters, String type) {
        if (METADATA_FILTER.equals(type)) {
            //XMP metadata should not be embedded in clear-text
            addFilter(new NullFilter());
            return;
        }
        List filterset = null;
        if (filters != null) {
            filterset = (List)filters.get(type);
            if (filterset == null) {
                filterset = (List)filters.get(DEFAULT_FILTER);
            }
        }
        if (filterset == null || filterset.size() == 0) {
            if (JPEG_FILTER.equals(type)) {
                //JPEG is already well compressed
                addFilter(new NullFilter());
            } else if (TIFF_FILTER.equals(type)) {
                //CCITT-encoded images are already well compressed
                addFilter(new NullFilter());
            } else if (PRECOMPRESSED_FILTER.equals(type)) {
                //precompressed content doesn't need further compression
                addFilter(new NullFilter());
            } else {
                // built-in default to flate
                addFilter(new FlateFilter());
            }
        } else {
            for (Object aFilterset : filterset) {
                String v = (String) aFilterset;
                addFilter(v);
            }
        }
    }

    List<PDFFilter> getFilters() {
        return Collections.unmodifiableList(filters);
    }

    /**
     * Apply the filters to the data
     * in the order given and return the /Filter and /DecodeParms
     * entries for the stream dictionary. If the filters have already
     * been applied to the data (either externally, or internally)
     * then the dictionary entries are built and returned.
     * @return a String representing the filter list
     */
    protected String buildFilterDictEntries() {
        if (filters.size() > 0) {
            List names = new java.util.ArrayList();
            List parms = new java.util.ArrayList();

            int nonNullParams = populateNamesAndParms(names, parms);

            // now build up the filter entries for the dictionary
            return buildFilterEntries(names)
                    + (nonNullParams > 0 ? buildDecodeParms(parms) : "");
        }
        return "";

    }

    /**
     * Apply the filters to the data
     * in the order given and add the /Filter and /DecodeParms
     * entries to the stream dictionary. If the filters have already
     * been applied to the data (either externally, or internally)
     * then the dictionary entries added.
     * @param dict the PDFDictionary to set the entries on
     */
    protected void putFilterDictEntries(PDFDictionary dict) {
        if (filters.size() > 0) {
            List names = new java.util.ArrayList();
            List parms = new java.util.ArrayList();

            populateNamesAndParms(names, parms);

            // now build up the filter entries for the dictionary
            putFilterEntries(dict, names);
            putDecodeParams(dict, parms);
        }
    }

    private int populateNamesAndParms(List names, List parms) {
        // run the filters
        int nonNullParams = 0;
        for (PDFFilter filter : filters) {
            // place the names in our local vector in reverse order
            if (filter.getName().length() > 0) {
                names.add(0, filter.getName());
                PDFObject param = filter.getDecodeParms();
                if (param != null) {
                    parms.add(0, param);
                    nonNullParams++;
                } else {
                    parms.add(0, null);
                }
            }
        }
        return nonNullParams;
    }

    private String buildFilterEntries(List names) {
        int filterCount = 0;
        StringBuffer sb = new StringBuffer(64);
        for (Object name1 : names) {
            final String name = (String) name1;
            if (name.length() > 0) {
                filterCount++;
                sb.append(name);
                sb.append(" ");
            }
        }
        if (filterCount > 0) {
            if (filterCount > 1) {
                return "/Filter [ " + sb + "]";
            } else {
                return "/Filter " + sb;
            }
        } else {
            return "";
        }
    }

    private void putFilterEntries(PDFDictionary dict, List names) {
        PDFArray array = new PDFArray(dict);
        for (Object name1 : names) {
            final String name = (String) name1;
            if (name.length() > 0) {
                array.add(new PDFName(name));
            }
        }
        if (array.length() > 0) {
            if (array.length() > 1) {
                dict.put("Filter", array);
            } else {
                dict.put("Filter", array.get(0));
            }
        }
    }

    private String buildDecodeParms(List parms) {
        StringBuffer sb = new StringBuffer();
        boolean needParmsEntry = false;
        sb.append("\n/DecodeParms ");

        if (parms.size() > 1) {
            sb.append("[ ");
        }
        for (Object parm : parms) {
            String s = (String) parm;
            if (s != null) {
                sb.append(s);
                needParmsEntry = true;
            } else {
                sb.append("null");
            }
            sb.append(" ");
        }
        if (parms.size() > 1) {
            sb.append("]");
        }
        if (needParmsEntry) {
            return sb.toString();
        } else {
            return "";
        }
    }

    private void putDecodeParams(PDFDictionary dict, List parms) {
        boolean needParmsEntry = false;
        PDFArray array = new PDFArray(dict);
        for (Object obj : parms) {
            if (obj != null) {
                array.add(obj);
                needParmsEntry = true;
            } else {
                array.add(null);
            }
        }
        if (array.length() > 0 & needParmsEntry) {
            if (array.length() > 1) {
                dict.put("DecodeParms", array);
            } else {
                dict.put("DecodeParms", array.get(0));
            }
        }
    }

    /**
     * Applies all registered filters as necessary. The method returns an
     * OutputStream which will receive the filtered contents.
     * @param stream raw data output stream
     * @return OutputStream filtered output stream
     * @throws IOException In case of an I/O problem
     */
    public OutputStream applyFilters(OutputStream stream) throws IOException {
        OutputStream out = stream;
        if (!isDisableAllFilters()) {
            for (int count = filters.size() - 1; count >= 0; count--) {
                PDFFilter filter = filters.get(count);
                out = filter.applyFilter(out);
            }
        }
        return out;
    }
}
