'use strict';

var md5 = require('../lib/md5');
var CryptoJS = require('../lib/crypto');
var xml2json = require('../lib/xml2json');
var json2xml = require('../lib/json2xml');

function camSafeUrlEncode(str) {
    return encodeURIComponent(str)
        .replace(/!/g, '%21')
        .replace(/'/g, '%27')
        .replace(/\(/g, '%28')
        .replace(/\)/g, '%29')
        .replace(/\*/g, '%2A');
}

//测试用的key后面可以去掉
var getAuth = function (opt) {
    opt = opt || {};

    var SecretId = opt.SecretId;
    var SecretKey = opt.SecretKey;
    var method = (opt.method || opt.Method || 'get').toLowerCase();
    var queryParams = clone(opt.Query || opt.params || {});
    var headers = clone(opt.Headers || opt.headers || {});

    var Key = opt.Key || '';
    var pathname;
    if (opt.UseRawKey) {
        pathname = opt.Pathname || opt.pathname || '/' + Key;
    } else {
        pathname = opt.Pathname || opt.pathname || Key;
        pathname.indexOf('/') !== 0 && (pathname = '/' + pathname);
    }

    if (!SecretId) return console.error('missing param SecretId');
    if (!SecretKey) return console.error('missing param SecretKey');

    var getObjectKeys = function (obj) {
        var list = [];
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                list.push(key);
            }
        }
        return list.sort(function (a, b) {
            a = a.toLowerCase();
            b = b.toLowerCase();
            return a === b ? 0 : (a > b ? 1 : -1);
        });
    };

    var obj2str = function (obj) {
        var i, key, val;
        var list = [];
        var keyList = getObjectKeys(obj);
        for (i = 0; i < keyList.length; i++) {
            key = keyList[i];
            val = (obj[key] === undefined || obj[key] === null) ? '' : ('' + obj[key]);
            key = key.toLowerCase();
            key = camSafeUrlEncode(key);
            val = camSafeUrlEncode(val) || '';
            list.push(key + '=' + val)
        }
        return list.join('&');
    };

    // 签名有效起止时间
    var now = Math.round(getSkewTime(opt.SystemClockOffset) / 1000) - 1;
    var exp = now;

    var Expires = opt.Expires || opt.expires;
    if (Expires === undefined) {
        exp += 900; // 签名过期时间为当前 + 900s
    } else {
        exp += (Expires * 1) || 0;
    }

    // 要用到的 Authorization 参数列表
    var qSignAlgorithm = 'sha1';
    var qAk = SecretId;
    var qSignTime = now + ';' + exp;
    var qKeyTime = now + ';' + exp;
    var qHeaderList = getObjectKeys(headers).join(';').toLowerCase();
    var qUrlParamList = getObjectKeys(queryParams).join(';').toLowerCase();

    // 签名算法说明文档：https://www.qcloud.com/document/product/436/7778
    // 步骤一：计算 SignKey
    var signKey = CryptoJS.HmacSHA1(qKeyTime, SecretKey).toString();

    // 步骤二：构成 FormatString
    var formatString = [method, pathname, obj2str(queryParams), obj2str(headers), ''].join('\n');

    // 步骤三：计算 StringToSign
    var stringToSign = ['sha1', qSignTime, CryptoJS.SHA1(formatString).toString(), ''].join('\n');

    // 步骤四：计算 Signature
    var qSignature = CryptoJS.HmacSHA1(stringToSign, signKey).toString();

    // 步骤五：构造 Authorization
    var authorization = [
        'q-sign-algorithm=' + qSignAlgorithm,
        'q-ak=' + qAk,
        'q-sign-time=' + qSignTime,
        'q-key-time=' + qKeyTime,
        'q-header-list=' + qHeaderList,
        'q-url-param-list=' + qUrlParamList,
        'q-signature=' + qSignature
    ].join('&');

    return authorization;

};

var noop = function () {

};

// 清除对象里值为的 undefined 或 null 的属性
var clearKey = function (obj) {
    var retObj = {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key) && obj[key] !== undefined && obj[key] !== null) {
            retObj[key] = obj[key];
        }
    }
    return retObj;
};

var readAsBinaryString = function (blob, callback) {
    var readFun;
    var fr = new FileReader();
    if (FileReader.prototype.readAsBinaryString) {
        readFun = FileReader.prototype.readAsBinaryString;
        fr.onload = function () {
            callback(this.result);
        };
    } else if (FileReader.prototype.readAsArrayBuffer) { // 在 ie11 添加 readAsBinaryString 兼容
        readFun = function (fileData) {
            var binary = "";
            var pt = this;
            var reader = new FileReader();
            reader.onload = function (e) {
                var bytes = new Uint8Array(reader.result);
                var length = bytes.byteLength;
                for (var i = 0; i < length; i++) {
                    binary += String.fromCharCode(bytes[i]);
                }
                callback(binary);
            };
            reader.readAsArrayBuffer(fileData);
        };
    } else {
        console.error('FileReader not support readAsBinaryString');
    }
    readFun.call(fr, blob);
};

// 获取文件 md5 值
var getFileMd5 = function (blob, callback) {
    readAsBinaryString(blob, function (content) {
        var hash = md5(content, true);
        callback(null, hash);
    });
};

function clone(obj) {
    return map(obj, function (v) {
        return typeof v === 'object' ? clone(v) : v;
    });
}

function extend(target, source) {
    each(source, function (val, key) {
        target[key] = source[key];
    });
    return target;
}

function isArray(arr) {
    return arr instanceof Array;
}

function isInArray(arr, item) {
    var flag = false;
    for (var i = 0; i < arr.length; i++) {
        if (item === arr[i]) {
            flag = true;
            break;
        }
    }
    return flag;
}

function each(obj, fn) {
    for (var i in obj) {
        if (obj.hasOwnProperty(i)) {
            fn(obj[i], i);
        }
    }
}

function map(obj, fn) {
    var o = isArray(obj) ? [] : {};
    for (var i in obj) {
        if (obj.hasOwnProperty(i)) {
            o[i] = fn(obj[i], i);
        }
    }
    return o;
}

function filter(obj, fn) {
    var iaArr = isArray(obj);
    var o = iaArr ? [] : {};
    for (var i in obj) {
        if (obj.hasOwnProperty(i)) {
            if (fn(obj[i], i)) {
                if (iaArr) {
                    o.push(obj[i]);
                } else {
                    o[i] = obj[i];
                }
            }
        }
    }
    return o;
}

var binaryBase64 = function (str) {
    var i, len, char, res = '';
    for (i = 0, len = str.length / 2; i < len; i++) {
        char = parseInt(str[i * 2] + str[i * 2 + 1], 16);
        res += String.fromCharCode(char);
    }
    return btoa(res);
};
var uuid = function () {
    var S4 = function () {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    };
    return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
};

var hasMissingParams = function (apiName, params) {
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    if (apiName.indexOf('Bucket') > -1 || apiName === 'deleteMultipleObject' || apiName === 'multipartList' || apiName === 'listObjectVersions') {
        if (!Bucket) return 'Bucket';
        if (!Region) return 'Region';
    } else if (apiName.indexOf('Object') > -1 || apiName.indexOf('multipart') > -1 || apiName === 'sliceUploadFile' || apiName === 'abortUploadTask') {
        if (!Bucket) return 'Bucket';
        if (!Region) return 'Region';
        if (!Key) return 'Key';
    }
    return false;
};

var formatParams = function (apiName, params) {

    // 复制参数对象
    params = extend({}, params);

    // 统一处理 Headers
    if (apiName !== 'getAuth' && apiName !== 'getV4Auth' && apiName !== 'getObjectUrl') {
        var Headers = params.Headers || {};
        if (params && typeof params === 'object') {
            (function () {
                for (var key in params) {
                    if (params.hasOwnProperty(key) && key.indexOf('x-cos-') > -1) {
                        Headers[key] = params[key];
                    }
                }
            })();

            var headerMap = {
                // params headers
                'x-cos-mfa': 'MFA',
                'Content-MD5': 'ContentMD5',
                'Content-Length': 'ContentLength',
                'Content-Type': 'ContentType',
                'Expect': 'Expect',
                'Expires': 'Expires',
                'Cache-Control': 'CacheControl',
                'Content-Disposition': 'ContentDisposition',
                'Content-Encoding': 'ContentEncoding',
                'Range': 'Range',
                'If-Modified-Since': 'IfModifiedSince',
                'If-Unmodified-Since': 'IfUnmodifiedSince',
                'If-Match': 'IfMatch',
                'If-None-Match': 'IfNoneMatch',
                'x-cos-copy-source': 'CopySource',
                'x-cos-copy-source-Range': 'CopySourceRange',
                'x-cos-metadata-directive': 'MetadataDirective',
                'x-cos-copy-source-If-Modified-Since': 'CopySourceIfModifiedSince',
                'x-cos-copy-source-If-Unmodified-Since': 'CopySourceIfUnmodifiedSince',
                'x-cos-copy-source-If-Match': 'CopySourceIfMatch',
                'x-cos-copy-source-If-None-Match': 'CopySourceIfNoneMatch',
                'x-cos-acl': 'ACL',
                'x-cos-grant-read': 'GrantRead',
                'x-cos-grant-write': 'GrantWrite',
                'x-cos-grant-full-control': 'GrantFullControl',
                'x-cos-grant-read-acp': 'GrantReadAcp',
                'x-cos-grant-write-acp': 'GrantWriteAcp',
                'x-cos-storage-class': 'StorageClass',
                // SSE-C
                'x-cos-server-side-encryption-customer-algorithm': 'SSECustomerAlgorithm',
                'x-cos-server-side-encryption-customer-key': 'SSECustomerKey',
                'x-cos-server-side-encryption-customer-key-MD5': 'SSECustomerKeyMD5',
                // SSE-COS、SSE-KMS
                'x-cos-server-side-encryption': 'ServerSideEncryption',
                'x-cos-server-side-encryption-cos-kms-key-id': 'SSEKMSKeyId',
                'x-cos-server-side-encryption-context': 'SSEContext',
            };
            util.each(headerMap, function (paramKey, headerKey) {
                if (params[paramKey] !== undefined) {
                    Headers[headerKey] = params[paramKey];
                }
            });

            params.Headers = clearKey(Headers);
        }
    }

    return params;
};

