/*
 * 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.sling.feature.cpconverter.handlers.slinginitialcontent;

import static org.apache.sling.feature.cpconverter.shared.ConverterConstants.SLASH;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.time.ZoneOffset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;

import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.value.ValueFactoryImpl;
import org.apache.jackrabbit.vault.fs.io.DocViewFormat;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.DocViewProperty2;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.jackrabbit.vault.util.xml.serialize.FormattingXmlStreamWriter;
import org.apache.sling.feature.cpconverter.vltpkg.JcrNamespaceRegistry;
import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
import org.apache.sling.jcr.contentloader.ContentCreator;
import org.jetbrains.annotations.NotNull;

/**
 * ContentCreator implementation to write FileVault enhanced DocView XML files (to be packaged into a VaultPackage)
 */
public class VaultContentXMLContentCreator implements ContentCreator {

    private static final String ACL_NOT_SUPPORTED_MSG = "Sling Initial Content - ACL statements are not supported yet . SLING issue: https://issues.apache.org/jira/browse/SLING-11060";

    private final VaultPackageAssembler packageAssembler;
    private final Queue<DocViewTreeNode> currentNodeStack = Collections.asLifoQueue(new ArrayDeque<>());
    private final JcrNamespaceRegistry namespaceRegistry;
    private final NamePathResolver npResolver;
    private final XMLStreamWriter writer;
    private String rootPath;
    private final boolean isFileDescriptorEntry;
    private boolean isFinished = false;

    private DocViewTreeNode rootNode = null;

    VaultContentXMLContentCreator(@NotNull String repositoryPath,
                                  @NotNull OutputStream targetOutputStream,
                                  @NotNull JcrNamespaceRegistry namespaceRegistry,
                                  @NotNull VaultPackageAssembler packageAssembler,
                                  boolean isFileDescriptorEntry) throws RepositoryException, FactoryConfigurationError {
        this.packageAssembler = packageAssembler;
        this.namespaceRegistry = namespaceRegistry;
        this.isFileDescriptorEntry = isFileDescriptorEntry;
        this.npResolver = new DefaultNamePathResolver((NamespaceRegistry)namespaceRegistry);
        try {
            writer = FormattingXmlStreamWriter.create(targetOutputStream, new DocViewFormat().getXmlOutputFormat());
        } catch (XMLStreamException e) {
            throw new RepositoryException("Cannot create XML Writer " + e, e);
        }
        rootPath = repositoryPath;
    }

    /**
     * The absolute entry path inside the content package ZIP for the generated docview.xml
     * @throws NamespaceException 
     */
    public String getContentPackageEntryPath() throws NamespaceException {
        String suffix;
        if (isFileDescriptorEntry) {
            // it is potentially https://jackrabbit.apache.org/filevault/vaultfs.html#extended-file-aggregates (and may have file name clashes otherwise with the binary file in the content package)
            suffix = ".dir" + "/" + Constants.DOT_CONTENT_XML;
        } else {
            suffix =  "/" + Constants.DOT_CONTENT_XML;
        }
        return SLASH + org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(rootNode.getPath(npResolver)) + suffix;
    }

