/*
 * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports, module) {
    "use strict";

    var FileSystemEntry = require("filesystem/FileSystemEntry");

    /*
     * Model for a file system Directory.
     *
     * This class should *not* be instantiated directly. Use FileSystem.getDirectoryForPath,
     * FileSystem.resolve, or Directory.getContents to create an instance of this class.
     *
     * Note: Directory.fullPath always has a trailing slash.
     *
     * See the FileSystem class for more details.
     *
     * @constructor
     * @param {!string} fullPath The full path for this Directory.
     * @param {!FileSystem} fileSystem The file system associated with this Directory.
     */
    function Directory(fullPath, fileSystem) {
        this._isDirectory = true;
        FileSystemEntry.call(this, fullPath, fileSystem);
    }

    Directory.prototype = Object.create(FileSystemEntry.prototype);
    Directory.prototype.constructor = Directory;
    Directory.prototype.parentClass = FileSystemEntry.prototype;

    /**
     * The contents of this directory. This "private" property is used by FileSystem.
     * @type {Array<FileSystemEntry>}
     */
    Directory.prototype._contents = null;

    /**
     * The stats for the contents of this directory, such that this._contentsStats[i]
     * corresponds to this._contents[i].
     * @type {Array.<FileSystemStats>}
     */
    Directory.prototype._contentsStats = null;

    /**
     * The stats errors for the contents of this directory.
     * @type {object.<string: string>} fullPaths are mapped to FileSystemError strings
     */
    Directory.prototype._contentsStatsErrors = null;

    /**
     * Clear any cached data for this directory. By default, we clear the contents
     * of immediate children as well, because in some cases file watchers fail
     * provide precise change notifications. (Sometimes, like after a "git
     * checkout", they just report that some directory has changed when in fact
     * many of the file within the directory have changed.
     *
     * @private
     * @param {boolean=} preserveImmediateChildren
     */
    Directory.prototype._clearCachedData = function (preserveImmediateChildren) {
        FileSystemEntry.prototype._clearCachedData.apply(this);

        if (!preserveImmediateChildren) {
            if (this._contents) {
                this._contents.forEach(function (child) {
                    child._clearCachedData(true);
                });
            } else {
                // No cached _contents, but child entries may still exist.
                // Scan the full index to catch all of them.
                var dirPath = this.fullPath;
                this._fileSystem._index.visitAll(function (entry) {
                    if (entry.parentPath === dirPath) {
                        entry._clearCachedData(true);
                    }
                });
            }
        }

        this._contents = undefined;
        this._contentsStats = undefined;
        this._contentsStatsErrors = undefined;
    };

    /**
     * Apply each callback in a list to the provided arguments. Callbacks
     * can throw without preventing other callbacks from being applied.
     *
     * @private
     * @param {Array.<function>} callbacks The callbacks to apply
     * @param {Array} args The arguments to which each callback is applied
     */
    function _applyAllCallbacks(callbacks, args) {
        if (callbacks.length > 0) {
            var callback = callbacks.pop();
            try {
                callback.apply(undefined, args);
            } finally {
                _applyAllCallbacks(callbacks, args);
            }
        }
    }

    /**
     * Read the contents of a Directory. If this Directory is under a watch root,
     * the listing will exclude any items filtered out by the watch root's filter
     * function.
     *
     * @param {Directory} directory Directory whose contents you want to get
     * @param {function (?string, Array.<FileSystemEntry>=, Array.<FileSystemStats>=, Object.<string, string>=)} callback
     *          Callback that is passed an error code or the stat-able contents
     *          of the directory along with the stats for these entries and a
     *          fullPath-to-FileSystemError string map of unstat-able entries
     *          and their stat errors. If there are no stat errors then the last
     *          parameter shall remain undefined.
     */
    Directory.prototype.getContents = function (callback) {
        if (this._contentsCallbacks) {
            // There is already a pending call for this directory's contents.
            // Push the new callback onto the stack and return.
            this._contentsCallbacks.push(callback);
            return;
        }

        // Return cached contents if the directory is watched
        if (this._contents) {
            callback(null, this._contents, this._contentsStats, this._contentsStatsErrors);
            return;
        }

        this._contentsCallbacks = [callback];

        this._impl.readdir(this.fullPath, function (err, names, stats) {
            var contents = [],
                contentsStats = [],
                contentsStatsErrors;

            if (err) {
                this._clearCachedData();
            } else {
                // Use the "relaxed" parameter to _isWatched because it's OK to
                // cache data even while watchers are still starting up
                var watched = this._isWatched(true);

                names.forEach(function (name, index) {
                    var entryPath = this.fullPath + name;

                    var entryStats = stats[index];
                    if (this._fileSystem._indexFilter(entryPath, name, entryStats)) {
                        var entry;

                        // Note: not all entries necessarily have associated stats.
                        if (typeof entryStats === "string") {
                            // entryStats is an error string
                            if (contentsStatsErrors === undefined) {
                                contentsStatsErrors = {};
                            }
                            contentsStatsErrors[entryPath] = entryStats;
                        } else {
                            // entryStats is a FileSystemStats object
                            if (entryStats.isFile) {
                                entry = this._fileSystem.getFileForPath(entryPath);
                            } else {
                                entry = this._fileSystem.getDirectoryForPath(entryPath);
                            }

                            if (watched) {
                                entry._stat = entryStats;
                            }

                            contents.push(entry);
                            contentsStats.push(entryStats);
                        }
                    }
                }, this);

                if (watched) {
                    this._contents = contents;
                    this._contentsStats = contentsStats;
                    this._contentsStatsErrors = contentsStatsErrors;
                }
            }

            // Reset the callback list before we begin calling back so that
            // synchronous reentrant calls are handled correctly.
            var currentCallbacks = this._contentsCallbacks;

            this._contentsCallbacks = null;

            // Invoke all saved callbacks
            var callbackArgs = [err, contents, contentsStats, contentsStatsErrors];
            _applyAllCallbacks(currentCallbacks, callbackArgs);
        }.bind(this));
    };

    /**
     * Create a directory
     *
     * @param {function (?string, FileSystemStats=)=} callback Callback resolved with a
     *      FileSystemError string or the stat object for the created directory.
     */
    Directory.prototype.create = function (callback) {
        callback = callback || function () {};

        // Block external change events until after the write has finished
        this._fileSystem._beginChange();

        this._impl.mkdir(this._path, function (err, stat) {
            if (err) {
                this._clearCachedData();
                try {
                    callback(err);
                    return;
                } finally {
                    // Unblock external change events
                    this._fileSystem._endChange();
                }
            }

            var parent = this._fileSystem.getDirectoryForPath(this.parentPath);

            // Update internal filesystem state
            if (this._isWatched()) {
                this._stat = stat;
            }

            this._fileSystem._handleDirectoryChange(parent, function (added, removed) {
                try {
                    callback(null, stat);
                } finally {
                    if (parent._isWatched()) {
                        this._fileSystem._fireChangeEvent(parent, added, removed);
                    }
                    // Unblock external change events
                    this._fileSystem._endChange();
                }
            }.bind(this));
        }.bind(this));
    };

    // Export this class
    module.exports = Directory;
});
