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

package org.apache.jackrabbit.vault.fs.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;

import javax.jcr.NamespaceException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.jackrabbit.spi.commons.privilege.ParseException;
import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionReader;
import org.apache.jackrabbit.vault.fs.api.VaultFsConfig;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.spi.CNDReader;
import org.apache.jackrabbit.vault.fs.spi.NodeTypeSet;
import org.apache.jackrabbit.vault.fs.spi.PrivilegeDefinitions;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.impl.PackagePropertiesImpl;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.Text;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstracts the way of accessing the vault specific meta-info of a checkout.
 */
public class DefaultMetaInf implements MetaInf {

    private static Logger log = LoggerFactory.getLogger(DefaultMetaInf.class);

    private VaultSettings settings;

    private WorkspaceFilter filter;

    private VaultFsConfig config;

    private Properties properties;

    private Collection<NodeTypeSet> cnds = new LinkedList<>();

    private PrivilegeDefinitions privileges = new PrivilegeDefinitions();

    private boolean hasDefinition;

    /**
     * Returns the package format version of this package. If the package
     * lacks this information, {@link #FORMAT_VERSION_2} is returned, since this
     * feature was implemented recently.
     *
     * @return the package format version
     * @since 2.0
     */
    public int getPackageFormatVersion() {
        String prop = properties == null
                ? null
                : properties.getProperty(PACKAGE_FORMAT_VERSION);
        if (prop != null) {
            try {
                return Integer.parseInt(prop);
            } catch (Exception e) {
                // ignore
            }
        }
        return FORMAT_VERSION_2;
    }

    /**
     * Loads a setting based on the name of the system id.
     * <p>The specified stream remains open after this method returns.
     * 
     * @param systemId the system id of the setting to load
     * @param in the input stream
     * @return {@code true} if the setting was loaded.
     * @throws IOException if an I/O error occurrs
     * @throws ConfigurationException if a configuration error occurrs
     *
     * @since 3.1.32
     */
    public boolean load(@Nullable InputStream in, @NotNull String systemId) throws IOException, ConfigurationException {
        if (systemId.endsWith("/" + Constants.PACKAGE_DEFINITION_XML)) {
            setHasDefinition(true);
            log.trace("Contains package definition {}.", systemId);
            return true;
        }
        if (in == null) {
            return false;
        }

        final String name = Text.getName(systemId);
        if (Constants.FILTER_XML.equals(name)) {
            // load filter
            loadFilter(in, systemId);
            return true;
        } else if (Constants.CONFIG_XML.equals(name)) {
            // load config
            loadConfig(in, systemId);
            return true;
        } else if (Constants.SETTINGS_XML.equals(name)) {
            // load settings
            loadSettings(in, systemId);
            return true;
        } else if (Constants.PROPERTIES_XML.equals(name)) {
            // load properties
            loadProperties(in, systemId);
            return true;
        } else if (Constants.PRIVILEGES_XML.equals(name)) {
            // load privileges
            loadPrivileges(in, systemId);
            return true;
        } else if (name.endsWith(".cnd")) {
            Reader r = new InputStreamReader(in, "utf8");
            CNDReader reader = ServiceProviderFactory.getProvider().getCNDReader();
            reader.read(r, systemId, null);
            getNodeTypes().add(reader);
            log.trace("Loaded nodetypes from {}.", systemId);
            return true;
        }
        return false;
    }

    /**
     * 
     * <p>The specified stream remains open after this method returns.
     * 
     * @param in
     * @param systemId
     * @throws ConfigurationException
     * @throws IOException
     */
    public void loadFilter(@NotNull InputStream in, @NotNull String systemId)
            throws ConfigurationException, IOException {
        DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
        filter.load(in);
        setFilter(filter);
        log.trace("Loaded filter from {}.", systemId);
    }

    /**
     * <p>The specified stream remains open after this method returns.
     * @param in
     * @param systemId
     * @throws ConfigurationException
     * @throws IOException
     */
    public void loadConfig(@NotNull InputStream in, @NotNull String systemId)
            throws ConfigurationException, IOException {
        VaultFsConfig config = AbstractVaultFsConfig.load(in, systemId);
        setConfig(config);
        log.trace("Loaded config from {}.", systemId);
    }

    public void loadSettings(@NotNull InputStream in, @NotNull String systemId)
            throws ConfigurationException, IOException {
        VaultSettings settings = new VaultSettings();
        settings.load(in);
        setSettings(settings);
        log.trace("Loaded settings from {}.", systemId);
    }

    /**
     * <p>The specified stream remains open after this method returns.
     * @param in
     * @param systemId
     * @throws IOException
     */
    public void loadProperties(@NotNull InputStream in, @NotNull String systemId)
            throws IOException {
        Properties props = new Properties();
        // prevent the input stream from being closed for achieving a consistent behaviour
        props.loadFromXML(new CloseShieldInputStream(in));
        setProperties(props);
        log.trace("Loaded properties from {}.", systemId);
    }

    /**
     * <p>The specified stream remains open after this method returns.
     * @param in
     * @param systemId
     * @throws IOException
     */
    public void loadPrivileges(@NotNull InputStream in, @NotNull String systemId)
            throws IOException {
        try {
            PrivilegeDefinitionReader reader = new PrivilegeDefinitionReader(in, "text/xml");
            Collections.addAll(privileges.getDefinitions(), reader.getPrivilegeDefinitions());
            for (Map.Entry<String, String> e: reader.getNamespaces().entrySet()) {
                privileges.getNamespaceMapping().setMapping(e.getKey(), e.getValue());
            }
        } catch (ParseException | NamespaceException e) {
            log.error("Error while reading Privileges: {}", e.toString());
            throw new IOException("Error while reading privileges.", e);
        }
        log.trace("Loaded privileges from {}.", systemId);
    }

