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

package org.apache.fop.render.pdf;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.MimeConstants;
import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.fonts.DefaultFontConfig;
import org.apache.fop.fonts.DefaultFontConfig.DefaultFontConfigParser;
import org.apache.fop.fonts.FontEventAdapter;
import org.apache.fop.pdf.PDFEncryptionParams;
import org.apache.fop.pdf.PDFFilterList;
import org.apache.fop.render.RendererConfig;
import org.apache.fop.render.RendererConfigOption;
import org.apache.fop.util.LogUtil;

import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_LENGTH;
import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_PARAMS;
import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPT_METADATA;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ACCESSCONTENT;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ANNOTATIONS;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ASSEMBLEDOC;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_COPY_CONTENT;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_EDIT_CONTENT;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_FILLINFORMS;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINT;
import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINTHQ;
import static org.apache.fop.render.pdf.PDFEncryptionOption.OWNER_PASSWORD;
import static org.apache.fop.render.pdf.PDFEncryptionOption.USER_PASSWORD;
import static org.apache.fop.render.pdf.PDFRendererOption.DISABLE_SRGB_COLORSPACE;
import static org.apache.fop.render.pdf.PDFRendererOption.FILTER_LIST;
import static org.apache.fop.render.pdf.PDFRendererOption.FORM_XOBJECT;
import static org.apache.fop.render.pdf.PDFRendererOption.LINEARIZATION;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FONTS;
import static org.apache.fop.render.pdf.PDFRendererOption.OUTPUT_PROFILE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_A_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_UA_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_VT_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_X_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.VERSION;

/**
 * The PDF renderer configuration data object.
 */
public final class PDFRendererConfig implements RendererConfig {

    private static final Log LOG = LogFactory.getLog(PDFRendererConfig.class);

    private final PDFRendererOptionsConfig configOption;

    private final DefaultFontConfig fontConfig;

    private PDFRendererConfig(DefaultFontConfig fontConfig, PDFRendererOptionsConfig config) {
        this.fontConfig = fontConfig;
        this.configOption = config;
    }

    public PDFRendererOptionsConfig getConfigOptions() {
        return configOption;
    }

    public DefaultFontConfig getFontInfoConfig() {
        return fontConfig;
    }

    /**
     * The PDF renderer configuration data parser.
     */
    public static final class PDFRendererConfigParser implements RendererConfigParser {

        public PDFRendererConfig build(FOUserAgent userAgent, Configuration cfg) throws FOPException {
            boolean strict = userAgent != null ? userAgent.validateUserConfigStrictly() : false;
            return new ParserHelper(cfg, userAgent, strict).pdfConfig;
        }

        public String getMimeType() {
            return MimeConstants.MIME_PDF;
        }
    }

    private static final class ParserHelper {

        private final Map<PDFRendererOption, Object> configOptions
        = new EnumMap<PDFRendererOption, Object>(PDFRendererOption.class);

        private PDFEncryptionParams encryptionConfig;

        private PDFRendererConfig pdfConfig;

        private ParserHelper(Configuration cfg, FOUserAgent userAgent, boolean strict) throws FOPException {
            if (cfg != null) {
                configure(cfg, userAgent, strict);
            }
            if (userAgent == null) {
                pdfConfig = new PDFRendererConfig(new DefaultFontConfigParser().parse(cfg, strict),
                        new PDFRendererOptionsConfig(configOptions, encryptionConfig));
            } else {
                pdfConfig = new PDFRendererConfig(new DefaultFontConfigParser().parse(cfg, strict,
                        new FontEventAdapter(userAgent.getEventBroadcaster())),
                        new PDFRendererOptionsConfig(configOptions, encryptionConfig));
            }
        }

        private void parseAndPut(PDFRendererOption option, Configuration cfg) {
            put(option, option.parse(parseConfig(cfg, option)));
        }

        private void put(PDFRendererOption option, Object value) {
            if (value != null && !value.equals(option.getDefaultValue())) {
                configOptions.put(option, value);
            }
        }

        private void configure(Configuration cfg, FOUserAgent userAgent, boolean strict) throws FOPException {
            try {
                buildFilterMapFromConfiguration(cfg);
                parseAndPut(PDF_A_MODE, cfg);
                parseAndPut(PDF_UA_MODE, cfg);
                parseAndPut(PDF_X_MODE, cfg);
                parseAndPut(PDF_VT_MODE, cfg);
                configureEncryptionParams(cfg, userAgent, strict);
                parseAndPut(OUTPUT_PROFILE, cfg);
                parseAndPut(DISABLE_SRGB_COLORSPACE, cfg);
                parseAndPut(MERGE_FONTS, cfg);
                parseAndPut(LINEARIZATION, cfg);
                parseAndPut(FORM_XOBJECT, cfg);
                parseAndPut(VERSION, cfg);
            } catch (ConfigurationException e) {
                LogUtil.handleException(LOG, e, strict);
            }
        }