var apiWrapper = function (apiName, apiFn) {
    return function (params, callback) {

        // 处理参数
        if (typeof params === 'function') {
            callback = params;
            params = {};
        }

        // 整理参数格式
        params = formatParams(apiName, params);

        // 代理回调函数
        var formatResult = function (result) {
            if (result && result.headers) {
                result.headers['x-cos-version-id'] && (result.VersionId = result.headers['x-cos-version-id']);
                result.headers['x-cos-delete-marker'] && (result.DeleteMarker = result.headers['x-cos-delete-marker']);
            }
            return result;
        };
        var _callback = function (err, data) {
            callback && callback(formatResult(err), formatResult(data));
        };

        if (apiName !== 'getService' && apiName !== 'abortUploadTask') {
            // 判断参数是否完整
            var missingResult;
            if (missingResult = hasMissingParams(apiName, params)) {
                _callback({error: 'missing param ' + missingResult});
                return;
            }
            // 判断 region 格式
            if (params.Region) {
                if (params.Region.indexOf('cos.') > -1) {
                    _callback({error: 'param Region should not be start with "cos."'});
                    return;
                } else if (!/^([a-z\d-]+)$/.test(params.Region)) {
                    _callback({error: 'Region format error.'});
                    return;
                }
                // 判断 region 格式
                if (!this.options.CompatibilityMode && params.Region.indexOf('-') === -1 && params.Region !== 'yfb' && params.Region !== 'default') {
                    console.warn('warning: param Region format error, find help here: https://cloud.tencent.com/document/product/436/6224');
                }
            }
            // 兼容不带 AppId 的 Bucket
            if (params.Bucket) {
                if (!/^([a-z\d-]+)-(\d+)$/.test(params.Bucket)) {
                    if (params.AppId) {
                        params.Bucket = params.Bucket + '-' + params.AppId;
                    } else if (this.options.AppId) {
                        params.Bucket = params.Bucket + '-' + this.options.AppId;
                    } else {
                        _callback({error: 'Bucket should format as "test-1250000000".'});
                        return;
                    }
                }
                if (params.AppId) {
                    console.warn('warning: AppId has been deprecated, Please put it at the end of parameter Bucket(E.g Bucket:"test-1250000000" ).');
                    delete params.AppId;
                }
            }
            // 如果 Key 是 / 开头，强制去掉第一个 /
            if (!this.options.UseRawKey && params.Key && params.Key.substr(0, 1) === '/') {
                params.Key = params.Key.substr(1);
            }
        }
        var res = apiFn.call(this, params, _callback);
        if (apiName === 'getAuth' || apiName === 'getObjectUrl') {
            return res;
        }
    }
};

var throttleOnProgress = function (total, onProgress) {
    var self = this;
    var size0 = 0;
    var size1 = 0;
    var time0 = Date.now();
    var time1;
    var timer;

    function update() {
        timer = 0;
        if (onProgress && (typeof onProgress === 'function')) {
            time1 = Date.now();
            var speed = Math.max(0, Math.round((size1 - size0) / ((time1 - time0) / 1000) * 100) / 100);
            var percent;
            if (size1 === 0 && total === 0) {
                percent = 1;
            } else {
                percent = Math.round(size1 / total * 100) / 100 || 0;
            }
            time0 = time1;
            size0 = size1;
            try {
                onProgress({loaded: size1, total: total, speed: speed, percent: percent});
            } catch (e) {
            }
        }
    }

    return function (info, immediately) {
        if (info) {
            size1 = info.loaded;
            total = info.total;
        }
        if (immediately) {
            clearTimeout(timer);
            update();
        } else {
            if (timer) return;
            timer = setTimeout(update, self.options.ProgressInterval);
        }
    };
};

var getFileSize = function (api, params, callback) {
    var size;
    if (typeof params.Body === 'string') {
        params.Body = new Blob([params.Body], {type: 'text/plain'});
    }
    if ((params.Body && (params.Body instanceof Blob || params.Body.toString() === '[object File]' || params.Body.toString() === '[object Blob]'))) {
        size = params.Body.size;
    } else {
        callback({error: 'params body format error, Only allow File|Blob|String.'});
        return;
    }
    params.ContentLength = size;
    callback(null, size);
};

var getSkewTime = function (offset) {
    return Date.now() + (offset || 0);
};

var util = {
    noop: noop,
    formatParams: formatParams,
    apiWrapper: apiWrapper,
    xml2json: xml2json,
    json2xml: json2xml,
    md5: md5,
    clearKey: clearKey,
    getFileMd5: getFileMd5,
    binaryBase64: binaryBase64,
    extend: extend,
    isArray: isArray,
    isInArray: isInArray,
    each: each,
    map: map,
    filter: filter,
    clone: clone,
    uuid: uuid,
    camSafeUrlEncode: camSafeUrlEncode,
    throttleOnProgress: throttleOnProgress,
    getFileSize: getFileSize,
    getSkewTime: getSkewTime,
    getAuth: getAuth,
    isBrowser: true,
};

var fileSliceNeedCopy = (function () {
    var compareVersion = function(a, b) {
        a = a.split('.');
        b = b.split('.');
        for (var i = 0; i < b.length; i++) {
            if (a[i] !== b[i]) {
                return parseInt(a[i]) > parseInt(b[i]) ? 1 : -1;
            }
        }
        return 0;
    };
    var check = function (ua) {
        var ChromeVersion = (ua.match(/Chrome\/([.\d]+)/) || [])[1];
        var QBCoreVersion = (ua.match(/QBCore\/([.\d]+)/) || [])[1];
        var QQBrowserVersion = (ua.match(/QQBrowser\/([.\d]+)/) || [])[1];
        var need = ChromeVersion && compareVersion(ChromeVersion, '53.0.2785.116') < 0
            && QBCoreVersion && compareVersion(QBCoreVersion, '3.53.991.400') < 0
            && QQBrowserVersion && compareVersion(QQBrowserVersion, '9.0.2524.400') <= 0 || false;
        return need;
    };
    return check(navigator && navigator.userAgent);
})();
util.fileSlice = function (file, start, end, isUseToUpload, callback) {
    var blob;
    if (file.slice) {
        blob = file.slice(start, end);
    } else if (file.mozSlice) {
        blob = file.mozSlice(start, end);
    } else if (file.webkitSlice) {
        blob = file.webkitSlice(start, end);
    }
    if (isUseToUpload && fileSliceNeedCopy) {
        var reader = new FileReader();
        reader.onload = function (e) {
            blob = null;
            callback(new Blob([reader.result]));
        };
        reader.readAsArrayBuffer(blob);
    } else {
        callback(blob);
    }
};
util.getFileUUID = function (file, ChunkSize) {
    // 如果信息不完整，不获取
    if (file.name && file.size && file.lastModifiedDate && ChunkSize) {
        return util.md5([file.name, file.size, file.lastModifiedDate, ChunkSize].join('::'));
    } else {
        return null;
    }
};
util.getBodyMd5 = function (UploadCheckContentMd5, Body, callback) {
    callback = callback || noop;
    if (UploadCheckContentMd5) {
        if (typeof Body === 'string') {
            callback(util.md5(Body, true));
        } else if (Blob && Body instanceof Blob) {
            util.getFileMd5(Body, function (err, md5) {
                callback(md5);
            });
        } else {
            callback();
        }
    } else {
        callback();
    }
};

module.exports = util;


var Async = require('./async');
var EventProxy = require('./event').EventProxy;
var util = require('./util');