    public void save(@NotNull File metaDir) throws IOException {
        if (metaDir.isDirectory()) {
            saveConfig(metaDir);
            saveFilter(metaDir);
            saveSettings(metaDir);
            saveProperties(metaDir);
        } else {
            throw new IOException("meta directory does not exist or is non a directory: " + metaDir.getAbsolutePath());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public VaultSettings getSettings() {
        return settings;
    }

    public void setSettings(VaultSettings settings) {
        this.settings = settings;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WorkspaceFilter getFilter() {
        return filter;
    }

    public void setFilter(WorkspaceFilter filter) {
        this.filter = filter;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public VaultFsConfig getConfig() {
        return config;
    }

    public void setConfig(VaultFsConfig config) {
        this.config = config;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Properties getProperties() {
        return properties;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<NodeTypeSet> getNodeTypes() {
        return cnds;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public PrivilegeDefinitions getPrivileges() {
        return privileges;
    }

    public void setCNDs(Collection<NodeTypeSet> cnds) {
        this.cnds = cnds;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasDefinition() {
        return hasDefinition;
    }

    public void setHasDefinition(boolean hasDefinition) {
        this.hasDefinition = hasDefinition;
    }

    protected void loadSettings(@NotNull File metaDir)
            throws ConfigurationException, IOException {
        File file = new File(metaDir, Constants.SETTINGS_XML);
        if (file.isFile()) {
            VaultSettings settings = new VaultSettings();
            settings.load(file);
            this.settings = settings;
        } else {
            settings = VaultSettings.createDefault();
        }
    }

    protected void saveSettings(@NotNull File metaDir) throws IOException {
        if (settings != null) {
            File file = new File(metaDir, Constants.SETTINGS_XML);
            settings.save(file);
        }
    }

    protected void loadConfig(@NotNull File metaDir)
            throws ConfigurationException, IOException {
        File file = new File(metaDir, Constants.CONFIG_XML);
        if (file.isFile()) {
            this.config = AbstractVaultFsConfig.load(file);
        }
    }

    protected void saveConfig(@NotNull File metaDir)
            throws IOException {
        if (config != null) {
            File file = new File(metaDir, Constants.CONFIG_XML);
            try (OutputStream output = FileUtils.openOutputStream(file)) {
                IOUtils.copy(config.getSource(), output);
            }
        }
    }

    protected void loadFilter(@NotNull File metaDir, boolean vltMode)
            throws ConfigurationException, IOException {
        File file = new File(metaDir, Constants.FILTER_XML);
        if (vltMode) {
            File altFile = new File(metaDir, Constants.FILTER_VLT_XML);
            if (altFile.isFile()) {
                file = altFile;
                log.debug("Using alternative filter from {}", altFile.getPath());
            }
        }
        if (file.isFile()) {
            DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
            filter.load(file);
            this.filter = filter;
        }
    }

    protected void saveFilter(@NotNull File metaDir)
            throws IOException {
        if (filter != null) {
            File file = new File(metaDir, Constants.FILTER_XML);
            try (OutputStream output = FileUtils.openOutputStream(file)) {
                IOUtils.copy(filter.getSource(), output);
            }
        }
    }

    protected void loadProperties(@NotNull File metaDir) throws IOException {
        File file = new File(metaDir, Constants.PROPERTIES_XML);
        if (file.isFile()) {
            try (InputStream input = FileUtils.openInputStream(file)) {
                Properties properties = new Properties();
                properties.loadFromXML(input);
                this.properties = properties;
            }
        }
    }

    protected void saveProperties(@NotNull File metaDir) throws IOException {
        if (properties != null) {
            File file = new File(metaDir, Constants.PROPERTIES_XML);
            try (OutputStream output = FileUtils.openOutputStream(file)) {
                properties.storeToXML(output, "Custom Vault Properties", "utf-8");
            }
        }
    }

    protected void loadPrivileges(@NotNull File metaDir) throws IOException {
        File file = new File(metaDir, Constants.PRIVILEGES_XML);
        if (file.isFile()) {
            try (InputStream in = FileUtils.openInputStream(file)) {
                loadPrivileges(in, file.getPath());
            }
        }
    }

    protected void loadCNDs(@NotNull File metaDir) throws IOException {
        File[] files = metaDir.listFiles();
        if (files == null) {
            return;
        }
        for (File file: files) {
            if (file.getName().endsWith(".cnd")) {
                try(Reader r = new InputStreamReader(new FileInputStream(file), "utf8")) {
                    CNDReader reader = ServiceProviderFactory.getProvider().getCNDReader();
                    reader.read(r, file.getName(), null);
                    cnds.add(reader);
                } catch (IOException e) {
                    log.error("Error while reading CND: {}", e.toString());
                    throw new IOException("Error while reading CND.", e);
                }
            }
        }
    }

    @Override
    public PackageProperties getPackageProperties() {
        return new PackagePropertiesImpl() {
            
            @Override
            protected Properties getPropertiesMap() {
                return getProperties();
            }
        };
    }

}