/*
 * 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.Object}
 */
WebInspector.FileSystemModel = function()
{
    WebInspector.Object.call(this);

    this._originForFrameId = {};
    this._frameIdsForOrigin = {};
    this._fileSystemsForOrigin = {};

    WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameAdded, this._frameAdded, this);
    WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameNavigated, this._frameNavigated, this);
    WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameDetached, this._frameDetached, this);

    FileSystemAgent.enable();

    if (WebInspector.resourceTreeModel.mainFrame)
        this._attachFrameRecursively(WebInspector.resourceTreeModel.mainFrame);
}

WebInspector.FileSystemModel.prototype = {
    /**
     * @param {WebInspector.Event} event
     */
    _frameAdded: function(event)
    {
        var frame = /** @type {WebInspector.ResourceTreeFrame} */ (event.data);
        this._attachFrameRecursively(frame);
    },

    /**
     * @param {WebInspector.Event} event
     */
    _frameNavigated: function(event)
    {
        var frame = /** @type {WebInspector.ResourceTreeFrame} */ (event.data);
        this._attachFrameRecursively(frame);
    },

    /**
     * @param {WebInspector.Event} event
     */
    _frameDetached: function(event)
    {
        var frame = /** @type {WebInspector.ResourceTreeFrame} */ (event.data);
        this._detachFrameRecursively(frame);
    },

    /**
     * @param {WebInspector.ResourceTreeFrame} frame
     */
    _attachFrame: function(frame)
    {
        if (this._originForFrameId[frame.id])
            this._detachFrameRecursively(frame);

        if (frame.securityOrigin === "null")
            return;

        this._originForFrameId[frame.id] = frame.securityOrigin;

        var newOrigin = false;
        if (!this._frameIdsForOrigin[frame.securityOrigin]) {
            this._frameIdsForOrigin[frame.securityOrigin] = {};
            newOrigin = true;
        }
        this._frameIdsForOrigin[frame.securityOrigin][frame.id] = frame.id;
        if (newOrigin)
            this._originAdded(frame.securityOrigin);
    },

    /**
     * @param {WebInspector.ResourceTreeFrame} frame
     */
    _attachFrameRecursively: function(frame)
    {
        this._attachFrame(frame);
        for (var i = 0; i < frame.childFrames.length; ++i)
            this._attachFrameRecursively(frame.childFrames[i]);
    },

    /**
     * @param {WebInspector.ResourceTreeFrame} frame
     */
    _detachFrame: function(frame)
    {
        if (!this._originForFrameId[frame.id])
            return;
        var origin = this._originForFrameId[frame.id];
        delete this._originForFrameId[frame.id];
        delete this._frameIdsForOrigin[origin][frame.id];

        var lastOrigin = Object.isEmpty(this._frameIdsForOrigin[origin]);
        if (lastOrigin) {
            delete this._frameIdsForOrigin[origin];
            this._originRemoved(origin);
        }
    },

    /**
     * @param {WebInspector.ResourceTreeFrame} frame
     */
    _detachFrameRecursively: function(frame)
    {
        for (var i = 0; i < frame.childFrames.length; ++i)
            this._detachFrameRecursively(frame.childFrames[i]);
        this._detachFrame(frame);
    },

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

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

    /**
     * @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);
        }

        FileSystemAgent.requestFileSystemRoot(origin, type, innerCallback.bind(this));
    },

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

    /**
     * @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()
    {
        if (WebInspector.resourceTreeModel.mainFrame) {
            this._detachFrameRecursively(WebInspector.resourceTreeModel.mainFrame);
            this._attachFrameRecursively(WebInspector.resourceTreeModel.mainFrame);
        }
    },

    /**
     * @param {string} origin
     * @param {string} type
     * @param {Object.<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, null);
                return;
            }

            callback(errorCode, backendEntries);
        }

        FileSystemAgent.requestDirectoryContent(url, innerCallback.bind(this));
    },

    /**
     * @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)
    {
        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);
        }

        FileSystemAgent.requestMetadata(entry.url, innerCallback.bind(this));
    },

    /**
     * @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);
        }

        FileSystemAgent.requestFileContent(url, readAsText, start, end, charset, innerCallback.bind(this));
    },
    /**
     * @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);
        }

        FileSystemAgent.deleteEntry(url, innerCallback.bind(this));
    },

    /**
     * @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.Object.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
}