// 文件分块上传全过程，暴露的分块上传接口
function sliceUploadFile(params, callback) {
    var self = this;
    var ep = new EventProxy();
    var TaskId = params.TaskId;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var Body = params.Body;
    var ChunkSize = params.ChunkSize || params.SliceSize || self.options.ChunkSize;
    var AsyncLimit = params.AsyncLimit;
    var StorageClass = params.StorageClass || 'Standard';
    var ServerSideEncryption = params.ServerSideEncryption;
    var FileSize;

    var onProgress;
    var onHashProgress = params.onHashProgress;

    // 上传过程中出现错误，返回错误
    ep.on('error', function (err) {
        if (!self._isRunningTask(TaskId)) return;
        return callback(err);
    });

    // 上传分块完成，开始 uploadSliceComplete 操作
    ep.on('upload_complete', function (UploadCompleteData) {
        callback(null, UploadCompleteData);
    });

    // 上传分块完成，开始 uploadSliceComplete 操作
    ep.on('upload_slice_complete', function (UploadData) {
        uploadSliceComplete.call(self, {
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            UploadId: UploadData.UploadId,
            SliceList: UploadData.SliceList,
        }, function (err, data) {
            if (!self._isRunningTask(TaskId)) return;
            delete uploadIdUsing[UploadData.UploadId];
            if (err) {
                onProgress(null, true);
                return ep.emit('error', err);
            }
            onProgress({loaded: FileSize, total: FileSize}, true);
            removeUploadId.call(self, UploadData.UploadId);
            ep.emit('upload_complete', data);
        });
    });

    // 获取 UploadId 完成，开始上传每个分片
    ep.on('get_upload_data_finish', function (UploadData) {

        // 处理 UploadId 缓存
        var uuid = util.getFileUUID(Body, params.ChunkSize);
        uuid && setUploadId.call(self, uuid, UploadData.UploadId); // 缓存 UploadId
        uploadIdUsing[UploadData.UploadId] = true; // 标记 UploadId 为正在使用
        TaskId && self.on('inner-kill-task', function (data) {
            if (data.TaskId === TaskId && data.toState === 'canceled') {
                delete uploadIdUsing[UploadData.UploadId]; // 去除 UploadId 正在使用的标记
            }
        });

        // 获取 UploadId
        uploadSliceList.call(self, {
            TaskId: TaskId,
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            Body: Body,
            FileSize: FileSize,
            SliceSize: ChunkSize,
            AsyncLimit: AsyncLimit,
            ServerSideEncryption: ServerSideEncryption,
            UploadData: UploadData,
            onProgress: onProgress
        }, function (err, data) {
            if (!self._isRunningTask(TaskId)) return;
            if (err) {
                onProgress(null, true);
                return ep.emit('error', err);
            }
            ep.emit('upload_slice_complete', data);
        });
    });

    // 开始获取文件 UploadId，里面会视情况计算 ETag，并比对，保证文件一致性，也优化上传
    ep.on('get_file_size_finish', function () {

        onProgress = util.throttleOnProgress.call(self, FileSize, params.onProgress);

        if (params.UploadData.UploadId) {
            ep.emit('get_upload_data_finish', params.UploadData);
        } else {
            var _params = util.extend({
                TaskId: TaskId,
                Bucket: Bucket,
                Region: Region,
                Key: Key,
                Headers: params.Headers,
                StorageClass: StorageClass,
                Body: Body,
                FileSize: FileSize,
                SliceSize: ChunkSize,
                onHashProgress: onHashProgress,
            }, params);
            getUploadIdAndPartList.call(self, _params, function (err, UploadData) {
                if (!self._isRunningTask(TaskId)) return;
                if (err) return ep.emit('error', err);
                params.UploadData.UploadId = UploadData.UploadId;
                params.UploadData.PartList = UploadData.PartList;
                ep.emit('get_upload_data_finish', params.UploadData);
            });
        }
    });

    // 获取上传文件大小
    FileSize = params.ContentLength;
    delete params.ContentLength;
    !params.Headers && (params.Headers = {});
    util.each(params.Headers, function (item, key) {
        if (key.toLowerCase() === 'content-length') {
            delete params.Headers[key];
        }
    });

    // 控制分片大小
    (function () {
        var SIZE = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 1024 * 2, 1024 * 4, 1024 * 5];
        var AutoChunkSize = 1024 * 1024;
        for (var i = 0; i < SIZE.length; i++) {
            AutoChunkSize = SIZE[i] * 1024 * 1024;
            if (FileSize / AutoChunkSize <= self.options.MaxPartNumber) break;
        }
        params.ChunkSize = params.SliceSize = ChunkSize = Math.max(ChunkSize, AutoChunkSize);
    })();

    // 开始上传
    if (FileSize === 0) {
        params.Body = '';
        params.ContentLength = 0;
        params.SkipTask = true;
        self.putObject(params, function (err, data) {
            if (err) {
                return callback(err);
            }
            callback(null, data);
        });
    } else {
        ep.emit('get_file_size_finish');
    }

}

// 按照文件特征值，缓存 UploadId
var uploadIdCache;
var uploadIdUsing = {};
var uploadIdCacheKey = 'cos_sdk_upload_cache';
function initUploadId() {
    var cacheLimit = this.options.UploadIdCacheLimit;
    if (!uploadIdCache) {
        if (cacheLimit) {
            try {
                uploadIdCache = JSON.parse(localStorage.getItem(uploadIdCacheKey)) || [];
            } catch (e) {}
        }
        if (!uploadIdCache) {
            uploadIdCache = [];
        }
    }
}
function setUploadId(uuid, UploadId, isDisabled) {
    initUploadId.call(this);
    for (var i = uploadIdCache.length - 1; i >= 0; i--) {
        if (uploadIdCache[i][0] === uuid && uploadIdCache[i][1] === UploadId) {
            uploadIdCache.splice(i, 1);
        }
    }
    uploadIdCache.unshift([uuid, UploadId]);
    var cacheLimit = this.options.UploadIdCacheLimit;
    if (uploadIdCache.length > cacheLimit) {
        uploadIdCache.splice(cacheLimit);
    }
    cacheLimit && setTimeout(function () {
        try {
            localStorage.setItem(uploadIdCacheKey, JSON.stringify(uploadIdCache));
        } catch (e) {}
    });
}
function removeUploadId(UploadId) {
    initUploadId.call(this);
    delete uploadIdUsing[UploadId];
    for (var i = uploadIdCache.length - 1; i >= 0; i--) {
        if (uploadIdCache[i][1] === UploadId) {
            uploadIdCache.splice(i, 1)
        }
    }
    var cacheLimit = this.options.UploadIdCacheLimit;
    if (uploadIdCache.length > cacheLimit) {
        uploadIdCache.splice(cacheLimit);
    }
    cacheLimit && setTimeout(function () {
        try {
            if (uploadIdCache.length) {
                localStorage.setItem(uploadIdCacheKey, JSON.stringify(uploadIdCache));
            } else {
                localStorage.removeItem(uploadIdCacheKey);
            }
        } catch (e) {}
    });
}
function getUploadId(uuid) {
    initUploadId.call(this);
    var CacheUploadIdList = [];
    for (var i = 0; i < uploadIdCache.length; i++) {
        if (uploadIdCache[i][0] === uuid) {
            CacheUploadIdList.push(uploadIdCache[i][1]);
        }
    }
    return CacheUploadIdList.length ? CacheUploadIdList : null;
}

