/**
 * dfs-master-index.js
 * Copyright (c) 2000 - 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * DongHee Yang <donghee.yang@samsung.com>
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jiil Hyoun <jiil.hyoun@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed 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.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

var fs = require('fs');
var extfs = require('fs-extra');
var path = require('path');
var async = require('async');

var dibs = require('../../core/dibs.js');
var DFSLock = require('./dfs-lock.js');
var Utils = require('../../lib/utils');
var DError = require('../../core/exception.js');
var DFSLock = require('./dfs-lock.js');

/**
 * @module core/dfs/dfs-master-index
 */

/**
 * @constructor
 * @memberOf module:core/dfs/dfs-master-index
 */
function DFSMasterIndex(p) {
    var self = this;
    var parent = p;

    var masterIndex = {};
    var masterLockMgr = new DFSLock();
    var dfsLog = parent.getLogger();

    this.getRawIndex = function () {
        return masterIndex;
    };

    /**
	 * Register new index to master
     * @method registerToMasterIndex
     * @param {string} sid - server id
     * @param {string} srcIndex - srcIndex
     * @return {boolean}
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.registerServer = function (sid, srcIndex) {
        dfsLog.info('Registering DFS index of server \'' + sid + '\' to master index...');

        // check integrity of index
        for (var dfsPath in srcIndex) {
            var masterInfo = masterIndex[dfsPath];
            if (masterInfo === undefined || masterInfo === null) {
                continue;
            }

            var info = srcIndex[dfsPath];
            if (info.size !== masterInfo.size ||
                info.checksum !== masterInfo.checksum) {
                dfsLog.warn('Checking integrity of DFS index failed!');
                return false;
            }
        }

        // merge index
        for (var dfsPath in srcIndex) {
            var masterInfo = masterIndex[dfsPath];
            if (masterInfo === undefined || masterInfo === null) {
                masterIndex[dfsPath] = {
                    servers: [sid],
                    size: srcIndex[dfsPath].size,
                    checksum: srcIndex[dfsPath].checksum
                };
            } else {
                masterIndex[dfsPath].servers.push(sid);
            }
        }

        dfsLog.info('Registering DFS index succeeded!');

        return true;
    };


    /**
	 * Unregister server from master index
     * @method registerToMasterIndex
     * @param {string} sid - server id
     * @return {boolean}
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.unregisterServer = function (sid, callback) {
        dfsLog.info('Unregistering \'' + sid + '\' from master index...');

        // get rpaths related server
        var rPaths = Object.keys(masterIndex).filter(function (dfsPath) {
            return (masterIndex[dfsPath].servers.indexOf(sid) >= 0);
        });

        dfsLog.info('Unregistering DFS paths: ' + JSON.stringify(rPaths));

        async.eachLimit(rPaths, 10,
            function (dfsPath, cb) {
                var lock = null;
                async.waterfall([
                    function (cb1) {
                        self.aquireFileLock(sid, dfsPath, {
                            exclusive: true
                        }, cb1);
                    },
                    function (l, cb1) {
                        lock = l;
                        removeFileInternal(sid, dfsPath);
                        cb1(null);
                    }], function (err) {
                    if (lock) {
                        self.releaseFileLock(lock, function (err1) {
                            dfsLog.info('Unregistering \'' + sid + '\' succeeded!');
                            cb(err);
                        });
                    } else {
                        dfsLog.info('Unregistering \'' + sid + '\' succeeded!');
                        cb(err);
                    }
                });
            },
            function (err) {
                callback(err);
            });
    };


    /**
     * @method existsFileMasterIndex
     * @param {string} rPath
     * @returns {boolean}
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.existsFile = function (dfsPath) {
        return (masterIndex[dfsPath] !== undefined && masterIndex[dfsPath] !== null);
    };


    /**
     * @method registerFileIndex
     * @param {string} sid - server id
     * @param {string} dfsPath
     * @param {module:core/dfs/dfs-server.header} fInfo - fInfo
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.registerFile = function (sid, dfsPath, fInfo, callback) {
        dfsLog.info('Registering single file to master index... ' + sid + ':' + dfsPath);

        var masterInfo = masterIndex[dfsPath];

        // if exists, append it, otherwise register it
        if (!masterInfo) {
            masterIndex[dfsPath] = {
                servers: [sid],
                size: fInfo.size,
                checksum: fInfo.checksum
            };
            dfsLog.info('Registered new DFS file index... ' + dfsPath);
            callback(null);
        } else {
            // the condition of 'size < 0 or checksum == null' means it's not initialized
            if ((masterInfo.size >= 0 && fInfo.checksum !== null) &&
                (fInfo.size !== masterInfo.size || fInfo.checksum !== masterInfo.checksum)) {
                dfsLog.warn('Checking integrity of DFS file index failed!');
                callback(new DError('DFS010'));
                return;
            }

            // update info
            masterInfo.size = fInfo.size;
            masterInfo.checksum = fInfo.checksum;

            if (masterIndex[dfsPath].servers.indexOf(sid) === -1) {
                masterIndex[dfsPath].servers.push(sid);
                dfsLog.info('Appended new server for DFS file index... ' + dfsPath);
            } else {
                dfsLog.warn('Tried to register existing DFS file index... ' + dfsPath);
            }
            callback(null);
        }
    };


    /**
     * @method getFileIndex
     * @param {string} dfsPath
     * @param {module:core/dfs/dist-fs.callback_err_info} callback
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.getFile = function (dfsPath, callback) {
        var masterInfo = masterIndex[dfsPath];
        if (masterInfo !== undefined && masterInfo !== null) {
            callback(null, masterInfo);
        } else {
            callback(new DError('DFS009', {
                path: dfsPath
            }), null);
        }
    };


    /**
     * @method removeFileIndex
     * @param {string} sid - server id
     * @param {string} dfsPath
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.removeFile = function (sid, dfsPath, callback) {
        if (!masterIndex[dfsPath]) {
            callback(new DError('DFS009', {
                path: dfsPath
            })); return;
        }

        removeFileInternal(sid, dfsPath);

        callback(null);
    };


    this.searchFiles = function (opts, callback) {
        dfsLog.info('Searching files on master index...');

        var result = [];
        for (var dfsPath in masterIndex) {
            var masterInfo = masterIndex[dfsPath];
            if (opts.size !== undefined && masterInfo.size !== opts.size) {
                continue;
            }
            if (opts.checksum !== undefined && masterInfo.checksum !== opts.checksum) {
                continue;
            }

            result.push(dfsPath);
        }

        callback(null, result);
    };


    /**
     * @method aquireFileLock
     * @param {string} sid - server id
     * @param {string} dfsPath
     * @param {object} opt
     * @param {module:core/dfs/dfs-lock.callback_err_lock} callback
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.aquireFileLock = function (sid, dfsPath, opt, callback) {
        dfsLog.info('Aquiring master DFS lock' +
            (opt.exclusive ? '(E)' : '(S)') +
            '... ' + sid + ':' + dfsPath);
        masterLockMgr.aquire(dfsPath, opt, function (err, ret) {
            dfsLog.info('Aquired master DFS lock... ' + sid + ':' + dfsPath);
            callback(err, ret);
        });
    };


    /**
     * @method releaseFileLock
     * @param {module:core/dfs/dfs-lock.FileLock} oldLock
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:core/dfs/dist-fs.DistributedFileSystem
     */
    this.releaseFileLock = function (oldLock, callback) {
        masterLockMgr.release(oldLock);
        dfsLog.info('Released master DFS lock... ' + oldLock.dfsPath);
        callback(null);
    };


    function removeFileInternal(sid, dfsPath) {
        if (!masterIndex[dfsPath]) {
            return;
        }

        var idx = masterIndex[dfsPath].servers.indexOf(sid);
        if (idx >= 0) {
            dfsLog.info('Removed file from DFS master... ' + sid + ':' + dfsPath);
            masterIndex[dfsPath].servers.splice(idx, 1);

            if (masterIndex[dfsPath].servers.length === 0) {
                delete masterIndex[dfsPath];
            }
        }
    }
}


module.exports = DFSMasterIndex;
