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

import javax.jcr.RepositoryException;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;

import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.vault.fs.api.AccessType;
import org.apache.jackrabbit.vault.fs.api.Aggregate;
import org.apache.jackrabbit.vault.fs.api.Artifact;
import org.apache.jackrabbit.vault.fs.api.ArtifactType;
import org.apache.jackrabbit.vault.fs.api.DumpContext;
import org.apache.jackrabbit.vault.fs.api.VaultFile;
import org.apache.jackrabbit.vault.fs.api.VaultFileSystem;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implements the bridge between the repository based artifacts nodes and their
 * file system representation as a collection of artifacts.
 *
 */
public class VaultFileImpl implements VaultFile {

    /**
     * The default logger
     */
    protected static Logger log = LoggerFactory.getLogger(VaultFileImpl.class);

    /**
     * Vault filesystem of this file
     */
    private final VaultFileSystem fs;

    /**
     * the name of this entry (must be in platform format)
     */
    private final String name;

    /**
     * The artifact this represents or {@code null} if not attached
     */
    private Artifact artifact;

    /**
     * the jcr file node that this file belongs to
     */
    private VaultFileNode node;

    /**
     * the parent file
     */
    private VaultFileImpl parent;

    /**
     * a map of child nodes that weren't loaded yet
     */
    private LinkedHashMap<String, VaultFileNode> pendingChildNodes;

    /**
     * The map of child entries.
     */
    private LinkedHashMap<String, VaultFileImpl> children;

    /**
     * Internal constructor for the root file
     *
     * @param fs the file system
     * @param rootPath path of the root node
     * @param node the node
     * @throws RepositoryException if an error occurs
     */
    protected VaultFileImpl(VaultFileSystem fs, String rootPath, VaultFileNode node) throws RepositoryException {
        this.fs = fs;
        this.node = node;
        if (rootPath.equals("")) {
            this.name = rootPath;
            // bit of a hack since we know how the root artifacts look like
            for (Artifact a : node.getAggregate().getArtifacts().values()) {
                if (a.getType() == ArtifactType.DIRECTORY) {
                    this.artifact = a;
                    node.getFiles().add(this);
                } else {
                    VaultFileImpl child = new VaultFileImpl(fs, a.getPlatformPath(), node, a);
                    node.getFiles().add(child);
                    this.addChild(child);
                }
            }
        } else {
            this.name = rootPath;
            // special case when mounted deeply
            for (Artifact a : node.getAggregate().getArtifacts().values()) {
                if (a.getType() == ArtifactType.DIRECTORY) {
                    this.artifact = a;
                    node.getFiles().add(this);
                } else {
                    String p[] = Text.explode(a.getPlatformPath(), '/');
                    VaultFileImpl entry = null;
                    for (String cName : p) {
                        if (entry == null) {
                            entry = this;
                        } else {
                            entry = entry.getOrAddChild(cName);
                        }
                    }
                    if (entry != null && entry != this) {
                        entry.init(node, a);
                    }
                    // add to set of related files
                    node.getFiles().add(entry);
                }
            }
        }
        for (VaultFileNode child : node.getChildren()) {
            addPendingNode(child);
        }
    }

    /**
     * Internal constructor
     *
     * @param fs the file system
     * @param name the file entry name
     * @param node the node
     * @param artifact the underlying artifact. can be {@code null}
     * @throws RepositoryException if an error occurs
     */
    protected VaultFileImpl(VaultFileSystem fs, String name, VaultFileNode node, Artifact artifact)
            throws RepositoryException {
        this.fs = fs;
        this.name = name;
        init(node, artifact);
    }

    /**
     * (re)initializes this file
     * @param node the artifacts node
     * @param a the artifact
     * @throws RepositoryException if an error occurs
     */
    protected void init(VaultFileNode node, Artifact a) throws RepositoryException {
        children = null;
        pendingChildNodes = null;
        this.node = node;
        this.artifact = a;
        if (node != null && a != null && a.getType() == ArtifactType.DIRECTORY) {
            for (VaultFileNode child : node.getChildren()) {
                addPendingNode(child);
            }
        }
    }