// 获取上传任务的 UploadId
function getUploadIdAndPartList(params, callback) {
    var TaskId = params.TaskId;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var StorageClass = params.StorageClass;
    var self = this;

    // 计算 ETag
    var ETagMap = {};
    var FileSize = params.FileSize;
    var SliceSize = params.SliceSize;
    var SliceCount = Math.ceil(FileSize / SliceSize);
    var FinishSliceCount = 0;
    var FinishSize = 0;
    var onHashProgress = util.throttleOnProgress.call(self, FileSize, params.onHashProgress);
    var getChunkETag = function (PartNumber, callback) {
        var start = SliceSize * (PartNumber - 1);
        var end = Math.min(start + SliceSize, FileSize);
        var ChunkSize = end - start;

        if (ETagMap[PartNumber]) {
            callback(null, {
                PartNumber: PartNumber,
                ETag: ETagMap[PartNumber],
                Size: ChunkSize
            });
        } else {
            util.fileSlice(params.Body, start, end, false, function (chunkItem) {
                util.getFileMd5(chunkItem, function (err, md5) {
                    if (err) return callback(err);
                    var ETag = '"' + md5 + '"';
                    ETagMap[PartNumber] = ETag;
                    FinishSliceCount += 1;
                    FinishSize += ChunkSize;
                    callback(err, {
                        PartNumber: PartNumber,
                        ETag: ETag,
                        Size: ChunkSize
                    });
                    onHashProgress({loaded: FinishSize, total: FileSize});
                });
            });
        }
    };

    // 通过和文件的 md5 对比，判断 UploadId 是否可用
    var isAvailableUploadList = function (PartList, callback) {
        var PartCount = PartList.length;
        // 如果没有分片，通过
        if (PartCount === 0) {
            return callback(null, true);
        }
        // 检查分片数量
        if (PartCount > SliceCount) {
            return callback(null, false);
        }
        // 检查分片大小
        if (PartCount > 1) {
            var PartSliceSize = Math.max(PartList[0].Size, PartList[1].Size);
            if (PartSliceSize !== SliceSize) {
                return callback(null, false);
            }
        }
        // 逐个分片计算并检查 ETag 是否一致
        var next = function (index) {
            if (index < PartCount) {
                var Part = PartList[index];
                getChunkETag(Part.PartNumber, function (err, chunk) {
                    if (chunk && chunk.ETag === Part.ETag && chunk.Size === Part.Size) {
                        next(index + 1);
                    } else {
                        callback(null, false);
                    }
                });
            } else {
                callback(null, true);
            }
        };
        next(0);
    };

    var ep = new EventProxy();
    ep.on('error', function (errData) {
        if (!self._isRunningTask(TaskId)) return;
        return callback(errData);
    });

    // 存在 UploadId
    ep.on('upload_id_ready', function (UploadData) {
        // 转换成 map
        var map = {};
        var list = [];
        util.each(UploadData.PartList, function (item) {
            map[item.PartNumber] = item;
        });
        for (var PartNumber = 1; PartNumber <= SliceCount; PartNumber++) {
            var item = map[PartNumber];
            if (item) {
                item.PartNumber = PartNumber;
                item.Uploaded = true;
            } else {
                item = {
                    PartNumber: PartNumber,
                    ETag: null,
                    Uploaded: false
                };
            }
            list.push(item);
        }
        UploadData.PartList = list;
        callback(null, UploadData);
    });

    // 不存在 UploadId, 初始化生成 UploadId
    ep.on('no_available_upload_id', function () {
        if (!self._isRunningTask(TaskId)) return;
        var _params = util.extend({
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            Headers: util.clone(params.Headers),
            StorageClass: StorageClass,
            Body: params.Body,
        }, params);
        // 获取 File 或 Blob 的 type 属性，如果有，作为文件 Content-Type
        var ContentType = params.Headers['Content-Type'] || (params.Body && params.Body.type);
        if (ContentType) {
            _params.Headers['Content-Type'] = ContentType;
        }
        self.multipartInit(_params, function (err, data) {
            if (!self._isRunningTask(TaskId)) return;
            if (err) return ep.emit('error', err);
            var UploadId = data.UploadId;
            if (!UploadId) {
                return callback({Message: 'no upload id'});
            }
            ep.emit('upload_id_ready', {UploadId: UploadId, PartList: []});
        });
    });

    // 如果已存在 UploadId，找一个可以用的 UploadId
    ep.on('has_upload_id', function (UploadIdList) {
        // 串行地，找一个内容一致的 UploadId
        UploadIdList = UploadIdList.reverse();
        Async.eachLimit(UploadIdList, 1, function (UploadId, asyncCallback) {
            if (!self._isRunningTask(TaskId)) return;
            // 如果正在上传，跳过
            if (uploadIdUsing[UploadId]) {
                asyncCallback(); // 检查下一个 UploadId
                return;
            }
            // 判断 UploadId 是否可用
            wholeMultipartListPart.call(self, {
                Bucket: Bucket,
                Region: Region,
                Key: Key,
                UploadId: UploadId,
            }, function (err, PartListData) {
                if (!self._isRunningTask(TaskId)) return;
                if (err) {
                    removeUploadId.call(self, UploadId);
                    return ep.emit('error', err);
                }
                var PartList = PartListData.PartList;
                PartList.forEach(function (item) {
                    item.PartNumber *= 1;
                    item.Size *= 1;
                    item.ETag = item.ETag || '';
                });
                isAvailableUploadList(PartList, function (err, isAvailable) {
                    if (!self._isRunningTask(TaskId)) return;
                    if (err) return ep.emit('error', err);
                    if (isAvailable) {
                        asyncCallback({
                            UploadId: UploadId,
                            PartList: PartList
                        }); // 马上结束
                    } else {
                        asyncCallback(); // 检查下一个 UploadId
                    }
                });
            });
        }, function (AvailableUploadData) {
            if (!self._isRunningTask(TaskId)) return;
            onHashProgress(null, true);
            if (AvailableUploadData && AvailableUploadData.UploadId) {
                ep.emit('upload_id_ready', AvailableUploadData);
            } else {
                ep.emit('no_available_upload_id');
            }
        });
    });

    // 在本地缓存找可用的 UploadId
    ep.on('seek_local_avail_upload_id', function (RemoteUploadIdList) {
        // 在本地找可用的 UploadId
        var uuid = util.getFileUUID(params.Body, params.ChunkSize), LocalUploadIdList;
        if (uuid && (LocalUploadIdList = getUploadId.call(self, uuid))) {
            var next = function (index) {
                // 如果本地找不到可用 UploadId，再一个个遍历校验远端
                if (index >= LocalUploadIdList.length) {
                    ep.emit('has_upload_id', RemoteUploadIdList);
                    return;
                }
                var UploadId = LocalUploadIdList[index];
                // 如果不在远端 UploadId 列表里，跳过并删除
                if (!util.isInArray(RemoteUploadIdList, UploadId)) {
                    removeUploadId.call(self, UploadId);
                    next(index + 1);
                    return;
                }
                // 如果正在上传，跳过
                if (uploadIdUsing[UploadId]) {
                    next(index + 1);
                    return;
                }
                // 判断 UploadId 是否存在线上
                wholeMultipartListPart.call(self, {
                    Bucket: Bucket,
                    Region: Region,
                    Key: Key,
                    UploadId: UploadId,
                }, function (err, PartListData) {
                    if (!self._isRunningTask(TaskId)) return;
                    if (err) {
                        removeUploadId.call(self, UploadId);
                        next(index + 1);
                    } else {
                        // 找到可用 UploadId
                        ep.emit('upload_id_ready', {
                            UploadId: UploadId,
                            PartList: PartListData.PartList,
                        });
                    }
                });
            };
            next(0);
        } else {
            ep.emit('has_upload_id', RemoteUploadIdList);
        }
    });

    // 获取线上 UploadId 列表
    ep.on('get_remote_upload_id_list', function (RemoteUploadIdList) {
        // 获取符合条件的 UploadId 列表，因为同一个文件可以有多个上传任务。
        wholeMultipartList.call(self, {
            Bucket: Bucket,
            Region: Region,
            Key: Key,
        }, function (err, data) {
            if (!self._isRunningTask(TaskId)) return;
            if (err) {
                return ep.emit('error', err);
            }
            // 整理远端 UploadId 列表
            var RemoteUploadIdList = util.filter(data.UploadList, function (item) {
                return item.Key === Key && (!StorageClass || item.StorageClass.toUpperCase() === StorageClass.toUpperCase());
            }).reverse().map(function (item) {
                return item.UploadId || item.UploadID;
            });
            if (RemoteUploadIdList.length) {
                ep.emit('seek_local_avail_upload_id', RemoteUploadIdList);
            } else {
                var uuid = util.getFileUUID(params.Body, params.ChunkSize), LocalUploadIdList;
                if (uuid && (LocalUploadIdList = getUploadId.call(self, uuid))) {
                    util.each(LocalUploadIdList, function (UploadId) {
                        removeUploadId.call(self, UploadId);
                    });
                }
                ep.emit('no_available_upload_id');
            }
        });
    });

    // 开始找可用 UploadId
    ep.emit('get_remote_upload_id_list');

}

// 获取符合条件的全部上传任务 (条件包括 Bucket, Region, Prefix)
function wholeMultipartList(params, callback) {
    var self = this;
    var UploadList = [];
    var sendParams = {
        Bucket: params.Bucket,
        Region: params.Region,
        Prefix: params.Key
    };
    var next = function () {
        self.multipartList(sendParams, function (err, data) {
            if (err) return callback(err);
            UploadList.push.apply(UploadList, data.Upload || []);
            if (data.IsTruncated === 'true') { // 列表不完整
                sendParams.KeyMarker = data.NextKeyMarker;
                sendParams.UploadIdMarker = data.NextUploadIdMarker;
                next();
            } else {
                callback(null, {UploadList: UploadList});
            }
        });
    };
    next();
}

// 获取指定上传任务的分块列表
function wholeMultipartListPart(params, callback) {
    var self = this;
    var PartList = [];
    var sendParams = {
        Bucket: params.Bucket,
        Region: params.Region,
        Key: params.Key,
        UploadId: params.UploadId
    };
    var next = function () {
        self.multipartListPart(sendParams, function (err, data) {
            if (err) return callback(err);
            PartList.push.apply(PartList, data.Part || []);
            if (data.IsTruncated === 'true') { // 列表不完整
                sendParams.PartNumberMarker = data.NextPartNumberMarker;
                next();
            } else {
                callback(null, {PartList: PartList});
            }
        });
    };
    next();
}

// 上传文件分块，包括
/*
 UploadId (上传任务编号)
 AsyncLimit (并发量)，
 SliceList (上传的分块数组)，
 FilePath (本地文件的位置)，
 SliceSize (文件分块大小)
 FileSize (文件大小)
 onProgress (上传成功之后的回调函数)
 */
function uploadSliceList(params, cb) {
    var self = this;
    var TaskId = params.TaskId;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var UploadData = params.UploadData;
    var FileSize = params.FileSize;
    var SliceSize = params.SliceSize;
    var ChunkParallel = Math.min(params.AsyncLimit || self.options.ChunkParallelLimit || 1, 256);
    var Body = params.Body;
    var SliceCount = Math.ceil(FileSize / SliceSize);
    var FinishSize = 0;
    var ServerSideEncryption = params.ServerSideEncryption;
    var needUploadSlices = util.filter(UploadData.PartList, function (SliceItem) {
        if (SliceItem['Uploaded']) {
            FinishSize += SliceItem['PartNumber'] >= SliceCount ? (FileSize % SliceSize || SliceSize) : SliceSize;
        }
        return !SliceItem['Uploaded'];
    });
    var onProgress = params.onProgress;

    Async.eachLimit(needUploadSlices, ChunkParallel, function (SliceItem, asyncCallback) {
        if (!self._isRunningTask(TaskId)) return;
        var PartNumber = SliceItem['PartNumber'];
        var currentSize = Math.min(FileSize, SliceItem['PartNumber'] * SliceSize) - (SliceItem['PartNumber'] - 1) * SliceSize;
        var preAddSize = 0;
        uploadSliceItem.call(self, {
            TaskId: TaskId,
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            SliceSize: SliceSize,
            FileSize: FileSize,
            PartNumber: PartNumber,
            ServerSideEncryption: ServerSideEncryption,
            Body: Body,
            UploadData: UploadData,
            onProgress: function (data) {
                FinishSize += data.loaded - preAddSize;
                preAddSize = data.loaded;
                onProgress({loaded: FinishSize, total: FileSize});
            },
        }, function (err, data) {
            if (!self._isRunningTask(TaskId)) return;
            if (util.isBrowser && !err && !data.ETag) {
                err = 'get ETag error, please add "ETag" to CORS ExposeHeader setting.';
            }
            if (err) {
                FinishSize -= preAddSize;
            } else {
                FinishSize += currentSize - preAddSize;
                SliceItem.ETag = data.ETag;
            }
            asyncCallback(err || null, data);
        });
    }, function (err) {
        if (!self._isRunningTask(TaskId)) return;
        if (err)  return cb(err);
        cb(null, {
            UploadId: UploadData.UploadId,
            SliceList: UploadData.PartList
        });
    });
}

