/*
 * Copyright (c) 2014 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * 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.
 */

/*global define*/

/**
 * Archive module.
 * @requires {@link core/tizen}
 * @requires {@link core/event}
 * @namespace ArchiveManager/model/archive
 * @memberof ArchiveManager/model
 */
define({
    name: 'model/archive',
    requires: [
        'core/tizen',
        'core/event'
    ],
    def: function modelArchive(tizen, e) {
        'use strict';

        /**
         * Write mode string.
         * @type {string}
         */
        var WRITE_MODE = 'w',

            /**
             * Read mode string.
             * @type {string}
             */
            READ_MODE = 'r',

            /**
             * Regular expression recognizing slash character
             * at the end of the string.
             * @type {RegExp}
             */
            SLASH_AT_THE_END_REGEXP = /(\/)$/,

            /**
             * List of current archive members.
             * @type {object[]}
             */
            entriesArray = [],

            /**
             * Associative array indexed by paths
             * which stores archive members data.
             * @type {object}
             */
            entriesMap = {};

        /**
         * Returns percentage of partial progress.
         * @return {number} Progress value from 0 to 100.
         */
        function getPartial() {
            /*jshint validthis: true */
            if (this.partialMax === 0) {
                return 0;
            }
            return (this.value / this.partialMax) * 100;
        }

        /**
         * Returns percentage of total progress.
         * @return {number} Progress value from 0 to 100.
         */
        function getTotal() {
            /*jshint validthis: true */
            if (this.max === 0) {
                return 0;
            }
            if (!this.hasPartialInfo) {
                return (this.value * 100);
            }
            return (this.current / this.max) * 100;
        }

        /**
         * Progress object constructor.
         * @param {number} max
         * @param {boolean} hasPartialInfo
         */
        function Progress(max, hasPartialInfo) {
            /*jshint validthis: true */
            this.hasPartialInfo = !!hasPartialInfo;

            // Total progress.
            this.id = 0;
            this.current = 0;
            this.subTotal = 0;
            this.max = max;

            // Partial progress.
            this.partialId = 0;
            this.partialMax = 0;
            this.value = 0.0;
            this.filename = '';

            this.getPartial = getPartial;
            this.getTotal = getTotal;
        }

        function onError(ev) {
            e.fire('error', ev);
        }

        /**
         * Executed when aborting archiving finished.
         *
         * Deletes archive and fires 'aborted' event.
         * @param {object} progress
         */
        function onArchivingAbortFinished(progress) {
            tizen.filesystem.resolve(progress.destination,
                function onResolve(file) {
                    file.parent.deleteFile(file.fullPath,
                        function fireAbort() {
                            e.fire('aborted');
                        },
                        onError);
                });
        }

        /**
         * Updates progress data unless operation is aborted.
         * @param {number} fileSize Size of archived file.
         * @param {string} sourcePath Path of archived file or directory.
         * @param {number} value Value of progress.
         * @param {object} progress
         */
        function updateProgress(fileSize, sourcePath, value, progress) {
            if (progress.aborted) {
                return;
            }
            progress.partialMax = fileSize;
            progress.value = fileSize * value;
            progress.filename = sourcePath.split('/').pop();
            progress.current = progress.value + progress.subTotal;

            e.fire('progress', progress);
        }

        /**
         * Creates object that handles archived file progress event.
         *
         * @constructor
         * @param {number} fileSize Size of archived file.
         * @param {object} progress
         */
        function ArchiveFileProgressHandler(fileSize, progress) {
            /*jslint unparam: true */
            this.handle = function handle(operationId, value, filePath) {
                updateProgress(fileSize, filePath, value, progress);
                if (progress.aborted && value === 1) {
                    onArchivingAbortFinished(progress);
                }
            };
        }

        /**
         * Creates object that handles archived directory progress event.
         *
         * @constructor
         * @param {number} dirSize Size of archived directory.
         * @param {string} dirPath Path of archived directory.
         * @param {object} progress
         */
        function ArchiveDirectoryProgressHandler(dirSize, dirPath, progress) {
            /**
             * Size of all files that were added to archive plus size of file
             * that is being added to archive.
             * @type {number}
             */
            var limit = 0,

                /**
                 * Path of file that is being added to archive currently.
                 * @type {string}
                 */
                currentFilePath = null,

                /**
                 * Current value of progress.
                 * @type {number}
                 */
                currentValue = 0,

                /**
                 * If is 'false' then the checking if aborting process is
                 * finished (function aborted) is blocked.
                 * @type {boolean}
                 */
                checkForAbort = true;

            /**
             * Returns 'true' if aborting process is finished, 'false'
             * otherwise.
             * The aborting process will be finished when the currently added
             * file will be added completely.
             * This function checks if function abort was called
             * (archivingProgress.aborted) and if currently added file is
             * already added completely.
             * @return {boolean}
             */
            function isAborted() {
                return checkForAbort && progress.aborted && limit > 0 &&
                        // if currently file was added completely, 'limit'
                        // variable should be equal to 'currentValue * dirSize'.
                        // Due to avoid rounding errors, value of
                        // 'currentValue * dirSize / limit' is rounded to 4
                        // decimal places and then compared to '1'.
                    Number((currentValue * dirSize / limit)
                        .toFixed(4)) === 1;
            }

            this.handle = function handle(operationId, value, filePath) {
                /*jslint unparam: true */
                currentValue = value;

                if (currentFilePath !== filePath) {
                    // The adding of previous file is finished and next file
                    // from directory is being added.
                    // Until the size of new file is obtained the checking if
                    // operation is aborted has to be blocked because value of
                    // the 'limit' variable is not valid.
                    checkForAbort = false;
                    currentFilePath = filePath;

                    tizen.filesystem.resolve(
                        dirPath + filePath.substring(filePath.indexOf('/')),
                        function onResolve(file) {
                            // Now size of new file is obtained so limit is
                            // updated and checking if operation is aborted is
                            // unblocked
                            limit += file.fileSize;
                            checkForAbort = true;
                            if (isAborted()) {
                                onArchivingAbortFinished(progress);
                            }
                        }
                    );
                } else if (isAborted()) {
                    onArchivingAbortFinished(progress);
                }
                updateProgress(dirSize, filePath, value, progress);
            };
        }

        /**
         * Adds multiple files to an archive.
         * @param {File[]} files
         * @param {ArchiveFile} archive
         * @param {object} progress
         */
        function addToArchive(files, archive, progress) {
            var file = files.shift(),
                size = 0,
                progressHandler = null;

            if (progress.aborted) {
                return;
            }

            if (!file) {
                e.fire('completed', progress);
                return;
            }

            size = file.filesize;
            if (file.isDirectory) {
                progressHandler = new ArchiveDirectoryProgressHandler(size,
                    file.path, progress);
            } else {
                progressHandler = new ArchiveFileProgressHandler(size,
                    progress);
            }

            progress.value = 0;
            progress.filename = '';
            progress.partialId = 0;
            progress.partialId = archive.add(
                file.path,
                function onAddSuccess() {
                    progress.subTotal += size;

                    // Add the rest of the files.
                    addToArchive(files, archive, progress);

                    e.fire('progress', progress);
                },
                function errorHandler(ev) {
                    progress.partialId = 0;
                    onError(ev);
                },
                progressHandler.handle,
                {stripSourceDirectory: true}
            );
        }

        /**
         * Creates a new archive and adds files to it.
         * @param {File[]} files
         * @param {string} destination
         * @fires "create.started"
         * @fires "progress"
         * @fires "completed"
         * @fires "open.error"
         * @return {object} Progress object.
         * @memberof ArchiveManager/model/archive
         */
        function createArchive(files, destination) {
            files = files.slice(0);

            // Create progress object for the whole process.
            var progress = null,
                len = files.length,
                i = 0,
                totalSize = 0;

            for (i; i < len; i += 1) {
                totalSize += files[i].filesize;
            }

            progress = new Progress(totalSize, true);

            try {
                progress.id = tizen.archive.open(
                    destination,
                    WRITE_MODE,
                    function onCreateSuccess(archive) {
                        e.fire('create.started');
                        e.fire('progress', progress);
                        progress.archive = archive;
                        progress.destination = destination;
                        addToArchive(files, archive, progress);
                    }
                );
            } catch (ev) {
                e.fire('open.error', ev);
            }

            return progress;
        }

        /**
         * Extracts archive.
         * @param {string} archiveFullPath
         * @param {File} destinationFolder
         * @fires "extract.started"
         * @memberof ArchiveManager/model/archive
         */
        function extractArchive(archiveFullPath, destinationFolder) {
            var progress = new Progress(100, false);

            try {
                tizen.archive.open(
                    archiveFullPath,
                    READ_MODE,
                    function onOpenSuccess(archive) {
                        e.fire('extract.started', archive.extractAll(
                            destinationFolder,
                            function extractSuccess() {
                                e.fire('completed', progress);
                            },
                            onError,
                            function onProgress(operationId, value, filename) {
                                progress.partialId = operationId;
                                progress.value = value;
                                progress.filename = filename;
                                e.fire('progress', progress);
                            }
                        ));
                    }, onError
                );
            } catch (ev) {
                e.fire('open.error', ev);
            }
        }

        /**
         * Aborts extraction.
         * @param {number} taskId Task ID returned by 'extractArchive' method.
         * @memberof ArchiveManager/model/archive
         */
        function abortExtraction(taskId) {
            tizen.archive.abort(taskId);
            e.fire('aborted');
        }

        /**
         * Aborts archiving.
         *
         * Calls abort function from API on task related to creation archive
         * (progress.id) and on task related to archiving file into the archive
         * (progress.partialId) and closes archive.
         *
         * @param {object} progress Progress instance returned by
         * 'createArchive' method.
         *
         * @memberof ArchiveManager/model/archive
         */
        function abortArchiving(progress) {
            progress.aborted = true;
            tizen.archive.abort(progress.id);
            tizen.archive.abort(progress.partialId);
            progress.archive.close();
        }

        /**
         * Checks if the object with given key and value is in array.
         * Returns position of the object in array when the object is in array,
         * -1 otherwise.
         * @param {array} array
         * @param {string} key
         * @param {string} value
         * @return {number}
         */
        function findObjectInArray(array, key, value) {
            var len = array.length,
                i = 0;

            for (i; i < len; i += 1) {
                if (array[i][key] === value) {
                    return i;
                }
            }
            return -1;
        }

        /**
         * Prepares entries object with informations about archive members.
         * @param {string} name Archive file name.
         * @param {ArchiveFileEntry[]} entries
         */
        function prepareEntriesObject(name, entries) {
            var lenI = entries.length,
                lenJ = 0,
                i = 0,
                j = 0,
                entry = null,
                entryName = '',
                entryArray = null,
                currentArray = null,
                currentObject = null,
                currentIndex = 0,
                endsWithSeparator = false;

            entriesArray = [];
            entriesMap = {};

            for (i; i < lenI; i += 1) {
                entry = entries[i];
                endsWithSeparator = SLASH_AT_THE_END_REGEXP.test(entry.name);
                entryName = entry.name.replace(SLASH_AT_THE_END_REGEXP, '');
                entryArray = entryName.split('/');
                lenJ = entryArray.length;
                currentArray = entriesArray;
                for (j = 0; j < lenJ; j += 1) {
                    currentIndex = findObjectInArray(
                        currentArray,
                        'name',
                        entryArray[j]
                    );
                    if (currentIndex === -1) {
                        currentObject = {
                            name: entryArray[j],
                            list: [],
                            fullPath: entryName,
                            isFile: !((j < lenJ - 1) || endsWithSeparator),
                            isDirectory: (j < lenJ - 1) || endsWithSeparator
                        };
                        entriesMap[entryName] = currentObject;
                        currentArray.push(currentObject);
                    } else {
                        currentArray = currentArray[currentIndex].list;
                    }
                }
            }

            e.fire('entries.loaded', {name: name, entries: entriesArray});
        }

        /**
         * Returns archive entry with given path.
         * @param {string} path
         * @return {object}
         * @memberof ArchiveManager/model/archive
         */
        function getEntry(path) {
            return entriesMap[path];
        }

        /**
         * Returns array of root entries.
         * @return {object[]}
         * @memberof ArchiveManager/model/archive
         */
        function getRootEntries() {
            return entriesArray;
        }

        /**
         * Creates request for obtaining archive file members.
         * @param {string} url
         * @fires "entries.loaded"
         * @fires "open.error"
         * @memberof ArchiveManager/model/archive
         */
        function loadEntries(url) {
            try {
                tizen.archive.open(
                    url,
                    READ_MODE,
                    function onOpenSuccess(archive) {
                        archive.getEntries(
                            function onGetEntriesSuccess(entries) {
                                prepareEntriesObject(
                                    url.split('/').pop(),
                                    entries
                                );
                            }, onError
                        );
                    }, onError
                );
            } catch (ev) {
                e.fire('open.error', ev);
            }
        }

        return {
            createArchive: createArchive,
            extractArchive: extractArchive,
            loadEntries: loadEntries,
            getEntry: getEntry,
            getRootEntries: getRootEntries,
            abortExtraction: abortExtraction,
            abortArchiving: abortArchiving
        };
    }
});