    protected void attach(VaultFileNode node, Artifact a) {
        this.node = node;
        this.artifact = a;
    }

    public String getPath() {
        if (parent == null) {
            return name.length() == 0 ? "/" : name;
        } else {
            return internalGetPath().toString();
        }
    }

    public String getRepoRelPath() {
        if (artifact == null) {
            return null;
        } else {
            String relPath = artifact.getRelativePath();
            int idx = relPath.indexOf('/');
            if (idx > 0 && idx < relPath.length() - 1) {
                return relPath.substring(idx + 1);
            }
            return "";
        }
    }

    public String getAggregatePath() {
        return node == null ? parent.getAggregatePath() : node.getPath();
    }

    /**
     * Internal builder for the path
     * @return the string buffer.
     */
    private StringBuffer internalGetPath() {
        if (parent == null) {
            return new StringBuffer(name);
        } else {
            return parent.internalGetPath().append('/').append(name);
        }
    }

    public String getName() {
        return name;
    }

    public Artifact getArtifact() {
        return artifact;
    }

    public boolean isDirectory() {
        return artifact == null || artifact.getType() == ArtifactType.DIRECTORY;
    }

    public boolean isTransient() {
        return node == null;
    }

    public VaultFileImpl getParent() throws IOException, RepositoryException {
        return parent;
    }

    public Aggregate getAggregate() {
        return node == null ? null : node.getAggregate();
    }

    public Aggregate getControllingAggregate() {
        if (node == null && parent != null) {
            return parent.getControllingAggregate();
        } else {
            return node == null ? null : node.getAggregate();
        }
    }

    public VaultFileImpl getChild(String name) throws RepositoryException {
        VaultFileImpl child = children == null ? null : children.get(name);
        if (child == null) {
            // try to load child
            String repoName = PlatformNameFormat.getRepositoryName(name);
            loadPendingNode(repoName);
            child = children == null ? null : children.get(name);
            // if still not present, load all
            if (child == null) {
                loadChildren();
            }
            child = children == null ? null : children.get(name);
        }
        return child;
    }

    public Collection<? extends VaultFile> getChildren() throws RepositoryException {
        loadChildren();
        return children.values();
    }

    /**
     * Internally loads all children out of the map of pending node.
     * @throws RepositoryException if an error occurs
     */
    private void loadChildren() throws RepositoryException {
        if (children == null) {
            children = new LinkedHashMap<String, VaultFileImpl>();
        }
        if (pendingChildNodes != null) {
            Iterator<String> iter = pendingChildNodes.keySet().iterator();
            while (iter.hasNext()) {
                loadPendingNode(iter.next());
                iter = pendingChildNodes.keySet().iterator();
            }
        }
    }

    /**
     * Adds a node to the map of pending ones
     * @param n the node
     * @throws RepositoryException if an error occurs
     */
    protected void addPendingNode(VaultFileNode n) throws RepositoryException {
        // we would need to assert that (n.getParent() == this.node) but this
        // is not possible since this.node might not be loaded yet
        if (pendingChildNodes == null) {
            pendingChildNodes = new LinkedHashMap<String, VaultFileNode>();
        }
        String name = n.getName();
        pendingChildNodes.put(name, n);
        // if pending node is deep, we need to load it. otherwise it might not
        // be found afterwards
        if (name.indexOf('/') > 0) {
            loadPendingNode(name);
        }
    }