// 上传指定分片
function uploadSliceItem(params, callback) {
    var self = this;
    var TaskId = params.TaskId;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var FileSize = params.FileSize;
    var FileBody = params.Body;
    var PartNumber = params.PartNumber * 1;
    var SliceSize = params.SliceSize;
    var ServerSideEncryption = params.ServerSideEncryption;
    var UploadData = params.UploadData;
    var ChunkRetryTimes = self.options.ChunkRetryTimes + 1;

    var start = SliceSize * (PartNumber - 1);

    var ContentLength = SliceSize;

    var end = start + SliceSize;

    if (end > FileSize) {
        end = FileSize;
        ContentLength = end - start;
    }

    var PartItem = UploadData.PartList[PartNumber - 1];
    Async.retry(ChunkRetryTimes, function (tryCallback) {
        if (!self._isRunningTask(TaskId)) return;
        util.fileSlice(FileBody, start, end, true, function (Body) {
            self.multipartUpload({
                TaskId: TaskId,
                Bucket: Bucket,
                Region: Region,
                Key: Key,
                ContentLength: ContentLength,
                PartNumber: PartNumber,
                UploadId: UploadData.UploadId,
                ServerSideEncryption: ServerSideEncryption,
                Body: Body,
                onProgress: params.onProgress,
            }, function (err, data) {
                if (!self._isRunningTask(TaskId)) return;
                if (err) {
                    return tryCallback(err);
                } else {
                    PartItem.Uploaded = true;
                    return tryCallback(null, data);
                }
            });
        });
    }, function (err, data) {
        if (!self._isRunningTask(TaskId)) return;
        return callback(err, data);
    });
}

// 完成分块上传
function uploadSliceComplete(params, callback) {
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var UploadId = params.UploadId;
    var SliceList = params.SliceList;
    var self = this;
    var ChunkRetryTimes = this.options.ChunkRetryTimes + 1;
    var Parts = SliceList.map(function (item) {
        return {
            PartNumber: item.PartNumber,
            ETag: item.ETag
        };
    });
    // 完成上传的请求也做重试
    Async.retry(ChunkRetryTimes, function (tryCallback) {
        self.multipartComplete({
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            UploadId: UploadId,
            Parts: Parts
        }, tryCallback);
    }, function (err, data) {
        callback(err, data);
    });
}

// 抛弃分块上传任务
/*
 AsyncLimit (抛弃上传任务的并发量)，
 UploadId (上传任务的编号，当 Level 为 task 时候需要)
 Level (抛弃分块上传任务的级别，task : 抛弃指定的上传任务，file ： 抛弃指定的文件对应的上传任务，其他值 ：抛弃指定Bucket 的全部上传任务)
 */
function abortUploadTask(params, callback) {
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var UploadId = params.UploadId;
    var Level = params.Level || 'task';
    var AsyncLimit = params.AsyncLimit;
    var self = this;

    var ep = new EventProxy();

    ep.on('error', function (errData) {
        return callback(errData);
    });

    // 已经获取到需要抛弃的任务列表
    ep.on('get_abort_array', function (AbortArray) {
        abortUploadTaskArray.call(self, {
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            Headers: params.Headers,
            AsyncLimit: AsyncLimit,
            AbortArray: AbortArray
        }, function (err, data) {
            if (err) {
                return callback(err);
            }
            callback(null, data);
        });
    });

    if (Level === 'bucket') {
        // Bucket 级别的任务抛弃，抛弃该 Bucket 下的全部上传任务
        wholeMultipartList.call(self, {
            Bucket: Bucket,
            Region: Region
        }, function (err, data) {
            if (err) {
                return callback(err);
            }
            ep.emit('get_abort_array', data.UploadList || []);
        });
    } else if (Level === 'file') {
        // 文件级别的任务抛弃，抛弃该文件的全部上传任务
        if (!Key) return callback({error: 'abort_upload_task_no_key'});
        wholeMultipartList.call(self, {
            Bucket: Bucket,
            Region: Region,
            Key: Key
        }, function (err, data) {
            if (err) {
                return callback(err);
            }
            ep.emit('get_abort_array', data.UploadList || []);
        });
    } else if (Level === 'task') {
        // 单个任务级别的任务抛弃，抛弃指定 UploadId 的上传任务
        if (!UploadId) return callback({error: 'abort_upload_task_no_id'});
        if (!Key) return callback({error: 'abort_upload_task_no_key'});
        ep.emit('get_abort_array', [{
            Key: Key,
            UploadId: UploadId
        }]);
    } else {
        return callback({error: 'abort_unknown_level'});
    }
}

// 批量抛弃分块上传任务
function abortUploadTaskArray(params, callback) {

    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var AbortArray = params.AbortArray;
    var AsyncLimit = params.AsyncLimit || 1;
    var self = this;

    var index = 0;
    var resultList = new Array(AbortArray.length);
    Async.eachLimit(AbortArray, AsyncLimit, function (AbortItem, callback) {
        var eachIndex = index;
        if (Key && Key !== AbortItem.Key) {
            resultList[eachIndex] = {error: {KeyNotMatch: true}};
            callback(null);
            return;
        }
        var UploadId = AbortItem.UploadId || AbortItem.UploadID;

        self.multipartAbort({
            Bucket: Bucket,
            Region: Region,
            Key: AbortItem.Key,
            Headers: params.Headers,
            UploadId: UploadId
        }, function (err, data) {
            var task = {
                Bucket: Bucket,
                Region: Region,
                Key: AbortItem.Key,
                UploadId: UploadId
            };
            resultList[eachIndex] = {error: err, task: task};
            callback(null);
        });
        index++;

    }, function (err) {
        if (err) {
            return callback(err);
        }

        var successList = [];
        var errorList = [];

        for (var i = 0, len = resultList.length; i < len; i++) {
            var item = resultList[i];
            if (item['task']) {
                if (item['error']) {
                    errorList.push(item['task']);
                } else {
                    successList.push(item['task']);
                }
            }
        }

        return callback(null, {
            successList: successList,
            errorList: errorList
        });
    });
}

// 批量上传文件
function uploadFiles(params, callback) {
    var self = this;

    // 判断多大的文件使用分片上传
    var SliceSize = params.SliceSize === undefined ? self.options.SliceSize : params.SliceSize;

    // 汇总返回进度
    var TotalSize = 0;
    var TotalFinish = 0;
    var onTotalProgress = util.throttleOnProgress.call(self, TotalFinish, params.onProgress);

    // 汇总返回回调
    var unFinishCount = params.files.length;
    var _onTotalFileFinish = params.onFileFinish;
    var resultList = Array(unFinishCount);
    var onTotalFileFinish = function (err, data, options) {
        onTotalProgress(null, true);
        _onTotalFileFinish && _onTotalFileFinish(err, data, options);
        resultList[options.Index] = {
            options: options,
            error: err,
            data: data
        };
        if (--unFinishCount <= 0 && callback) {
            callback(null, {
                files: resultList,
            });
        }
    };

    // 开始处理每个文件
    var taskList = [];
    util.each(params.files, function (fileParams, index) {
        (function () { // 对齐 nodejs 缩进

            var Body = fileParams.Body;
            var FileSize = Body.size || Body.length || 0;
            var fileInfo = {Index: index, TaskId: ''};

            // 更新文件总大小
            TotalSize += FileSize;

            // 整理 option，用于返回给回调
            util.each(fileParams, function (v, k) {
                if (typeof v !== 'object' && typeof v !== 'function') {
                    fileInfo[k] = v;
                }
            });

            // 处理单个文件 TaskReady
            var _onTaskReady = fileParams.onTaskReady;
            var onTaskReady = function (tid) {
                fileInfo.TaskId = tid;
                _onTaskReady && _onTaskReady(tid);
            };
            fileParams.onTaskReady = onTaskReady;

            // 处理单个文件进度
            var PreAddSize = 0;
            var _onProgress = fileParams.onProgress;
            var onProgress = function (info) {
                TotalFinish = TotalFinish - PreAddSize + info.loaded;
                PreAddSize = info.loaded;
                _onProgress && _onProgress(info);
                onTotalProgress({loaded: TotalFinish, total: TotalSize});
            };
            fileParams.onProgress = onProgress;

            // 处理单个文件完成
            var _onFileFinish = fileParams.onFileFinish;
            var onFileFinish = function (err, data) {
                _onFileFinish && _onFileFinish(err, data);
                onTotalFileFinish && onTotalFileFinish(err, data, fileInfo);
            };

            // 添加上传任务
            var api = FileSize >= SliceSize ? 'sliceUploadFile' : 'putObject';
            taskList.push({
                api: api,
                params: fileParams,
                callback: onFileFinish,
            });
        })();
    });
    self._addTasks(taskList);
}

