/**
 * pkg-installer.js
 * Copyright (c) 2000 - 2017 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * Sungmin Kim <sm.art.kim@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 os = require('os');
var async = require('async');
var path = require('path');
var extfs = require('fs-extra');
var _ = require('underscore');

var utils = require('../../lib/utils');
var Process = require('../dibs.core/process.js');
var FileSystem = require('../dibs.core/filesystem.js');
var Zip = require('../dibs.core/zip.js');
var Package = require('./package.js');

var PACKAGE_INFO_DIR = '.info';
var INSTALLED_PKGLIST_FILE = 'installed.list';
var PACKAGE_INFO_FILE = 'package.json';
var PLUGIN_INFO_FILE = 'plugin.json';
var EXTRACT_LOG = 'extract.log';


module.exports.installPackages = installPackages;
module.exports.uninstallPackages = uninstallPackages;


function installPackages(pkgs, targetDir, options, monitor, callback) {
    var localPkgInfos = [];
    var installedPkgs = [];

    targetDir = path.resolve(targetDir);
    async.waterfall([
        // read installed pkgs
        function (cb) {
            monitor.updateProgress('Reading installed package list...');
            readInstalledPkgs(targetDir, function (err, iPkgs) {
                if (!err) {
                    installedPkgs = iPkgs;
                }
                cb(err);
            });
        },
        // read local package info & return
        function (cb) {
            monitor.updateProgress('Reading package information...');
            readLocalPkgs(pkgs, function (err, pkgInfos) {
                if (!err) {
                    cb(err, sortByInstallOrder(pkgInfos));
                } else {
                    cb(err, null);
                }
            });
        },
        // install packages
        function (orderedPkgs, cb) {
            installPackagesInSeries(orderedPkgs, targetDir, installedPkgs, options, monitor, cb);
        }
    ], function (err) {
        callback(err);
    });
}


function readInstalledPkgs(targetPath, callback) {
    var installedListPath = path.join(targetPath, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);
    fs.exists(installedListPath, function (exist) {
        if (exist) {
            callback(null, extfs.readJsonSync(installedListPath));
        } else {
            callback(null, {});
        }
    });
}


function readLocalPkgs(pkgs, callback) {

    if (pkgs.length === 0) {
        callback(null, []);return;
    }

    async.map(pkgs,
        function (pkgFilePath, cb) {
            Package.readPackageInfoFromFile(pkgFilePath, function (err, pkg, isPlugin) {
                cb(err, {
                    info: pkg,
                    isPlugin: isPlugin,
                    path: pkgFilePath
                });
            });
        },
        callback);
}


// TODO:
function sortByInstallOrder(srcPkgs) {
    return srcPkgs;
}


function installPackagesInSeries(orderedPkgs, targetDir, installedPkgs, options, monitor, callback) {
    // NOTE. To accumulate 'installedPkgs' , must use 'eachSeries'
    async.eachSeries(orderedPkgs,
        function (pkg, cb) {
            monitor.updateProgress('Installing package... ' + pkg.path);
            installSinglePackage(pkg, targetDir, installedPkgs, options, monitor, cb);
        }, callback);
}


function installSinglePackage(pkg, targetDir, installedPkgs, options, monitor, callback) {
    // compare package version
    if (installedPkgs[pkg.info.name] && !options.force &&
        utils.compareVersion(pkg.info.version, installedPkgs[pkg.info.name].version) <= 0) {

        callback(new DError('DPKG003')); return;
    }

    async.series([
        // uninstall old if exists
        function (cb) {
            if (installedPkgs[pkg.info.name]) {
                monitor.updateProgress('Uninstalling existing package... ' + pkg.info.name);
                uninstallRaw(pkg.info.name, targetDir, monitor, cb);
            } else {
                cb(null);
            }
        },
        // install
        function (cb) {
            installRaw(pkg, targetDir, monitor, cb);
        },
        // update installed pkgs
        function (cb) {
            updateInstalledPkgs(pkg, targetDir, cb);
        }], function (err) {
        callback(err);
    });
}


function installRaw(pkg, targetDir, monitor, callback) {
    var tempDir = null;
    var configPath = path.resolve(path.join(targetDir, PACKAGE_INFO_DIR, pkg.info.name));

    async.waterfall([
        function (cb) {
            monitor.updateProgress('Generating temp directory for extracting package file... ');
            utils.genTemp(cb);
        },
        function (temp, cb) {
            tempDir = temp;
            monitor.updateProgress('Extracting package file... ' + pkg.path);
            extractPackageFile(pkg.path, tempDir, monitor, cb);
        },
        // set pkg extract log
        function (cb) {
            monitor.updateProgress('Creating package config path...');
            if (!fs.existsSync(configPath)) {
                extfs.mkdirsSync(configPath);
            }
            cb(null);
        },
        function (cb) {
            monitor.updateProgress('Writing the list of files in package...');
            FileSystem.copy(path.join(tempDir, EXTRACT_LOG),
                path.join(configPath, pkg.info.name + '.list'),
                {
                    hardlink: false
                }, cb);
        },
        function (cb) {
            monitor.updateProgress('Copying package files to installation path...');
            moveData(tempDir, targetDir, cb);
        },
        function (cb) {
            monitor.updateProgress('Checking install script...');
            runScript(tempDir, targetDir, 'install', monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Saving remove script...');
            moveRemoveScript(tempDir, configPath, cb);
        }
    ], function (err) {
        if (tempDir !== null) {
            utils.removePathIfExist(tempDir, function (err1) {
                callback(err);
            });
        } else {
            callback(err);
        }
    });
}


function extractPackageFile(pkgFile, tempDir, monitor, callback) {
    var delim = null;
    if (os.platform() === 'win32') {
        delim = 'data' + path.sep;
    } else {
        delim = path.join(tempDir, 'data') + path.sep;
    }
    var contentsList = [];
    extractPackageFileInternal(pkgFile, tempDir, {
        onStdout: function (line) {
            if (line.indexOf(delim) !== -1) {
                contentsList.push(line.substring(line.indexOf(delim) + delim.length).trim());
            }
        },
        onStderr: function (line) {
            monitor.updateProgress({
                log: line,
                logType: 'error'
            });
        },
        onExit: function (code) {
            if (code !== 0) {
                var error = new Error('extract file process exited with code ' + code);
                callback(error);
            } else {
                fs.writeFile(path.join(tempDir, EXTRACT_LOG),
                    _.flatten(contentsList).join('\n'), callback);
            }
        }
    });
}


function extractPackageFileInternal(pkgFile, targetDir, options) {
    Zip.uncompress(pkgFile, targetDir, options, function (err) {
        // DO nothing
    });
}


function moveData(srcDir, targetDir, callback) {
    var dataPath = path.join(srcDir, 'data');
    fs.exists(dataPath, function (exist) {
        if (exist) {
            FileSystem.copy(dataPath, targetDir, {
                hardlink: true
            }, callback);
        } else {
            var error = new Error(srcDir + ' has no \'data\' folder!!');
            callback(error);
        }
    });
}


function runScript(srcDir, targetDir, scriptType, monitor, callback) {

    async.waterfall([
        function (cb) {
            findScript(srcDir, scriptType, cb);
        },
        function (scripts, cb) {
            if (scripts && scripts.length > 0) {
                monitor.updateProgress(' Executing ' + scriptType + ' script...');
                executeScript(scripts[0], targetDir, monitor, cb);
            } else {
                monitor.updateProgress(' No ' + scriptType + ' script exists!');
                cb(null);
            }
        }], function (err) {
        callback(err);
    });
}


function findScript(workDir, scriptType, callback) {
    var candidates = [];

    // check os name
    if (os.platform() === 'win32') {
        candidates.push(path.join(workDir, scriptType + '.bat'));
    } else {
        candidates.push(path.join(workDir, scriptType + '.sh'));
    }

    // get existing files
    async.filterSeries(candidates, fs.exists,
        function (results) {
            callback(null, results);
        });
}


function executeScript(script, targetDir, monitor, callback) {
    async.waterfall([
        function (wcb) {
            fs.chmod(script, '0777', wcb);
        },
        function (wcb) {
            var env = process.env;
            env.INSTALLED_PATH = targetDir;
            var run = Process.create(script,
                [],
                {
                    cwd: targetDir,
                    env: env
                },
                {
                    onStdout: function (line) {
                        monitor.updateProgress(line);
                    },
                    onStderr: function (line) {
                        monitor.updateProgress(line);
                    },
                    onExit: function (code) {
                        if (code !== 0) {
                            var error = new Error('Executing file(' + script + ') process exited with code ' + code);
                            wcb(error);
                        } else {
                            wcb(null);
                        }
                    }
                });
        }
    ], callback);
}


function moveRemoveScript(srcDir, targetDir, callback) {
    findScript(srcDir, 'remove', function (err, scripts) {
        if (scripts && scripts.length > 0) {
            var removeScript = path.join(targetDir, path.basename(scripts[0]));
            async.waterfall([
                function (cb) {
                    FileSystem.move(scripts[0], removeScript, cb);
                },
                function (cb) {
                    fs.chmod(removeScript, '0755', cb);
                }
            ],
                callback);
        } else {
            // no remove script exist
            callback(null);
        }
    });
}


function uninstallPackages(pkgNames, targetDir, monitor, callback) {
    targetDir = path.resolve(targetDir);

    async.eachSeries(pkgNames,
        function (pkgName, cb) {
            uninstallSinglePackage(pkgName, targetDir, monitor, cb);
        },
        function (err) {
            callback(err);
        });
}


function uninstallSinglePackage(pkgName, targetDir, monitor, callback) {
    async.series([
        function (cb) {
            uninstallRaw(pkgName, targetDir, monitor, cb);
        },
        function (cb) {
            removeFromInstalledPkgs(pkgName, targetDir, cb);
        }], function (err) {
        callback(err);
    });
}


function uninstallRaw(pkgName, targetDir, monitor, callback) {
    var configPath = path.join(targetDir, PACKAGE_INFO_DIR, pkgName);

    async.waterfall([
        function (cb) {
            runScript(configPath, targetDir, 'remove', monitor, function (err) {
                cb(err);
            });
        },
        function (cb) {
            var listFile = path.join(configPath, pkgName + '.list');
            monitor.updateProgress('Removing the files listed on ...' + listFile);
            removeFilesInList(targetDir, listFile, monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Removing package config...' + pkgName);
            FileSystem.remove(configPath, cb);
        }
    ],
        callback);
}


function removeFilesInList(targetDir, listFile, monitor, callback) {
    fs.exists(listFile, function (exist) {
        if (exist) {
            var dirs = [];
            async.waterfall([
                // read list file
                function (wcb) {
                    fs.readFile(listFile, wcb);
                },
                // remove files
                function (data, wcb) {
                    async.each(_.compact(data.toString().split('\n')), function (offset, ecb) {
                        var rmPath = path.join(targetDir, offset);
                        monitor.updateProgress(' * ' + rmPath);
                        fs.lstat(rmPath, function (err, stat) {
                            //it's OK if file not exist
                            if (err) {
                                monitor.updateProgress(rmPath + ' does not exist');
                                ecb(null);
                            } else {
                                if (stat.isDirectory()) {
                                    dirs.push(rmPath);
                                    ecb(null);
                                } else {
                                    fs.unlink(rmPath, ecb);
                                }
                            }
                        });
                    }, wcb);
                },
                // remove dirs
                function (wcb) {
                    async.eachSeries(_.sortBy(dirs, function (dir) {
                        return dir.length;
                    }).reverse(), function (sdir, ecb) {
                        fs.readdir(sdir, function (err, files) {
                            //it's OK if file not exist
                            if (err) {
                                ecb(null);
                            } else {
                                if (_.without(files, '.', '..').length === 0) {
                                    fs.rmdir(sdir, ecb);
                                } else {
                                    ecb(null);
                                }
                            }
                        });
                    }, wcb);
                }
            ], callback);
        } else {
            var error = new Error('File not exist : ' + listFile);
            callback(error);
        }
    });
}


function updateInstalledPkgs(pkg, targetDir, callback) {
    var installedListPath = path.join(targetDir, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);

    async.waterfall([
        function (cb) {
            readInstalledPkgs(targetDir, cb);
        },
        function (pkgs, cb) {
            pkgs[pkg.info.name] = pkg.info;
            extfs.outputJsonSync(installedListPath, pkgs);
            cb(null);
        }], function (err) {
        callback(err);
    });
}


function removeFromInstalledPkgs(pkgName, targetDir, callback) {
    var installedListPath = path.join(targetDir, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);

    async.waterfall([
        function (cb) {
            readInstalledPkgs(targetDir, cb);
        },
        function (pkgs, cb) {
            delete pkgs[pkgName];
            extfs.outputJsonSync(installedListPath, pkgs);
            cb(null);
        }], function (err) {
        callback(err);
    });
}