    /**
     * Loads a pending node
     * @param repoName the repository name of the node
     * @throws RepositoryException if an error occurs
     */
    private void loadPendingNode(String repoName) throws RepositoryException {
        // get node from pending ones
        VaultFileNode n = pendingChildNodes == null ? null : pendingChildNodes.remove(repoName);
        if (n == null) {
            return;
        }
        VaultFileImpl parent = this;
        // if pending node has a deep aggregate, we create intermediate
        // "directories"
        String aggName = n.getAggregate().getRelPath();
        if (aggName.indexOf('/') > 0) {
            String[] p = Text.explode(aggName, '/');
            for (int i = 0; i < p.length - 1; i++) {
                parent = parent.getOrAddChild(PlatformNameFormat.getPlatformName(p[i]));
            }
        }

        for (Artifact a : n.getAggregate().getArtifacts().values()) {
            String p[] = Text.explode(a.getPlatformPath(), '/');
            VaultFileImpl entry = parent;
            for (String cName : p) {
                entry = entry.getOrAddChild(cName);
            }
            entry.init(n, a);
            // add to set of related files
            n.getFiles().add(entry);
        }
    }

    /**
     * Attaches a child file
     * @param child the child
     */
    private void addChild(VaultFileImpl child) {
        child.parent = this;
        if (children == null) {
            children = new LinkedHashMap<String, VaultFileImpl>();
        }
        children.put(child.name, child);
    }

    /**
     * Returns the child of the given name or creates and adds a new one.
     * @param name the name of the file
     * @return the child
     * @throws RepositoryException if an error occurs
     */
    protected VaultFileImpl getOrAddChild(String name) throws RepositoryException {
        VaultFileImpl c = children == null ? null : children.get(name);
        if (c == null) {
            c = new VaultFileImpl(fs, name, null, null);
            addChild(c);
        }
        return c;
    }

    public Collection<? extends VaultFile> getRelated() throws RepositoryException {
        if (node == null) {
            return null;
        }
        return node.getFiles();
    }

    public boolean canRead() {
        return artifact != null && artifact.getPreferredAccess() != AccessType.NONE;
    }

    public long lastModified() {
        return artifact == null ? 0 : artifact.getLastModified();
    }

    public long length() {
        return artifact == null ? -1 : artifact.getContentLength();
    }

    public String getContentType() {
        return artifact == null ? null : artifact.getContentType();
    }

    public VaultFileSystem getFileSystem() {
        return fs;
    }

    public void invalidate() throws RepositoryException {
        log.info("invalidating file {}", getPath());

        // special handling for root node
        if (parent == null) {
            // bit of a hack since we know how the root artifacts look like
            node.invalidate();
            for (Artifact a : node.getAggregate().getArtifacts().values()) {
                if (a.getType() == ArtifactType.DIRECTORY) {
                    this.artifact = a;
                    node.getFiles().add(this);
                } else {
                    VaultFileImpl child = new VaultFileImpl(fs, a.getPlatformPath(), node, a);
                    node.getFiles().add(child);
                    this.addChild(child);
                }
            }
            for (VaultFileNode child : node.getChildren()) {
                addPendingNode(child);
            }
        } else {
            // get the directory artifact of this file
            for (VaultFileImpl f : node.getFiles()) {
                if (f.parent != null && f.parent.artifact.getType() == ArtifactType.DIRECTORY) {
                    f.parent.node.invalidate();
                    f.parent.init(f.parent.node, f.parent.artifact);
                    break;
                }
            }
            // node.invalidate();
            // init(node, artifact);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void dump(DumpContext ctx, boolean isLast) {
        ctx.println(isLast, "Vault file");
        ctx.indent(isLast);
        ctx.printf(false, "name: %s", name);
        ctx.printf(false, "path: %s", getPath());
        ctx.printf(false, "# pending: %d", pendingChildNodes == null ? -1 : pendingChildNodes.size());
        ctx.printf(false, "# children: %d", children == null ? -1 : children.size());
        if (artifact != null) {
            artifact.dump(ctx, false);
        } else {
            ctx.println(false, "Artifact: (null)");
        }
        if (node != null) {
            node.dump(ctx, true);
        } else {
            ctx.println(true, "ArtifactsNode: (null)");
        }
        ctx.outdent();
    }
}