// 分片复制文件
function sliceCopyFile(params, callback) {
    var ep = new EventProxy();

    var self = this;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var CopySource = params.CopySource;
    var m = CopySource.match(/^([^.]+-\d+)\.cos(v6)?\.([^.]+)\.[^/]+\/(.+)$/);
    if (!m) {
        callback({error: 'CopySource format error'});
        return;
    }

    var SourceBucket = m[1];
    var SourceRegion = m[3];
    var SourceKey = decodeURIComponent(m[4]);
    var CopySliceSize = params.SliceSize === undefined ? self.options.CopySliceSize : params.SliceSize;
    CopySliceSize = Math.max(0, Math.min(CopySliceSize, 5 * 1024 * 1024 * 1024));

    var ChunkSize = params.ChunkSize || this.options.CopyChunkSize;
    var ChunkParallel = this.options.CopyChunkParallelLimit;

    var FinishSize = 0;
    var FileSize;
    var onProgress;

    // 分片复制完成，开始 multipartComplete 操作
    ep.on('copy_slice_complete', function (UploadData) {
        self.multipartComplete({
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            UploadId: UploadData.UploadId,
            Parts: UploadData.PartList,
        },function (err, data) {
            if (err) {
                onProgress(null, true);
                return callback(err);
            }
            onProgress({loaded: FileSize, total: FileSize}, true);
            callback(null, data);
        });
    });

    ep.on('get_copy_data_finish',function (UploadData) {
        Async.eachLimit(UploadData.PartList, ChunkParallel, function (SliceItem, asyncCallback) {
            var PartNumber = SliceItem.PartNumber;
            var CopySourceRange = SliceItem.CopySourceRange;
            var currentSize = SliceItem.end - SliceItem.start;
            var preAddSize = 0;

            copySliceItem.call(self, {
                Bucket: Bucket,
                Region: Region,
                Key: Key,
                CopySource: CopySource,
                UploadId: UploadData.UploadId,
                PartNumber: PartNumber,
                CopySourceRange: CopySourceRange,
                onProgress: function (data) {
                    FinishSize += data.loaded - preAddSize;
                    preAddSize = data.loaded;
                    onProgress({loaded: FinishSize, total: FileSize});
                }
            },function (err,data) {
                if (err) {
                    return asyncCallback(err);
                }
                onProgress({loaded: FinishSize, total: FileSize});

                FinishSize += currentSize - preAddSize;
                SliceItem.ETag = data.ETag;
                asyncCallback(err || null, data);
            });
        }, function (err) {
            if (err) {
                onProgress(null, true);
                return callback(err);
            }

            ep.emit('copy_slice_complete', UploadData);
        });
    });

    ep.on('get_file_size_finish', function (SourceHeaders) {
        // 控制分片大小
        (function () {
            var SIZE = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 1024 * 2, 1024 * 4, 1024 * 5];
            var AutoChunkSize = 1024 * 1024;
            for (var i = 0; i < SIZE.length; i++) {
                AutoChunkSize = SIZE[i] * 1024 * 1024;
                if (FileSize / AutoChunkSize <= self.options.MaxPartNumber) break;
            }
            params.ChunkSize = ChunkSize = Math.max(ChunkSize, AutoChunkSize);

            var ChunkCount = Math.ceil(FileSize / ChunkSize);

            var list = [];
            for (var partNumber = 1; partNumber <= ChunkCount; partNumber++) {
                var start = (partNumber - 1) * ChunkSize;
                var end = partNumber * ChunkSize < FileSize ? (partNumber * ChunkSize - 1) : FileSize - 1;
                var item = {
                    PartNumber: partNumber,
                    start: start,
                    end: end,
                    CopySourceRange: "bytes=" + start + "-" + end,
                };
                list.push(item);
            }
            params.PartList = list;
        })();

        var TargetHeader;
        if (params.Headers['x-cos-metadata-directive'] === 'Replaced') {
            TargetHeader = params.Headers;
        } else {
            TargetHeader = SourceHeaders;
        }
        TargetHeader['x-cos-storage-class'] = params.Headers['x-cos-storage-class'] || SourceHeaders['x-cos-storage-class'];
        TargetHeader = util.clearKey(TargetHeader);
        self.multipartInit({
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            Headers: TargetHeader,
        },function (err,data) {
            if (err) {
                return callback(err);
            }
            params.UploadId = data.UploadId;
            ep.emit('get_copy_data_finish', params);
        });
    });

    // 获取远端复制源文件的大小
    self.headObject({
        Bucket: SourceBucket,
        Region: SourceRegion,
        Key: SourceKey,
    },function(err, data) {
        if (err) {
            if (err.statusCode && err.statusCode === 404) {
                callback({ErrorStatus: SourceKey + ' Not Exist'});
            } else {
                callback(err);
            }
            return;
        }

        FileSize = params.FileSize = data.headers['content-length'];
        if (FileSize === undefined || !FileSize) {
            callback({error: 'get Content-Length error, please add "Content-Length" to CORS ExposeHeader setting.'});
            return;
        }

        onProgress = util.throttleOnProgress.call(self, FileSize, params.onProgress);

        // 开始上传
        if (FileSize <= CopySliceSize) {
            if (!params.Headers['x-cos-metadata-directive']) {
                params.Headers['x-cos-metadata-directive'] = 'Copy';
            }
            self.putObjectCopy(params, function (err, data) {
                if (err) {
                    onProgress(null, true);
                    return callback(err);
                }
                onProgress({loaded: FileSize, total: FileSize}, true);
                callback(err, data);
            });
        } else {
            var resHeaders = data.headers;
            var SourceHeaders = {
                'Cache-Control': resHeaders['cache-control'],
                'Content-Disposition': resHeaders['content-disposition'],
                'Content-Encoding': resHeaders['content-encoding'],
                'Content-Type': resHeaders['content-type'],
                'Expires': resHeaders['expires'],
                'x-cos-storage-class': resHeaders['x-cos-storage-class'],
            };
            util.each(resHeaders, function (v, k) {
                var metaPrefix = 'x-cos-meta-';
                if (k.indexOf(metaPrefix) === 0 && k.length > metaPrefix.length) {
                    SourceHeaders[k] = v;
                }
            });
            ep.emit('get_file_size_finish', SourceHeaders);
        }
    });
}

// 复制指定分片
function copySliceItem(params, callback) {
    var TaskId = params.TaskId;
    var Bucket = params.Bucket;
    var Region = params.Region;
    var Key = params.Key;
    var CopySource = params.CopySource;
    var UploadId = params.UploadId;
    var PartNumber = params.PartNumber * 1;
    var CopySourceRange = params.CopySourceRange;

    var ChunkRetryTimes = this.options.ChunkRetryTimes + 1;
    var self = this;

    Async.retry(ChunkRetryTimes, function (tryCallback) {
        self.uploadPartCopy({
            TaskId: TaskId,
            Bucket: Bucket,
            Region: Region,
            Key: Key,
            CopySource: CopySource,
            UploadId: UploadId,
            PartNumber:PartNumber,
            CopySourceRange:CopySourceRange,
            onProgress:params.onProgress,
        },function (err,data) {
            tryCallback(err || null, data);
        })
    }, function (err, data) {
        return callback(err, data);
    });
}

var API_MAP = {
    sliceUploadFile: sliceUploadFile,
    abortUploadTask: abortUploadTask,
    uploadFiles: uploadFiles,
    sliceCopyFile: sliceCopyFile,
};

module.exports.init = function (COS, task) {
    task.transferToTaskMethod(API_MAP, 'sliceUploadFile');
    util.each(API_MAP, function (fn, apiName) {
        COS.prototype[apiName] = util.apiWrapper(apiName, fn);
    });
};


const stringifiedRegexp = /^'|".*'|"$/;

/**
 * If already stringified - return original content
 * @param {Object|Array} content
 * @return {string}
 */
function stringify(content) {
  if (typeof content === 'string' && stringifiedRegexp.test(content)) {
    return content;
  }
  return JSON.stringify(content, null, 2);
}

module.exports = stringify;


const webpackVersion = require('./get-webpack-version');

/**
 * Find nearest module chunk (not sure that is reliable method, but who cares).
 * @see http://stackoverflow.com/questions/43202761/how-to-determine-all-module-chunks-in-webpack
 * @param {NormalModule} module
 * @param {NormalModule[]} modules - webpack 1 compat
 * @return {Chunk|null}
 */
function getModuleChunk(module, modules) {
  let chunks;

  if (webpackVersion.IS_4) {
    chunks = Array.from(module.chunksIterable);
  } else if (parseInt(webpackVersion(), 10) >= 3) {
    chunks = module.mapChunks();
  } else {
    chunks = module.chunks;
  }

  // webpack 1 compat
  const issuer = typeof module.issuer === 'string'
    ? modules.find(m => m.request === module.issuer)
    : module.issuer;

  if (Array.isArray(chunks) && chunks.length > 0) {
    return chunks[chunks.length - 1];
  } else if (issuer) {
    return getModuleChunk(issuer, modules);
  }

  return null;
}

module.exports = getModuleChunk;


const normalizeRule = require('./normalize-rule');
const isWebpack1 = require('./is-webpack-1');

/**
 * webpack 1 compat loader options finder. Returns normalized options.
 * @param {string} loaderPath
 * @param {Object|Rule} rule
 * @return {Object|null}
 */
function getLoaderOptions(loaderPath, rule) {
  let multiRuleProp;

  if (isWebpack1) {
    multiRuleProp = 'loaders';
  } else if (rule.oneOf) {
    multiRuleProp = 'oneOf';
  } else {
    multiRuleProp = 'use';
  }

  const multiRule = typeof rule === 'object' && Array.isArray(rule[multiRuleProp]) ? rule[multiRuleProp] : null;
  let options;

  if (multiRule) {
    const rules = [].concat(...multiRule.map(r => (r.use || r)));
    options = rules.map(normalizeRule).find(r => loaderPath.includes(r.loader)).options;
  } else {
    options = normalizeRule(rule).options;
  }

  return options;
}

module.exports = getLoaderOptions;


const { parseQuery } = require('loader-utils');
const isWebpack1 = require('./is-webpack-1');

/**
 * webpack 1 compat rule normalizer
 * @param {string|Rule} rule (string - webpack 1, Object - webpack 2)
 * @return {Object<loader: string, options: Object|null>}
 */
function normalizeRule(rule) {
  if (!rule) {
    throw new Error('Rule should be string or object');
  }

  let data;

  if (typeof rule === 'string') {
    const parts = rule.split('?');
    data = {
      loader: parts[0],
      options: parts[1] ? parseQuery(`?${parts[1]}`) : null
    };
  } else {
    const options = isWebpack1 ? rule.query : rule.options;
    data = {
      loader: rule.loader,
      options: options || null
    };
  }

  return data;
}

module.exports = normalizeRule;


import * as hp from 'helper-js'
import * as th from 'tree-helper'
import Cache, {
  attachCache
} from '../plugins/Cache'
import * as vf from 'vue-functions'
import getTreeByPoint from './temporarily-fix-overlapping-tree-issue'