        private void configureEncryptionParams(Configuration cfg, FOUserAgent userAgent, boolean strict) {
            Configuration encryptCfg = cfg.getChild(ENCRYPTION_PARAMS, false);
            if (encryptCfg != null) {
                encryptionConfig = new PDFEncryptionParams();
                encryptionConfig.setOwnerPassword(parseConfig(encryptCfg, OWNER_PASSWORD));
                encryptionConfig.setUserPassword(parseConfig(encryptCfg, USER_PASSWORD));
                encryptionConfig.setAllowPrint(!doesValueExist(encryptCfg, NO_PRINT));
                encryptionConfig.setAllowCopyContent(!doesValueExist(encryptCfg, NO_COPY_CONTENT));
                encryptionConfig.setAllowEditContent(!doesValueExist(encryptCfg, NO_EDIT_CONTENT));
                encryptionConfig.setAllowEditAnnotations(!doesValueExist(encryptCfg, NO_ANNOTATIONS));
                encryptionConfig.setAllowFillInForms(!doesValueExist(encryptCfg, NO_FILLINFORMS));
                encryptionConfig.setAllowAccessContent(!doesValueExist(encryptCfg, NO_ACCESSCONTENT));
                encryptionConfig.setAllowAssembleDocument(!doesValueExist(encryptCfg, NO_ASSEMBLEDOC));
                encryptionConfig.setAllowPrintHq(!doesValueExist(encryptCfg, NO_PRINTHQ));
                encryptionConfig.setEncryptMetadata(getConfigValue(encryptCfg, ENCRYPT_METADATA, true));
                String encryptionLength = parseConfig(encryptCfg, ENCRYPTION_LENGTH);
                if (encryptionLength != null) {
                    int validatedLength = checkEncryptionLength(Integer.parseInt(encryptionLength), userAgent);
                    encryptionConfig.setEncryptionLengthInBits(validatedLength);
                }
            }
        }

        private void buildFilterMapFromConfiguration(Configuration cfg) throws ConfigurationException,
        FOPException {
            Configuration[] filterLists = cfg.getChildren(FILTER_LIST.getName());
            Map<String, List<String>> filterMap = new HashMap<String, List<String>>();
            for (Configuration filters : filterLists) {
                String type = filters.getAttribute("type", PDFFilterList.DEFAULT_FILTER);
                List<String> filterList = new ArrayList<String>();
                for (Configuration nameCfg : filters.getChildren("value")) {
                    filterList.add(nameCfg.getValue());
                }
                if (!filterList.isEmpty() && LOG.isDebugEnabled()) {
                    StringBuffer debug = new StringBuffer("Adding PDF filter");
                    if (filterList.size() != 1) {
                        debug.append("s");
                    }
                    debug.append(" for type ").append(type).append(": ");
                    for (int j = 0; j < filterList.size(); j++) {
                        if (j != 0) {
                            debug.append(", ");
                        }
                        debug.append(filterList.get(j));
                    }
                    LOG.debug(debug.toString());
                }

                if (filterMap.get(type) != null) {
                    throw new ConfigurationException("A filterList of type '"
                            + type + "' has already been defined");
                }
                filterMap.put(type, filterList);
            }
            put(FILTER_LIST, filterMap);
        }

        private String parseConfig(Configuration cfg, RendererConfigOption option) {
            Configuration child = cfg.getChild(option.getName());
            String value = child.getValue(null);
            if (value == null || "".equals(value)) {
                Object v = option.getDefaultValue();
                return v == null ? null : v.toString();
            }
            return value;
        }

        private boolean doesValueExist(Configuration cfg, RendererConfigOption option) {
            return cfg.getChild(option.getName(), false) != null;
        }

        private boolean getConfigValue(Configuration cfg, RendererConfigOption option, boolean defaultTo) {
            if (cfg.getChild(option.getName(), false) != null) {
                Configuration child = cfg.getChild(option.getName());
                try {
                    return child.getValueAsBoolean();
                } catch (ConfigurationException e) {
                    return defaultTo;
                }
            } else {
                return defaultTo;
            }
        }

        private int checkEncryptionLength(int encryptionLength, FOUserAgent userAgent) {
            int correctEncryptionLength = encryptionLength;
            if (encryptionLength < 40) {
                correctEncryptionLength = 40;
            } else if (encryptionLength > 256) {
                correctEncryptionLength = 256;
            } else if (encryptionLength > 128 && encryptionLength < 256) {
                correctEncryptionLength = 128;
            } else if (encryptionLength % 8 != 0) {
                correctEncryptionLength = Math.round(encryptionLength / 8.0f) * 8;
            }
            if (correctEncryptionLength != encryptionLength && userAgent != null) {
                PDFEventProducer.Provider.get(userAgent.getEventBroadcaster())
                .incorrectEncryptionLength(this, encryptionLength,
                        correctEncryptionLength);
            }
            return correctEncryptionLength;
        }
    }

}
