/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @constructor
 * @extends {WebInspector.SDKObject}
 * @param {!WebInspector.Target} target
 */
WebInspector.FileSystemModel = function(target)
{
    WebInspector.SDKObject.call(this, target);

    this._fileSystemsForOrigin = {};

    target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.SecurityOriginAdded, this._securityOriginAdded, this);
    target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.SecurityOriginRemoved, this._securityOriginRemoved, this);
    this._agent = target.fileSystemAgent();
    this._agent.enable();

    this._reset();
}

WebInspector.FileSystemModel.prototype = {
    _reset: function()
    {
        for (var securityOrigin in this._fileSystemsForOrigin)
            this._removeOrigin(securityOrigin);
        var securityOrigins = this.target().resourceTreeModel.securityOrigins();
        for (var i = 0; i < securityOrigins.length; ++i)
            this._addOrigin(securityOrigins[i]);
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _securityOriginAdded: function(event)
    {
        var securityOrigin = /** @type {string} */ (event.data);
        this._addOrigin(securityOrigin);
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _securityOriginRemoved: function(event)
    {
        var securityOrigin = /** @type {string} */ (event.data);
        this._removeOrigin(securityOrigin);
    },

    /**
     * @param {string} securityOrigin
     */
    _addOrigin: function(securityOrigin)
    {
        this._fileSystemsForOrigin[securityOrigin] = {};

        var types = ["persistent", "temporary"];
        for (var i = 0; i < types.length; ++i)
            this._requestFileSystemRoot(securityOrigin, types[i], this._fileSystemRootReceived.bind(this, securityOrigin, types[i], this._fileSystemsForOrigin[securityOrigin]));
    },

    /**
     * @param {string} securityOrigin
     */
    _removeOrigin: function(securityOrigin)
    {
        for (var type in this._fileSystemsForOrigin[securityOrigin]) {
            var fileSystem = this._fileSystemsForOrigin[securityOrigin][type];
            delete this._fileSystemsForOrigin[securityOrigin][type];
            this._fileSystemRemoved(fileSystem);
        }
        delete this._fileSystemsForOrigin[securityOrigin];
    },

    /**
     * @param {string} origin
     * @param {string} type
     * @param {function(number, !FileSystemAgent.Entry=)} callback
     */
    _requestFileSystemRoot: function(origin, type, callback)
    {
        /**
         * @param {?Protocol.Error} error
         * @param {number} errorCode
         * @param {!FileSystemAgent.Entry=} backendRootEntry
         */
        function innerCallback(error, errorCode, backendRootEntry)
        {
            if (error) {
                callback(FileError.SECURITY_ERR);
                return;
            }

            callback(errorCode, backendRootEntry);
        }

        this._agent.requestFileSystemRoot(origin, type, innerCallback);
    },

    /**
     * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
     */
    _fileSystemAdded: function(fileSystem)
    {
        this.dispatchEventToListeners(WebInspector.FileSystemModel.EventTypes.FileSystemAdded, fileSystem);
    },

    /**
     * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
     */
    _fileSystemRemoved: function(fileSystem)
    {
        this.dispatchEventToListeners(WebInspector.FileSystemModel.EventTypes.FileSystemRemoved, fileSystem);
    },

    refreshFileSystemList: function()
    {
        this._reset();
    },

    /**
     * @param {string} origin
     * @param {string} type
     * @param {!Object.<string, !WebInspector.FileSystemModel.FileSystem>} store
     * @param {number} errorCode
     * @param {!FileSystemAgent.Entry=} backendRootEntry
     */
    _fileSystemRootReceived: function(origin, type, store, errorCode, backendRootEntry)
    {
        if (!errorCode && backendRootEntry && this._fileSystemsForOrigin[origin] === store) {
            var fileSystem = new WebInspector.FileSystemModel.FileSystem(this, origin, type, backendRootEntry);
            store[type] = fileSystem;
            this._fileSystemAdded(fileSystem);
        }
    },

    /**
     * @param {!WebInspector.FileSystemModel.Directory} directory
     * @param {function(number, !Array.<!WebInspector.FileSystemModel.Entry>=)} callback
     */
    requestDirectoryContent: function(directory, callback)
    {
        this._requestDirectoryContent(directory.url, this._directoryContentReceived.bind(this, directory, callback));
    },

    /**
     * @param {string} url
     * @param {function(number, !Array.<!FileSystemAgent.Entry>=)} callback
     */
    _requestDirectoryContent: function(url, callback)
    {
        /**
         * @param {?Protocol.Error} error
         * @param {number} errorCode
         * @param {!Array.<!FileSystemAgent.Entry>=} backendEntries
         */
        function innerCallback(error, errorCode, backendEntries)
        {
            if (error) {
                callback(FileError.SECURITY_ERR);
                return;
            }

            if (errorCode !== 0) {
                callback(errorCode);
                return;
            }

            callback(errorCode, backendEntries);
        }

        this._agent.requestDirectoryContent(url, innerCallback);
    },

    /**
     * @param {!WebInspector.FileSystemModel.Directory} parentDirectory
     * @param {function(number, !Array.<!WebInspector.FileSystemModel.Entry>=)} callback
     * @param {number} errorCode
     * @param {!Array.<!FileSystemAgent.Entry>=} backendEntries
     */
    _directoryContentReceived: function(parentDirectory, callback, errorCode, backendEntries)
    {
        if (!backendEntries) {
            callback(errorCode);
            return;
        }

        var entries = [];
        for (var i = 0; i < backendEntries.length; ++i) {
            if (backendEntries[i].isDirectory)
                entries.push(new WebInspector.FileSystemModel.Directory(this, parentDirectory.fileSystem, backendEntries[i]));
            else
                entries.push(new WebInspector.FileSystemModel.File(this, parentDirectory.fileSystem, backendEntries[i]));
        }

        callback(errorCode, entries);
    },

    /**
     * @param {!WebInspector.FileSystemModel.Entry} entry
     * @param {function(number, !FileSystemAgent.Metadata=)} callback
     */
    requestMetadata: function(entry, callback)
    {
        /**
         * @param {?Protocol.Error} error
         * @param {number} errorCode
         * @param {!FileSystemAgent.Metadata=} metadata
         */
        function innerCallback(error, errorCode, metadata)
        {
            if (error) {
                callback(FileError.SECURITY_ERR);
                return;
            }

            callback(errorCode, metadata);
        }

        this._agent.requestMetadata(entry.url, innerCallback);
    },

    /**
     * @param {!WebInspector.FileSystemModel.File} file
     * @param {boolean} readAsText
     * @param {number=} start
     * @param {number=} end
     * @param {string=} charset
     * @param {function(number, string=, string=)=} callback
     */
    requestFileContent: function(file, readAsText, start, end, charset, callback)
    {
        this._requestFileContent(file.url, readAsText, start, end, charset, callback);
    },

    /**
     * @param {string} url
     * @param {boolean} readAsText
     * @param {number=} start
     * @param {number=} end
     * @param {string=} charset
     * @param {function(number, string=, string=)=} callback
     */
    _requestFileContent: function(url, readAsText, start, end, charset, callback)
    {
        /**
         * @param {?Protocol.Error} error
         * @param {number} errorCode
         * @param {string=} content
         * @param {string=} charset
         */
        function innerCallback(error, errorCode, content, charset)
        {
            if (error) {
                if (callback)
                    callback(FileError.SECURITY_ERR);
                return;
            }

            if (callback)
                callback(errorCode, content, charset);
        }

        this._agent.requestFileContent(url, readAsText, start, end, charset, innerCallback);
    },
    /**
     * @param {!WebInspector.FileSystemModel.Entry} entry
     * @param {function(number)=} callback
     */
    deleteEntry: function(entry, callback)
    {
        var fileSystemModel = this;
        if (entry === entry.fileSystem.root)
            this._deleteEntry(entry.url, hookFileSystemDeletion);
        else
            this._deleteEntry(entry.url, callback);

        function hookFileSystemDeletion(errorCode)
        {
            callback(errorCode);
            if (!errorCode)
                fileSystemModel._removeFileSystem(entry.fileSystem);
        }
    },

    /**
     * @param {string} url
     * @param {function(number)=} callback
     */
    _deleteEntry: function(url, callback)
    {
        /**
         * @param {?Protocol.Error} error
         * @param {number} errorCode
         */
        function innerCallback(error, errorCode)
        {
            if (error) {
                if (callback)
                    callback(FileError.SECURITY_ERR);
                return;
            }

            if (callback)
                callback(errorCode);
        }

        this._agent.deleteEntry(url, innerCallback);
    },

    /**
     * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
     */
    _removeFileSystem: function(fileSystem)
    {
        var origin = fileSystem.origin;
        var type = fileSystem.type;
        if (this._fileSystemsForOrigin[origin] && this._fileSystemsForOrigin[origin][type]) {
            delete this._fileSystemsForOrigin[origin][type];
            this._fileSystemRemoved(fileSystem);

            if (Object.isEmpty(this._fileSystemsForOrigin[origin]))
                delete this._fileSystemsForOrigin[origin];
        }
    },

    __proto__: WebInspector.SDKObject.prototype
}


WebInspector.FileSystemModel.EventTypes = {
    FileSystemAdded: "FileSystemAdded",
    FileSystemRemoved: "FileSystemRemoved"
}

/**
 * @constructor
 * @param {!WebInspector.FileSystemModel} fileSystemModel
 * @param {string} origin
 * @param {string} type
 * @param {!FileSystemAgent.Entry} backendRootEntry
 */
WebInspector.FileSystemModel.FileSystem = function(fileSystemModel, origin, type, backendRootEntry)
{
    this.origin = origin;
    this.type = type;

    this.root = new WebInspector.FileSystemModel.Directory(fileSystemModel, this, backendRootEntry);
}

WebInspector.FileSystemModel.FileSystem.prototype = {
    /**
     * @type {string}
     */
    get name()
    {
        return "filesystem:" + this.origin + "/" + this.type;
    }
}

/**
 * @constructor
 * @param {!WebInspector.FileSystemModel} fileSystemModel
 * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
 * @param {!FileSystemAgent.Entry} backendEntry
 */
WebInspector.FileSystemModel.Entry = function(fileSystemModel, fileSystem, backendEntry)
{
    this._fileSystemModel = fileSystemModel;
    this._fileSystem = fileSystem;

    this._url = backendEntry.url;
    this._name = backendEntry.name;
    this._isDirectory = backendEntry.isDirectory;
}

/**
 * @param {!WebInspector.FileSystemModel.Entry} x
 * @param {!WebInspector.FileSystemModel.Entry} y
 * @return {number}
 */
WebInspector.FileSystemModel.Entry.compare = function(x, y)
{
    if (x.isDirectory != y.isDirectory)
        return y.isDirectory ? 1 : -1;
    return x.name.compareTo(y.name);
}

WebInspector.FileSystemModel.Entry.prototype = {
    /**
     * @type {!WebInspector.FileSystemModel}
     */
    get fileSystemModel()
    {
        return this._fileSystemModel;
    },

    /**
     * @type {!WebInspector.FileSystemModel.FileSystem}
     */
    get fileSystem()
    {
        return this._fileSystem;
    },

    /**
     * @type {string}
     */
    get url()
    {
        return this._url;
    },

    /**
     * @type {string}
     */
    get name()
    {
        return this._name;
    },

    /**
     * @type {boolean}
     */
    get isDirectory()
    {
        return this._isDirectory;
    },

    /**
     * @param {function(number, !FileSystemAgent.Metadata)} callback
     */
    requestMetadata: function(callback)
    {
        this.fileSystemModel.requestMetadata(this, callback);
    },

    /**
     * @param {function(number)} callback
     */
    deleteEntry: function(callback)
    {
        this.fileSystemModel.deleteEntry(this, callback);
    }
}

/**
 * @constructor
 * @extends {WebInspector.FileSystemModel.Entry}
 * @param {!WebInspector.FileSystemModel} fileSystemModel
 * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
 * @param {!FileSystemAgent.Entry} backendEntry
 */
WebInspector.FileSystemModel.Directory = function(fileSystemModel, fileSystem, backendEntry)
{
    WebInspector.FileSystemModel.Entry.call(this, fileSystemModel, fileSystem, backendEntry);
}

WebInspector.FileSystemModel.Directory.prototype = {
    /**
     * @param {function(number, !Array.<!WebInspector.FileSystemModel.Directory>)} callback
     */
    requestDirectoryContent: function(callback)
    {
        this.fileSystemModel.requestDirectoryContent(this, callback);
    },

    __proto__: WebInspector.FileSystemModel.Entry.prototype
}

/**
 * @constructor
 * @extends {WebInspector.FileSystemModel.Entry}
 * @param {!WebInspector.FileSystemModel} fileSystemModel
 * @param {!WebInspector.FileSystemModel.FileSystem} fileSystem
 * @param {!FileSystemAgent.Entry} backendEntry
 */
WebInspector.FileSystemModel.File = function(fileSystemModel, fileSystem, backendEntry)
{
    WebInspector.FileSystemModel.Entry.call(this, fileSystemModel, fileSystem, backendEntry);

    this._mimeType = backendEntry.mimeType;
    this._resourceType = WebInspector.resourceTypes[backendEntry.resourceType];
    this._isTextFile = backendEntry.isTextFile;
}

WebInspector.FileSystemModel.File.prototype = {
    /**
     * @type {string}
     */
    get mimeType()
    {
        return this._mimeType;
    },

    /**
     * @type {!WebInspector.ResourceType}
     */
    get resourceType()
    {
        return this._resourceType;
    },

    /**
     * @type {boolean}
     */
    get isTextFile()
    {
        return this._isTextFile;
    },

    /**
     * @param {boolean} readAsText
     * @param {number=} start
     * @param {number=} end
     * @param {string=} charset
     * @param {function(number, string=)=} callback
     */
    requestFileContent: function(readAsText, start, end, charset, callback)
    {
        this.fileSystemModel.requestFileContent(this, readAsText, start, end, charset, callback);
    },

    __proto__: WebInspector.FileSystemModel.Entry.prototype
}