    @Override
    public void createNode(String name, String primaryNodeType, String[] mixinNodeTypes) throws RepositoryException {
        final Name currentNodeName;
        if (rootNode == null) {
            currentNodeName = NameConstants.JCR_ROOT;
            if (StringUtils.isNotBlank(name)) {
                // adjust root path in case the first node has an explicit name
                rootPath = Text.getRelativeParent(rootPath, 1) + "/" + name;
            }
        } else {
            currentNodeName = npResolver.getQName(name);
        }

        // if we are dealing with a descriptor file, we should use nt:file as default primaryType. 
        String defaultNtType = isFileDescriptorEntry ? JcrConstants.NT_FILE : JcrConstants.NT_UNSTRUCTURED;
        String toUsePrimaryNodeType = StringUtils.isNotBlank(primaryNodeType) ? primaryNodeType : defaultNtType;
        List<DocViewProperty2> currentProperties = new ArrayList<>();
        currentProperties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, toUsePrimaryNodeType));
        if (ArrayUtils.isNotEmpty(mixinNodeTypes)) {
            currentProperties.add(new DocViewProperty2(NameConstants.JCR_MIXINTYPES, Arrays.asList(mixinNodeTypes)));
        }
        final DocViewTreeNode newNode;
        if (rootNode == null) {
            newNode = new DocViewTreeNode(rootPath, currentNodeName, currentProperties);
            rootNode = newNode;
        } else {
            newNode = new DocViewTreeNode(rootNode.getPath(npResolver), currentNodeName, currentProperties);
            currentNodeStack.element().addChild(newNode);
        }
        currentNodeStack.add(newNode);
    }

    @Override
    public void finishNode() throws RepositoryException {
        this.currentNodeStack.remove();
    }

    @Override
    public void finish() throws RepositoryException {
        if (isFinished) {
            return;
        }
        isFinished = true;
        try {
            rootNode.write(writer, namespaceRegistry, Arrays.asList(namespaceRegistry.getPrefixes()));
            writer.close();
        } catch (XMLStreamException e) {
            throw new RepositoryException("Cannot close XML writer " + e, e);
        }
    }

    @Override
    public void createProperty(String name, int propertyType, String value) throws RepositoryException {
        // add explicit type due to https://issues.apache.org/jira/browse/JCRVLT-693
        currentNodeStack.peek().getProperties().add(new DocViewProperty2(npResolver.getQName(name), value, propertyType == PropertyType.UNDEFINED ? PropertyType.STRING : propertyType));
    }

    @Override
    public void createProperty(String name, int propertyType, String[] values) throws RepositoryException {
        // add explicit type due to https://issues.apache.org/jira/browse/JCRVLT-693
        currentNodeStack.peek().getProperties().add(new DocViewProperty2(npResolver.getQName(name), Arrays.asList(values), propertyType == PropertyType.UNDEFINED ? PropertyType.STRING : propertyType));
    }

    @Override
    public void createProperty(String name, Object value) throws RepositoryException {
        // store binaries outside of docview xml
        Value jcrValue = createValue(name, value, -1);
        DocViewProperty2 property = DocViewProperty2.fromValues(npResolver.getQName(name), new Value[] { jcrValue }, jcrValue.getType(), false, false, false);
        currentNodeStack.peek().getProperties().add(property);
    }

    @Override
    public void createProperty(String name, Object[] values) throws RepositoryException {
        try {
            AtomicInteger index = new AtomicInteger();
            Value[] jcrValues = Arrays.stream(values).map(v -> {
                try {
                    return createValue(name,v, index.getAndIncrement());
                } catch (RepositoryException e) {
                    throw new UncheckedRepositoryException(e);
                }
            }).toArray(Value[]::new);
            final int type;
            if (jcrValues.length == 0) {
                type = PropertyType.STRING;
            } else {
                type = jcrValues[0].getType();
            }
            DocViewProperty2 property = DocViewProperty2.fromValues(npResolver.getQName(name), jcrValues, type, true, false, false);
            currentNodeStack.peek().getProperties().add(property);
        } catch (UncheckedRepositoryException e) {
            throw e.getCause();
        }
    }

    static final class UncheckedRepositoryException extends RuntimeException {

        private static final long serialVersionUID = 1L;

        public UncheckedRepositoryException(RepositoryException e) {
            super(e);
        }

        @Override
        public synchronized RepositoryException getCause() {
            return (RepositoryException) super.getCause();
        }
    }

    private Value createValue(String name, Object value, int index) throws RepositoryException {
        ValueFactory valueFactory = ValueFactoryImpl.getInstance();
        final Value jcrValue;
        if (value instanceof String) {
            jcrValue = valueFactory.createValue((String)value);
        } else if (value instanceof Long) {
            jcrValue = valueFactory.createValue((long)value);
        } else if (value instanceof Double) {
            jcrValue = valueFactory.createValue((Double)value);
        } else if (value instanceof BigDecimal) {
            jcrValue = valueFactory.createValue((BigDecimal)value);
        } else if (value instanceof Date) {
            Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC), Locale.ROOT);
            calendar.setTime((Date)value);
            jcrValue = valueFactory.createValue(calendar);
        } else if (value instanceof Calendar) {
            jcrValue = valueFactory.createValue((Calendar)value);
        } else if (value instanceof Boolean) {
            jcrValue = valueFactory.createValue((Boolean)value);
        } else if (value instanceof InputStream) {
            // binaries are always stored outside the docview xml (https://jackrabbit.apache.org/filevault/vaultfs.html#Binary_Properties)
            String binaryPropertyEntryName = PlatformNameFormat.getPlatformName(name) + ((index != -1) ? "[" + index + "]" : "") + ".binary";
            createBinary((InputStream)value, SLASH + binaryPropertyEntryName);
            jcrValue = valueFactory.createValue("", PropertyType.BINARY);
        } else {
            throw new UnsupportedOperationException("Unsupported value type " + value.getClass());
        }
        return jcrValue;
    }

    private void createBinary(InputStream value, String suffix) throws RepositoryException {
        // this is called inside the node of the child
        String path = org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(currentNodeStack.peek().getPath(npResolver)) + suffix;
        try {
            // write binary directly (not during finish)
            packageAssembler.addEntry(path, value);
        } catch (IOException e) {
            throw new RepositoryException(e);
        }
    }

    @Override
    public void createFileAndResourceNode(String name, InputStream data, String mimeType, long lastModified) throws RepositoryException {
        this.createNode(name, JcrConstants.NT_FILE, null);
        createBinary(data, ""); // binary must be created with the name of the nt:file root node
        this.createNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE, null);

        final Date date;
        // ensure sensible last modification date
        if (lastModified >= 0) {
            date = new Date(lastModified);
        } else {
            date = new Date();
        }
        this.createProperty(JcrConstants.JCR_MIMETYPE, mimeType);
        this.createProperty(JcrConstants.JCR_LASTMODIFIED, date);
        // the data property does not need to be added to the enhanced docview as already derived from the binary file
    }


    @Override
    public boolean switchCurrentNode(String subPath, String newNodeType) {
        throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
    }

    @Override
    public void createUser(String name, String password, Map<String, Object> extraProperties) {
        throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
    }

    @Override
    public void createGroup(String name, String[] members, Map<String, Object> extraProperties) {
        throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
    }

    @Override
    public void createAce(String principal, String[] grantedPrivileges, String[] deniedPrivileges, String order) {
        throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
    }

    @Override
    public void createAce(String principalId, String[] grantedPrivilegeNames, String[] deniedPrivilegeNames,
                          String order, Map<String, Value> restrictions, Map<String, Value[]> mvRestrictions,
                          Set<String> removedRestrictionNames) {
        throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
    }

}
