/*
 * This file is part of ***  M y C o R e  ***
 * See http://www.mycore.de/ for details.
 *
 * MyCoRe is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MyCoRe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.mycore.datamodel.ifs2;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.mycore.common.content.MCRJDOMContent;
import org.mycore.common.content.MCRPathContent;
import org.mycore.datamodel.niofs.utils.MCRRecursiveDeleter;
import org.mycore.util.concurrent.MCRReadWriteGuard;

/**
 * Represents a set of files and directories belonging together, that are stored
 * in a persistent MCRFileStore. A FileCollection has a unique ID within the
 * store, it is the root folder of all files and directories in the collection.
 * 
 * @author Frank Lützenkirchen
 */
public class MCRFileCollection extends MCRDirectory {

    /**
     * The logger
     */
    private static final Logger LOGGER = LogManager.getLogger(MCRFileCollection.class);

    public static final String DATA_FILE = "mcrdata.xml";

    /**
     * The store this file collection is stored in.
     */
    private MCRStore store;

    /**
     * The ID of this file collection
     */
    private int id;

    /**
     * Guard for additional data
     *
     * MCR-1869
     */
    private MCRReadWriteGuard dataGuard;

    /**
     * Creates a new file collection in the given store, or retrieves an
     * existing one.
     *
     * @see MCRFileStore
     *
     * @param store
     *            the store this file collection is stored in
     * @param id
     *            the ID of this file collection
     */
    protected MCRFileCollection(MCRStore store, int id) throws IOException {
        super(null, store.getSlot(id), new Element("collection"));
        this.store = store;
        this.id = id;
        this.dataGuard = new MCRReadWriteGuard();
        if (Files.exists(path)) {
            readAdditionalData();
        } else {
            Files.createDirectories(path);
            writeData(Document::new);
            saveAdditionalData();
        }
    }

    MCRReadWriteGuard getDataGuard() {
        return dataGuard;
    }

    private void readAdditionalData() throws IOException {
        Path src = path.resolve(DATA_FILE);
        if (!Files.exists(src)) {
            LOGGER.warn("Metadata file is missing, repairing metadata...");
            writeData(e -> {
                e.detach();
                e.setName("collection");
                e.removeContent();
                new Document(e);
            });
            repairMetadata();
        }
        try {
            Element parsed = new MCRPathContent(src).asXML().getRootElement();
            writeData(e -> {
                e.detach();
                e.setName("collection");
                e.removeContent();
                List<Content> parsedContent = new ArrayList<>(parsed.getContent());
                parsedContent.forEach(Content::detach);
                e.addContent(parsedContent);
                new Document(e);
            });
        } catch (JDOMException e) {
            throw new IOException(e);
        }
    }

    protected void saveAdditionalData() throws IOException {
        Path target = path.resolve(DATA_FILE);
        try {
            readData(e -> {
                try {
                    boolean needsUpdate = true;
                    while (needsUpdate) {
                        try {
                            new MCRJDOMContent(e.getDocument()).sendTo(target, StandardCopyOption.REPLACE_EXISTING);
                            needsUpdate = false;
                        } catch (FileAlreadyExistsException ex) {
                            //other process writes to the same file, let us win
                        } catch (IOException | RuntimeException ex) {
                            throw ex;
                        }
                    }
                    return null;
                } catch (IOException e1) {
                    throw new UncheckedIOException(e1);
                }
            });
        } catch (UncheckedIOException e) {
            throw e.getCause();
        }
    }

    @Override
    public Stream<MCRNode> getChildren() throws IOException {
        return super.getChildren()
            .filter(f -> !DATA_FILE.equals(f.getName()));
    }

    /**
     * Deletes this file collection with all its data and children
     */
    public void delete() throws IOException {
        writeData(Element::removeContent);
        Files.walkFileTree(path, MCRRecursiveDeleter.instance());
        store.delete(id);
    }

    /**
     * Throws a exception, because a file collection's name is always the empty
     * string and therefore can not be renamed.
     */
    @Override
    public void renameTo(String name) {
        throw new UnsupportedOperationException("File collections can not be renamed");
    }

    /**
     * Returns the store this file collection is stored in.
     * 
     * @return the store this file collection is stored in.
     */
    public MCRStore getStore() {
        return store;
    }

    /**
     * Returns the ID of this file collection
     * 
     * @return the ID of this file collection
     */
    public int getID() {
        return id;
    }

    /**
     * Returns this object, because the FileCollection instance is the root of
     * all files and directories contained in the collection.
     * 
     * @return this
     */
    @Override
    public MCRFileCollection getRoot() {
        return this;
    }

    @Override
    public MCRNode getChild(String name) {
        if (DATA_FILE.equals(name)) {
            return null;
        } else {
            return super.getChild(name);
        }
    }

    @Override
    public boolean hasChildren() throws IOException {
        try (Stream<Path> stream = getUsableChildSream()) {
            return stream.findAny().isPresent();
        }
    }

    @Override
    public int getNumChildren() throws IOException {
        try (Stream<Path> stream = getUsableChildSream()) {
            return Math.toIntExact(stream.count());
        }
    }

    private Stream<Path> getUsableChildSream() throws IOException {
        return Files.list(path)
            .filter(p -> !DATA_FILE.equals(p.getFileName().toString()));
    }

    @Override
    public String getName() {
        return "";
    }

    /**
     * Repairs additional metadata stored for all files and directories in this
     * collection
     */
    @Override
    public void repairMetadata() throws IOException {
        super.repairMetadata();
        writeData(e -> {
            e.setName("collection");
            e.removeAttribute("name");
        });
        saveAdditionalData();
    }

    /**
     * Returns additional metadata stored for all files and directories in this
     * collection
     */
    Document getMetadata() {
        return new Document(readData(Element::clone));
    }

}