// actions for drag placeholder
// 对 drag placeholder进行的操作
const targets = {
  'nothing': info => {},
  'after': (info) => {
    insertDplhAfterTo(info.dplh, info.targetNode, info)
  },
  'before': (info) => {
    if (isNodeDroppable(info.targetNode.parent)) {
      th.insertBefore(info.dplh, info.targetNode)
    } else {
      insertDplhAfterTo(info.dplh, info.targetNode.parent, info)
    }
  },
  'append': (info) => {
    if (isNodeDroppable(info.targetNode)) {
      th.appendTo(info.dplh, info.targetNode)
      if (!info.targetNode.open) info.store.toggleOpen(info.targetNode)
    } else {
      insertDplhAfterTo(info.dplh, info.targetNode, info)
    }
  },
  'prepend': (info) => {
    if (isNodeDroppable(info.targetNode)) {
      th.prependTo(info.dplh, info.targetNode)
      if (!info.targetNode.open) info.store.toggleOpen(info.targetNode)
    } else {
      insertDplhAfterTo(info.dplh, info.targetNode, info)
    }
  },
  'after target parent': (info) => {
    insertDplhAfterTo(info.dplh, info.targetNode.parent, info)
  },
  // append to prev sibling
  'append prev': (info) => {
    if (isNodeDroppable(info.targetPrev)) {
      th.appendTo(info.dplh, info.targetPrev)
      if (!info.targetPrev.open) info.store.toggleOpen(info.targetPrev)
    } else {
      insertDplhAfterTo(info.dplh, info.targetPrev, info)
    }
  },
  // append to current tree
  'append current tree': (info) => {
    if (isNodeDroppable(info.currentTree.rootData)) {
      th.appendTo(info.dplh, info.currentTree.rootData)
    }
  },
}

function insertDplhAfterTo(dplh, targetNode, info) {
  if (!targetNode) {
    return false
  } else {
    const closest = findParent(targetNode, node => node.parent && isNodeDroppable(node.parent))
    if (closest) {
      th.insertAfter(dplh, closest)
    } else {
      return false
    }
  }
  return true
}

export function isNodeDraggable(node) {
  if (!draggableIds.hasOwnProperty(node._id)) {
    let r
    if (node.hasOwnProperty('draggable')) {
      r = node.draggable
    } else if (node.parent) {
      r = isNodeDraggable(node.parent)
    } else {
      r = true
    }
    draggableIds[node._id] = r
  }
  return draggableIds[node._id]
}

export function isNodeDroppable(node) {
  if (!droppableIds.hasOwnProperty(node._id)) {
    let r
    if (node.hasOwnProperty('droppable')) {
      r = node.droppable
    } else if (node.parent) {
      r = isNodeDroppable(node.parent)
    } else {
      r = true
    }
    droppableIds[node._id] = r
  }
  return droppableIds[node._id]
}

// find child, excluding dragging node default
function findChild(info, children, handler, reverse) {
  const len = children.length
  if (reverse) {
    for (let i = len - 1; i >= 0; i--) {
      const item = children[i]
      // excluding dragging node
      if (item !== info.node) {
        if (handler(item, i)) {
          return item
        }
      }
    }
  } else {
    for (let i = 0; i < len; i++) {
      const item = children[i]
      // excluding dragging node
      if (item !== info.node) {
        if (handler(item, i)) {
          return item
        }
      }
    }
  }
}
// start from node self
function findParent(node, handle) {
  let current = node
  while (current) {
    if (handle(current)) {
      return current
    }
    current = current.parent
  }
}
const rules = {
  // 另一节点存在
  'targetNode existed': info => info.targetNode,
  // 另一节点是拖动占位节点
  'targetNode is placeholder': info => info.targetNode.isDragPlaceHolder,
  // 另一节点在最上面
  'targetNode at top': info => info.targetAtTop,
  // 另一节点在最下面
  'targetNode at bottom': info => info.targetAtBottom,
  // 另一节点是根节点第二个子
  'targetNode is the second child of root': info => info.currentTreeRootSecondChildExcludingDragging === info.targetNode,
  // 拖动点坐标在任一树中, 同时, 起始树要可拖出, 当前树要可拖入
  'currentTree existed': info => info.currentTree,
  // 当前树为空(不包括占位节点)
  'currentTree empty': info => !findChild(info, info.currentTree.rootData.children, v => v),
  // 占位节点存在
  'placeholder existed': info => info.dplhEl,
  // 占位节点在当前树中
  'placeholder in currentTree': info => info.dplhElInCurrentTree,
  // 占位节点在最上面
  'placeholder at top': info => info.dplhAtTop,
  // 另一节点是打开的
  'targetNode is open': info => info.targetNode.open,
  // 另一节点有子(不包括占位节点)
  'targetNode has children excluding placeholder': info => findChild(info, info.targetNode.children, v => v !== info.dplh),
  // 另一节点是第一个节点
  'targetNode is 1st child': info => findChild(info, info.targetNode.parent.children, v => v) === info.targetNode,
  // 另一节点是最后节点
  'targetNode is last child': info => findChild(info, info.targetNode.parent.children, v => v, true) === info.targetNode,
  // 当前位置在另一节点inner垂直中线上
  'on targetNode middle': info => info.offset.y <= info.tiMiddleY,
  // 当前位置在另一节点inner左边
  'at left': info => info.offset.x < info.tiOffset.x,
  // 当前位置在另一节点innner indent位置右边
  'at indent right': info => info.offset.x > info.tiOffset.x + info.currentTree.indent,
}

// convert rule output to Boolean
for (const key of Object.keys(rules)) {
  const old = rules[key]
  rules[key] = (...args) => Boolean(old(...args))
}

let prevTree
let droppableIds = {}
let draggableIds = {}
// context is vm
export default function autoMoveDragPlaceHolder(draggableHelperInfo) {
  const trees = this.store.trees
  const dhStore = draggableHelperInfo.store
  // make info
  const info = {
    event: draggableHelperInfo.event,
    el: dhStore.el,
    vm: this,
    node: this.data,
    store: this.store,
    dplh: this.store.dplh,
    draggableHelperData: {
      opt: draggableHelperInfo.options,
      store: dhStore,
    },
  }
  //
  attachCache(info, new Cache(), {
    // dragging node coordinate
    // 拖动中的节点相关坐标
    nodeInnerEl() {
      return this.el.querySelector('.tree-node-inner')
    },
    offset() {
      return hp.getOffset(this.nodeInnerEl)
    }, // left top point
    offset2() {
      return {
        x: this.offset.x + this.nodeInnerEl.offsetWidth,
        y: this.offset.y + this.nodeInnerEl.offsetHeight
      }
    }, // right bottom point
    offsetToViewPort() {
      const r = this.nodeInnerEl.getBoundingClientRect()
      r.x = r.left
      r.y = r.top
      return r
    },
    // tree
    currentTree() {
      // const currentTree = trees.find(tree => hp.isOffsetInEl(this.offset.x, this.offset.y, tree.$el))
      const currentTree = getTreeByPoint(this.offsetToViewPort.x, this.offsetToViewPort.y, trees)
      if (currentTree) {
        const dragStartTree = this.store
        let treeChanged
        if (prevTree == null) {
          prevTree = dragStartTree
          treeChanged = true
        }
        if (prevTree !== currentTree) {
          if (!vf.isPropTrue(dragStartTree.crossTree) || !vf.isPropTrue(currentTree.crossTree)) {
            return
          }
          prevTree = currentTree
          treeChanged = true
        }
        if (!vf.isPropTrue(currentTree.droppable)) {
          return
        }
        return currentTree
      }
    },
    currentTreeRootEl() {
      return document.getElementById(this.currentTree.rootData._id)
    },
    currentTreeRootOf4() {
      return getOf4(this.currentTreeRootEl, this.currentTree.space)
    },
    // the second child of currentTree root, excluding dragging node
    currentTreeRootSecondChildExcludingDragging() {
      return this.currentTree.rootData.children.slice(0, 3).filter(v => v !== this.node)[1]
    },
    // placeholder
    dplhEl() {
      return document.getElementById(this.dplh._id)
    },
    dplhElInCurrentTree() {
      return Boolean(this.currentTree.$el.querySelector(`#${this.dplh._id}`))
    },
    dplhOf4() {
      return getOf4(this.dplhEl, this.currentTree.space)
    },
    dplhAtTop() {
      return Math.abs(this.dplhOf4.y - this.currentTreeRootOf4.y) < 5
    },
    targetAtTop() {
      return Math.abs(this.tiOf4.y - this.currentTreeRootOf4.y) < 5
    },
    targetAtBottom() {
      return Math.abs(this.tiOf4.y2 - this.currentTreeRootOf4.y2) < 5
    },
    // most related node
    // 最相关的另一个节点
    targetNode() {
      const {
        currentTree
      } = this
      if (!currentTree) {
        throw 'no currentTree'
      }
      //
      const {
        x,
        y
      } = this.offset
      let currentNode = currentTree.rootData
      while (true) {
        let children = currentNode.children
        if (!children) {
          break
        }
        if (this.node.parent === currentNode) {
          // dragging node is in currentNode children, remove it first
          children = children.slice()
          children.splice(children.indexOf(this.node), 1)
        }
        if (children.length === 0) {
          break
        }
        const t = hp.binarySearch(children, (node) => {
          const el = document.getElementById(node._id)
          const ty = hp.getOffset(el).y
          const ty2 = ty + el.offsetHeight + currentTree.space
          if (ty2 < y) {
            return -1
          } else if (ty <= y) {
            return 0
          } else {
            return 1
          }
        }, null, null, true)
        if (t.hit) {
          currentNode = t.value
        } else {
          if (t.bigger) {
            currentNode = children[t.index - 1]
          } else {
            currentNode = t.value
          }
        }
        if (!currentNode) {
          currentNode = children[0]
          break
        }
        if (!currentNode) {
          break
        }
        const innerEl = document.getElementById(currentNode._id).querySelector('.tree-node-inner')
        const of = getOf4(innerEl, currentTree.space)
        if ( of .y <= y && y <= of .y2) {
          break
        }
      }
      return currentNode
    },
    targetNodeEl() {
      return document.getElementById(this.targetNode._id)
    },
    // targetNodeInnerElOffset
    tiInnerEl() {
      return this.targetNodeEl.querySelector('.tree-node-inner')
    },
    tiOffset() {
      return hp.getOffset(this.tiInnerEl)
    },
    tiOf4() {
      return getOf4(this.tiInnerEl, this.currentTree.space)
    },
    tiMiddleY() {
      return this.tiOffset.y + this.tiInnerEl.offsetHeight / 2
    },
    //
    targetPrevEl() {
      // tree node 之间不要有其他元素, 否则这里会获取到错误的元素
      let r = this.targetNodeEl.previousSibling
      if (hp.hasClass(r, 'dragging')) {
        r = r.previousSibling
      }
      return r
    },
    targetPrev() {
      const id = this.targetPrevEl.getAttribute('id')
      return this.currentTree.getNodeById(id)
    },
  })
  // attachCache end

  // decision start =================================
  const executedRuleCache = {}
  // exec rule
  const exec = (ruleId) => {
  if (!executedRuleCache.hasOwnProperty(ruleId)) {
    let r
    try {
      r = rules[ruleId](info)
    } catch (e) {
      r = e
      try {
        if (process.env.DEVELOPE_SELF) {
          // only visible when develop its self
          console.warn(`failed to execute rule '${ruleId}'`, e);
        }
      } catch (e2) {}
    }
    executedRuleCache[ruleId] = r
  }
  return executedRuleCache[ruleId]
  }
  if (exec('currentTree existed') === true){
    if (exec('targetNode is placeholder') === false){
      if (exec('targetNode is the second child of root') === true){
        if (exec('targetNode has children excluding placeholder') === false){
          if (exec('on targetNode middle') === true){
            targets['before'](info)
          }
          else if (exec('on targetNode middle') === false){
            if (exec('at indent right') === true){
              targets['append'](info)
            }
            else if (exec('at indent right') === false){
              targets['after'](info)
            }
          }
        }
        else if (exec('targetNode has children excluding placeholder') === true){
          targets['prepend'](info)
        }
      }
      else if (exec('targetNode is the second child of root') === false){
        if (exec('currentTree empty') === false){
          if (exec('targetNode at top') === true){
            if (exec('placeholder in currentTree') === true){
              if (exec('targetNode has children excluding placeholder') === false){
                if (exec('on targetNode middle') === false){
                  if (exec('at indent right') === false){
                    targets['after'](info)
                  }
                  else if (exec('at indent right') === true){
                    targets['append'](info)
                  }
                }
                else if (exec('on targetNode middle') === true){
                  targets['before'](info)
                }
              }
              else if (exec('targetNode has children excluding placeholder') === true){
                if (exec('on targetNode middle') === false){
                  targets['prepend'](info)
                }
                else if (exec('on targetNode middle') === true){
                  targets['before'](info)
                }
              }
            }
            else if (exec('placeholder in currentTree') === false){
              targets['before'](info)
            }
          }
          else if (exec('targetNode at top') === false){
            if (exec('targetNode at bottom') === false){
              if (exec('placeholder at top') === true){
                targets['prepend'](info)
              }
              else if (exec('placeholder at top') === false){
                if (exec('targetNode has children excluding placeholder') === true){
                  targets['prepend'](info)
                }
                else if (exec('targetNode has children excluding placeholder') === false){
                  if (exec('targetNode is 1st child') === false){
                    if (exec('targetNode is last child') === false){
                      if (exec('on targetNode middle') === true){
                        if (exec('at indent right') === true){
                          targets['append'](info)
                        }
                        else if (exec('at indent right') === false){
                          targets['after'](info)
                        }
                      }
                      else if (exec('on targetNode middle') === false){
                        if (exec('at indent right') === true){
                          targets['append'](info)
                        }
                        else if (exec('at indent right') === false){
                          targets['after'](info)
                        }
                      }
                    }
                    else if (exec('targetNode is last child') === true){
                      if (exec('at indent right') === true){
                        targets['append'](info)
                      }
                      else if (exec('at indent right') === false){
                        targets['after'](info)
                      }
                    }
                  }
                  else if (exec('targetNode is 1st child') === true){
                    if (exec('targetNode is last child') === true){
                      targets['append'](info)
                    }
                    else if (exec('targetNode is last child') === false){
                      if (exec('on targetNode middle') === false){
                        if (exec('at indent right') === false){
                          targets['after'](info)
                        }
                        else if (exec('at indent right') === true){
                          targets['append'](info)
                        }
                      }
                      else if (exec('on targetNode middle') === true){
                        if (exec('at indent right') === false){
                          targets['after'](info)
                        }
                        else if (exec('at indent right') === true){
                          targets['append'](info)
                        }
                      }
                    }
                  }
                }
              }
            }
            else if (exec('targetNode at bottom') === true){
              if (exec('placeholder in currentTree') === true){
                if (exec('on targetNode middle') === false){
                  if (exec('at indent right') === true){
                    targets['append'](info)
                  }
                  else if (exec('at indent right') === false){
                    targets['after'](info)
                  }
                }
                else if (exec('on targetNode middle') === true){
                  targets['append'](info)
                }
              }
              else if (exec('placeholder in currentTree') === false){
                targets['append'](info)
              }
            }
          }
        }
        else if (exec('currentTree empty') === true){
          targets['append current tree'](info)
        }
      }
    }
    else if (exec('targetNode is placeholder') === true){
      if (exec('targetNode at bottom') === false){
        if (exec('targetNode is the second child of root') === false){
          if (exec('targetNode is 1st child') === true){
            if (exec('targetNode is last child') === false){
              targets['nothing'](info)
            }
            else if (exec('targetNode is last child') === true){
              if (exec('on targetNode middle') === false){
                if (exec('at left') === true){
                  targets['after target parent'](info)
                }
                else if (exec('at left') === false){
                  targets['nothing'](info)
                }
              }
              else if (exec('on targetNode middle') === true){
                if (exec('at left') === true){
                  targets['after target parent'](info)
                }
                else if (exec('at left') === false){
                  targets['nothing'](info)
                }
              }
            }
          }
          else if (exec('targetNode is 1st child') === false){
            if (exec('targetNode is last child') === true){
              if (exec('on targetNode middle') === true){
                if (exec('at left') === true){
                  targets['after target parent'](info)
                }
                else if (exec('at left') === false){
                  if (exec('at indent right') === true){
                    targets['append prev'](info)
                  }
                  else if (exec('at indent right') === false){
                    targets['nothing'](info)
                  }
                }
              }
              else if (exec('on targetNode middle') === false){
                if (exec('at left') === true){
                  targets['after target parent'](info)
                }
                else if (exec('at left') === false){
                  if (exec('at indent right') === true){
                    targets['append prev'](info)
                  }
                  else if (exec('at indent right') === false){
                    targets['nothing'](info)
                  }
                }
              }
            }
            else if (exec('targetNode is last child') === false){
              if (exec('on targetNode middle') === true){
                if (exec('at left') === true){
                  targets['nothing'](info)
                }
                else if (exec('at left') === false){
                  if (exec('at indent right') === true){
                    targets['append prev'](info)
                  }
                  else if (exec('at indent right') === false){
                    targets['nothing'](info)
                  }
                }
              }
              else if (exec('on targetNode middle') === false){
                if (exec('at left') === true){
                  targets['nothing'](info)
                }
                else if (exec('at left') === false){
                  if (exec('at indent right') === true){
                    targets['append prev'](info)
                  }
                  else if (exec('at indent right') === false){
                    targets['nothing'](info)
                  }
                }
              }
            }
          }
        }
        else if (exec('targetNode is the second child of root') === true){
          if (exec('on targetNode middle') === true){
            if (exec('at indent right') === true){
              targets['append prev'](info)
            }
            else if (exec('at indent right') === false){
              targets['nothing'](info)
            }
          }
          else if (exec('on targetNode middle') === false){
            if (exec('at indent right') === true){
              targets['append prev'](info)
            }
            else if (exec('at indent right') === false){
              targets['nothing'](info)
            }
          }
        }
      }
      else if (exec('targetNode at bottom') === true){
        if (exec('targetNode is 1st child') === true){
          if (exec('on targetNode middle') === false){
            if (exec('at left') === true){
              targets['after target parent'](info)
            }
            else if (exec('at left') === false){
              targets['nothing'](info)
            }
          }
          else if (exec('on targetNode middle') === true){
            if (exec('at left') === false){
              targets['nothing'](info)
            }
            else if (exec('at left') === true){
              targets['after target parent'](info)
            }
          }
        }
        else if (exec('targetNode is 1st child') === false){
          if (exec('on targetNode middle') === false){
            if (exec('at left') === true){
              targets['after target parent'](info)
            }
            else if (exec('at left') === false){
              if (exec('at indent right') === true){
                targets['append prev'](info)
              }
              else if (exec('at indent right') === false){
                targets['nothing'](info)
              }
            }
          }
          else if (exec('on targetNode middle') === true){
            if (exec('at left') === true){
              targets['after target parent'](info)
            }
            else if (exec('at left') === false){
              if (exec('at indent right') === true){
                targets['append prev'](info)
              }
              else if (exec('at indent right') === false){
                targets['nothing'](info)
              }
            }
          }
        }
      }
    }
  }
  else if (exec('currentTree existed') === false){
    targets['nothing'](info)
  }
  // decision end =================================

  //
}

function getOf4(el, space) {
  const r = hp.getOffset(el)
  r.x2 = r.x + el.offsetWidth
  r.y2 = r.y + el.offsetHeight + space
  return r
}

autoMoveDragPlaceHolder.dragStart = function dragStart() {}
autoMoveDragPlaceHolder.dragEnd = function dragEnd() {
  prevTree = null
  droppableIds = {}
  draggableIds = {}
}