'use strict';

/**
 *
 *
 * @author Zhong
 * @date 2019/6/20
 * @version
 */
const XML_EXPORT_BASE = (() => {
    // 自检提示的开始记号，区分提示属于哪一部分的类型（eg: 招标、控制价），便于后续做提示的合并去重
    const HINT_START = '--start--';
    // 属性类型
    const TYPE = {
        DATE: 1,        //日期类型YYYY-MM-DD
        DATE_TIME: 2,   //日期类型YYY-MM-DDTHH:mm:ss
        INT: 3,         //整数类型
        DECIMAL: 4,     //数值类型，不限制小数位数
        NUM2: 5,        //数值类型2：最多两位小数
        BOOL: 6         //布尔型
    };
    // 需要特殊处理的属性类型默认空值（当一个值为undefined、null的时候，默认给赋什么值）
    const DEFAULT_VALUE = {
        [TYPE.INT]: '0',
        [TYPE.DECIMAL]: '0',
        [TYPE.NUM2]: '0',
        [TYPE.BOOL]: 'false'
    };
    // 空白字符处理
    const WHITE_SPACE = {
        COLLAPSE: 1 //移除所有空白字符（换行、回车、空格以及制表符会被替换为空格，开头和结尾的空格会被移除，而多个连续的空格会被缩减为一个单一的空格）
    };
    // 计税方法
    const TAX_TYPE = {
        '1': '一般计税法',
        '2': '简易计税法',
    };
    // 承包人材料调整类型
    const ADJUST_TYPE = {
        info: 'priceInfo', // 造价信息差额调整法
        coe: 'priceCoe' // 价格指数调整法
    };
    // 加载数据间隔，减少服务器压力
    const TIMEOUT_TIME = 400;
    // 导出粒度
    const GRANULARITY = {
        PROJECT: 1,         //导出建设项目
        ENGINEERING: 2,     //导出单项工程
        TENDER: 3           //导出单位工程
    };
    // 导出的文件类型选项
    const EXPORT_KIND = {
        Tender: 1,  //投标
        Bid: 2,     //招标
        Control: 3  //控制价
    };
    const EXPORT_KIND_NAME = {
        1: '投标',
        2: '招标',
        3: '控制价'
    };
    // 配置项
    const CONFIG = Object.freeze({
        HINT_START,
        TYPE,
        WHITE_SPACE,
        TAX_TYPE,
        ADJUST_TYPE,
        TIMEOUT_TIME,
        GRANULARITY,
        EXPORT_KIND,
        EXPORT_KIND_NAME
    });

    // 缓存项 不需要的时候需要清空
    let _cache = {
        // 失败列表
        failList: [],
        // 项目数据（不包含详细数据，项目管理数据）
        projectData: {},
        // 当前导出类型，默认投标
        exportKind: EXPORT_KIND.Tender,
        // 记录拉取的单位工程项目详细数据，导出的时候，可能会导出多个文件，只有导出第一个文件的时候需要请求数据
        tenderDetailMap: {}
    };
    // 返回缓存项
    function getItem(key) {
        return _cache[key] || null;
    }
    // 设置缓存项
    function setItem(key, value) {
        // 与原数据是同类型的数据才可设置成功
        if (_cache[key] &&
            Object.prototype.toString.call(_cache[key]) ===
            Object.prototype.toString.call(value)) {
            _cache[key] = value;
        }
    }
    // 清空缓存项
    function clear() {
        _cache.failList = [];
        _cache.projectData = {};
        _cache.exportKind = EXPORT_KIND.Tender;
        _cache.tenderDetailMap = {};
    }
    const CACHE = Object.freeze({
        getItem,
        setItem,
        clear
    });

    /*
     * 定义不设置一个Node方法统一进入的原因：模板化比较直观，不分开定义节点的话，调用传参也很麻烦而且不直观。
     * 一个节点对应一个构造方法，方便调整配置、方便其他版本开发、接手的人看起来更直观
     * @param  {String}name 节点名
     *         {Array}attrs 节点属性数据
     *         {String}alias 别名，有些节点名称是英文，但是提示的时候需要显示中文的时候用
     * @return {void}
     * */
    function Element(name, attrs, alias) {
        this.name = name;
        const hintName = alias || name;
        const checkData = check(hintName, attrs);
        this.fail = checkData.failHints;
        this.attrs = checkData.filterAttrs;
        handleXMLEntity(this.attrs);
        this.children = [];
        _cache.failList.push(...this.fail);
    }
    /*
     * xml字符实体的处理，这些特殊字符不处理会导致xml文件格式出错：""、<>、&
     * 要先处理&amp
     * */
    let _xmlEntity = {
        '&': '&amp;',
        '\n': '&#xA;',
        '"': '&quot;',
        '\'': '&apos;',
        '<': '&lt;',
        '>': '&gt;'
    };
    // 对每个元素的所有属性值进行特殊字符处理
    function handleXMLEntity(attrs) {
        for (let attr of attrs) {
            if (!attr.value) {
                continue;
            }
            for (let [key, value] of Object.entries(_xmlEntity)) {
                attr.value = attr.value.replace(new RegExp(key, 'g'), value);
            }
        }
    }
    // 获取处理实体字符后的数据
    function getParsedData(arr) {
        return arr.map(data => {
            for (let [key, value] of Object.entries(_xmlEntity)) {
                data = data.replace(new RegExp(key, 'g'), value);
            }
            return data;
        });
    }
    /*
     * 检查
     * 创建节点时检查节点的数据
     * 1.长度限制minLen，maxLen
     * 2.值的限制，固定范围：enumeration
     * @param  {String}eleName 节点名称
     *         {Array}datas 需要检查的属性数据
     * @return {Object} failHints没通过的属性提示 filterAttrs过滤后的属性数据(失败提示在属性是必须的时候才提示，如果该属性失败了，但是是非必要属性，那么该属性不显示)
     * */
    function check(eleName, datas) {
        let rst = { failHints: [], filterAttrs: [] };
        let dateReg = /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))/;
        for (let data of datas) {
            const isHasValue = hasValue(data.value);
            // 某属性是非必须的，并且设置了mustHasValue，意味着必须要有值才输出该属性
            if (!data.required && data.mustHasValue && !isHasValue) {
                continue;
            }
            // 值统一转换成String，并且处理各类型属性空值时的默认取值
            data.value = !isHasValue
                ? DEFAULT_VALUE[data.type]
                    ? DEFAULT_VALUE[data.type]
                    : ''
                : String(data.value);
            if (data.whiteSpace && data.whiteSpace === WHITE_SPACE.COLLAPSE) {  //处理空格相关
                data.value = data.value.replace(/[\r\n\t]/g, ' ');
                data.value = data.value.trim();
                data.value = data.value.replace(/\s{1,}/g, ' ');
            }
            let isFail = false;
            let tempFail = '';
            // 提示的名称需要处理，有的接口xml属性为英文，需要显示其展示名称
            const name = data.dName || data.name;
            if (data.minLen && data.value.length < data.minLen) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}字符数不可小于${data.minLen}个。`
                    : `${eleName}-“${name}”字符数不可小于${data.minLen}个。`;
            } else if (data.maxLen && data.value.length > data.maxLen) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}字符数不可大于${data.maxLen}个。`
                    : `${eleName}-“${name}”字符数不可大于${data.maxLen}个。`;
            } else if (data.enumeration && !data.enumeration.includes(data.value)) {
                isFail = true;
                let enumerationHint = data.enumerationHint
                    ? data.enumerationHint.join(';')
                    : data.enumeration.join(';');
                tempFail = data.failHint
                    ? `${data.failHint}只能从“${enumerationHint}”中选择。`
                    : `${eleName}-“${name}”只能从“${enumerationHint}”中选择。`;
            } else if (data.type && data.type === TYPE.DATE && !dateReg.test(data.value)) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}日期格式必须是YYYY-MM-DD。`
                    : `${eleName}-“${name}”日期格式必须是YYYY-MM-DD。`;
            } else if (data.type && data.type === TYPE.INT && !Number.isInteger(parseFloat(data.value))) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}必须为整数。`
                    : `${eleName}-“${name}”必须为整数。`;
            } else if (data.type && data.type === TYPE.DECIMAL && isNaN(parseFloat(data.value))) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}必须为数值。`
                    : `${eleName}-“${name}”必须为数值。`;
            } else if (data.type && data.type === TYPE.NUM2) {
                let v = parseFloat(data.value);
                if (isNaN(v)) {
                    isFail = true;
                    tempFail = data.failHint
                        ? `${data.failHint}必须为数值。`
                        : `${eleName}-“${name}”必须为数值。`;
                } else if (!data.value.length || (data.value.split('.').length > 1 && data.value.split('.')[1].length > 2)) {
                    isFail = true;
                }
            } else if (data.type && data.type === TYPE.BOOL && !['true', 'false'].includes(String(data.value))) {
                isFail = true;
                tempFail = data.failHint
                    ? `${data.failHint}必须为true或false。`
                    : `${eleName}-“${name}”必须为true或false。`;
            }
            if (!isFail || data.required) {
                rst.filterAttrs.push(data);
            }
            if (isFail && data.required && tempFail) {
                rst.failHints.push(tempFail);
            }
        }
        return rst;
    }
    // 提取各导出类型的自检数据
    function _extractHintParts(failList) {
        let rst = [],
            curPart;
        for (let hint of failList) {
            if (hint === HINT_START) {
                curPart = [];
                rst.push(curPart);
                continue;
            }
            curPart.push(hint);
        }
        return rst;
    }
    // 自检提示去重
    function deWeightHints(failList) {
        let rst = [];
        let hintParts = _extractHintParts(failList);
        // 建设项目提示文本
        let rootHints = [],
            // 单位工程提示文本映射
            tenderMap = {},
            reg = /^<span style="font-weight: bold">单位工程/;
        for (let hintPart of hintParts) {
            // 单位工程xxx提示
            let curTenderHint;
            // 提取建设项目提示、各自单位工程提示
            for (let hint of hintPart) {
                if (reg.test(hint)) {
                    curTenderHint = hint;
                    if (!tenderMap[curTenderHint]) {
                        tenderMap[curTenderHint] = [];
                    }
                    continue;
                }
                if (curTenderHint) {
                    tenderMap[curTenderHint].push(hint);
                } else {
                    rootHints.push(hint);
                }
            }
        }
        // 建设项目提示去重，放入结果中
        rootHints = [...new Set(rootHints)];
        rst.push(...rootHints);
        // 单位工程提示放入结果中
        for (let tenderHint in tenderMap) {
            rst.push(tenderHint);
            // 单位工程提示去重
            let tenderHints = [...new Set(tenderMap[tenderHint])];
            rst.push(...tenderHints);
        }
        return rst;
    }
    // 等待一段时间
    function setTimeoutSync(handle, time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (handle && typeof handle === 'function') {
                    handle();
                }
                resolve();
            }, time);
        });
    }
    // v是否定义了，不为undefined和null
    function isDef(v) {
        return typeof v !== 'undefined' && v !== null;
    }
    function /*  */hasValue(v) {
        // v是否有值，不为undefined、null、''
        return typeof v !== 'undefined' && v !== null && v !== '';
    }
    /*
     * 将节点属性数据(attr数组)转换成简单key-value数据
     * @param  {Object}ele 元素节点数据Element实例
     * @return {Object}
     * */
    function getPlainAttrs(ele) {
        const obj = {};
        ele.attrs.forEach(attr => obj[attr.name] = attr.value);
        return obj;
    }
    /*
     * 从fees数组中获取相关费用
     * @param  {Array}fees 费用数组
     *         {String}feeFields 费用字段
     * @return {Number}
     * @example getFee(source.fees, 'common.totalFee')
     * */
    function getFee(fees, feeFields) {
        if (!Array.isArray(fees)) {
            return 0;
        }
        let fields = feeFields.split('.');
        let fee = fees.find(data => data.fieldName === fields[0]);
        if (!fee) {
            return 0;
        }
        return fee[fields[1]] || 0;
    }
    // 获取节点的汇总价格
    function getAggregateFee(nodes) {
        let total = nodes.reduce((acc, node) => {
            const price = getFee(node.data.fees, 'common.totalFee');
            return acc += price;
        }, 0);
        return scMathUtil.roundTo(total, -2);
    }
    // 获取固定类别行的费用
    function getFeeByFlag(items, flag, feeFields) {
        const node = items.find(node => node.getFlag() === flag);
        return node ? getFee(node.data.fees, feeFields) : '0';
    }
    /*
     * 根据key获取对应的基本信息、工程特征数据
     * @param  {Array}data
     *         {String}key
     * @return {String}
     * @example getValueByKey(source.basicInformation, 'projectScale')
     * */
    function getValueByKey(items, key) {
        for (const item of items) {
            if (item.key === key) {
                return item.value;
            }
            if (item.items && item.items.length) {
                const value = getValueByKey(item.items, key);
                if (value) {
                    return value;
                }
            }
        }
        return '';
    }
    // 获取关联材料
    function getRelGLJ(allGLJs, gljId) {
        return allGLJs.find(glj => glj.id === gljId);
    }
    // 随机生成机器信息码：CPU信息;硬盘序列号;mac地址;
    // 保存在localStorage中
    function generateHardwareId() {
        const hardwareCacheId = window.localStorage.getItem('hardwareId');
        if (hardwareCacheId) {
            return hardwareCacheId;
        }
        const charList = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
            'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
            'V', 'W', 'X', 'Y', 'Z'];
        function generateCpuId() {
            let id = '';
            let count = 16;
            while (count--) {
                const randomIdx = parseInt(Math.random() * 16);
                id += charList[randomIdx];
            }
            return id;
        }
        function generateDiskId() {
            let id = '';
            let count = 8;
            while (count--) {
                const randomIdx = parseInt(Math.random() * 36);
                id += charList[randomIdx];
            }
            return id;
        }
        function generateMacId() {
            const idList = [];
            let outerCount = 6;
            while (outerCount--) {
                let tempId = '';
                let innerCount = 2;
                while (innerCount--) {
                    const randomIdx = parseInt(Math.random() * 16);
                    tempId += charList[randomIdx];
                }
                idList.push(tempId);
            }
            return idList.join('-');
        }
        const cpuId = generateCpuId();
        const diskId = generateDiskId();
        const macId = generateMacId();
        const hardwareId = [cpuId, diskId, macId].join(';');
        window.localStorage.setItem('hardwareId', hardwareId);
        return hardwareId;
    }
    // 数组打平成对象
    function arrayToObj(arr) {
        let rst = {};
        for (let data of arr) {
            rst[data.key] = data.value;
        }
        return rst;
    }
    /*
     * 检测层数
     * @param  {Number}maxDepth(最大深度)
     *         {Object}node(需要检测的清单树节点)
     * @return {Boolean}
     * */
    function validDepth(maxDepth, node) {
        let nodeDepth = node.depth();
        let allNodes = node.getPosterity();
        //检测相对深度
        for (let n of allNodes) {
            let relativeDepth = n.depth() - nodeDepth;
            if (relativeDepth > maxDepth) {
                return false;
            }
        }
        return true;
    }
    /*
     * 检测唯一性，有些属性在规定的数据范围内唯一
     * @param  {Object}constraints(约束池)
     *         {All}data(检测的数据)
     *         {String}hint(提示已存在的内容)
     *         {String}subHint(额外提示，有额外提示时，不用data提示)
     * @return {void}
     * */
    function checkUnique(constraints, data, hint, subHint) {
        if (constraints.includes(data)) {
            let failHint = subHint
                ? `${hint}“${subHint}”已存在`
                : `${hint}“${data}”已存在`;
            _cache.failList.push(failHint);
        } else if (data) {
            constraints.push(data);
        }
    }
    //根据数据的NextSiblingID进行排序，返回排序后的数组
    function sortByNext(datas) {
        let target = [],
            temp = {};
        for (let data of datas) {
            temp[data.ID] = { me: data, next: null, prev: null };
        }
        for (let data of datas) {
            let next = temp[data.NextSiblingID] || null;
            temp[data.ID].next = next;
            if (next) {
                next.prev = temp[data.ID];
            }
        }
        let first = null;
        for (let data of datas) {
            let me = temp[data.ID];
            if (!me.prev) {
                first = me;
            }
        }
        if (!first) {
            return datas;
        }
        while (first) {
            target.push(first.me);
            first = first.next;
        }
        return target;
    }

    /*
     * 根据粒度获取项目(不包含详细数据)数据
     * @param  {Number}granularity 导出粒度
     *         {Object}summaryObj 汇总字段
     *         {Number}tenderID 单位工程ID
     *         {String}userID 用户ID
     * @return {Object} 返回的数据结构：{children: [{children: []}]} 最外层为建设项目，中间为单项工程，最底层为单位工程
     * */
    async function getProjectByGranularity(granularity, summaryObj, tenderID, userID) {
        let projectData = _cache.projectData;
        // 没有数据，需要拉取
        if (!Object.keys(projectData).length) {
            projectData = await ajaxPost('/pm/api/getProjectByGranularity', { user_id: userID, tenderID, granularity, summaryObj });
            _cache.projectData = projectData;
        }
        return projectData;
    }

    /*
     * 通过getData接口获取单位工程详细数据(带缓存功能)
     * @param  {Number}tenderID 单位工程ID
     *         {String}userID 用户ID
     * @return {Object} 跟projectObj.project的数据结构一致
     * */
    async function getTenderDetail(tenderID, userID) {
        // 获取单位工程详细数据
        let tenderDetail = _cache.tenderDetailMap[tenderID];
        if (!tenderDetail) {
            tenderDetail = PROJECT.createNew(tenderID, userID);
            await tenderDetail.loadDataSync();
            // 标记序号
            const count = Object.keys(_cache.tenderDetailMap).length;
            tenderDetail.serialNo = count + 1;
            _cache.tenderDetailMap[tenderID] = tenderDetail;
        }
        return tenderDetail;
    }

    /*
     * 提取要导出的数据
     * @param  {Function}entryFunc 提取数据的入口方法
     *         {Number}granularity 导出粒度： 1-建设项目、2-单项工程、3-单位工程
     *         {Number}exportKind 导出的文件类型：1-投标、2-招标、3-控制价
     *         {Number}tenderID 单位工程ID
     *         {String}userID 用户ID
     * @return {Array} 数据结构为：[{data: Object, exportKind: Number, fileName: String}]
     * */
    async function extractExportData(entryFunc, granularity, summaryObj, exportKind, tenderID, userID) {
        // 默认导出建设项目
        if (!granularity || ![1, 2, 3].includes(granularity)) {
            granularity = GRANULARITY.PROJECT;
        }
        // 默认导出投标文件
        if (!exportKind || ![1, 2, 3].includes(exportKind)) {
            exportKind = EXPORT_KIND.Tender;
        }
        // 拉取标段数据:建设项目、单项工程、单位工程数据
        let projectData = await getProjectByGranularity(granularity, summaryObj, tenderID, userID);
        if (!projectData) {
            throw '获取项目数据错误';
        }
        // 单项工程、单位工程按照树结构数据进行排序
        projectData.children = sortByNext(projectData.children);
        for (let engData of projectData.children) {
            engData.children = sortByNext(engData.children);
        }
        // 提取相关项目的详细导出数据
        return await entryFunc(userID, exportKind, projectData);
    }

    //获取普通基数: {xxx}
    function getNormalBase(str) {
        let reg = /{.+?}/g,
            matchs = str.match(reg);
        return matchs || [];
    }
    //获取id引用基数: @xxx-xxx-xx
    function getIDBase(str) {
        let reg = /@.{36}/g,
            matchs = str.match(reg);
        return matchs || [];
    }

    // 转换基数表达式
    // 1.有子项，则取固定清单对应基数
    // 2.无子项，有基数，a.优先转换为行代号(不可自身) b.不能转换为行代号则找对应字典
    // 3.基数中有无法转换的，根据导出类型决定
    function transformCalcBase(exportKind, tenderDetail, node, { CalcBaseMap, FlagCalcBaseMap }) {
        let expr = node.data.calcBase || '';
        if (node.children.length) {
            let flag = node.getFlag();
            return FlagCalcBaseMap[flag] || '';
        }
        if (expr) {
            let illegal = false;
            let normalBase = getNormalBase(expr),
                idBase = getIDBase(expr);
            //普通基数转基数字典
            normalBase.forEach(base => {
                let replaceStr = CalcBaseMap[base];
                //转换成行代号的优先级比较高，进行清单匹配
                let flag = FlagCalcBaseMap[base];
                if (flag) {
                    let flagNode = tenderDetail.mainTree.items.find(mNode => mNode.getFlag() === flag);
                    //匹配到了 普通基数转换成行引用
                    if (flagNode) {
                        replaceStr = `F${flagNode.serialNo() + 1}`;
                    }
                }
                //存在无法处理的基数
                if (!replaceStr) {
                    illegal = true;
                    return;
                }
                expr = expr.replace(new RegExp(base, 'g'), replaceStr);
            });
            //id引用转行代号引用
            idBase.forEach(base => {
                let id = base.match(/[^@]+/)[0];
                let theNode = tenderDetail.mainTree.getNodeByID(id),
                    rowCode = theNode ? `F${theNode.serialNo() + 1}` : '';
                if (!rowCode) {
                    illegal = true;
                    return;
                }
                expr = expr.replace(new RegExp(base, 'g'), rowCode);
            });
            //不合法
            // 在我们软件中的基数无法找到映射代号的情况下
            // 导出招标、控制价时，基数为空
            // 导出投标时，基数=综合合价/费率
            if (illegal) {
                if (exportKind === EXPORT_KIND.Bid || exportKind === EXPORT_KIND.Control) {
                    return '';
                } else {
                    let totalFee = getFee(node.data.fees, 'common.totalFee'),
                        feeRate = node.data.feeRate;
                    return +feeRate ? scMathUtil.roundTo(totalFee / (feeRate / 100), -2) : totalFee
                }
            }
            return expr;
        }
    }
    // 转换基数说明，根据转换后的基数处理
    //1.行引用转换为对应行的名称
    //2.基数字典转换为中文
    function transformCalcBaseState(tenderDetail, expr, CalcStateMap) {
        if (!expr) {
            return '';
        }
        expr = String(expr);
        //提取基数
        let bases = expr.split(/[\+\-\*\/]/g);
        //提取操作符
        let oprs = expr.match(/[\+\-\*\/]/g);
        //转换后的基数
        let newBase = [];
        let illegal = false;
        for (let base of bases) {
            //行引用转换为名称.
            if (/F\d+/.test(base)) {
                let rowCode = base.match(/\d+/)[0],
                    node = tenderDetail.mainTree.items[rowCode - 1];
                if (!node || !node.data.name) {
                    illegal = true;
                    break;
                }
                newBase.push(node && node.data.name ? node.data.name : '');
            } else if (CalcStateMap[base]) {    //字典转换为中文
                newBase.push(CalcStateMap[base]);
            } else if (/^\d+(\.\d+)?$/.test(base)) {    //金额
                newBase.push(base);
            } else {
                illegal = true;
                break;
            }
        }
        if (illegal) {
            return '';
        }
        let newExpr = '';
        for (let i = 0; i < newBase.length; i++) {
            newExpr += newBase[i];
            if (oprs && oprs[i]) {
                newExpr += oprs[i];
            }
        }
        return newExpr;
    }
    // 获取工程编号表格相关数据(导出需要弹出工程编号让用户选择)
    function getCodeSheetData(projectData) {
        let curCode = '0';
        let sheetData = [];
        sheetData.push(getObj(projectData));
        projectData.children.forEach(eng => {
            sheetData.push(getObj(eng));
            eng.children.forEach(tender => {
                sheetData.push(getObj(tender));
            });
        });
        //建设项目父ID设置为-1
        if (sheetData.length) {
            sheetData[0].ParentID = -1;
            sheetData[0].code = '';
        }
        return sheetData;
        function getObj(data) {
            return {
                collapsed: false,
                ID: data.ID,
                ParentID: data.ParentID,
                NextSiblingID: data.NextSiblingID,
                name: data.name,
                code: data.code || String(curCode++)
            };
        }
    }
    // 获取节点的某属性
    function getAttr(ele, name) {
        return (ele.attrs.find(attr => attr.name === name) || {}).value;
    }
    // 设置节点的某属性
    function setAttr(ele, name, value) {
        const attr = ele.attrs.find(attr => attr.name === name);
        if (attr) {
            attr.value = value;
        }
    }
    // 从srcEle节点中获取元素名为eleName的元素
    function getElementFromSrc(srcEle, eleName) {
        if (!srcEle || !srcEle.children || !srcEle.children.length) {
            return [];
        }
        return srcEle.children.filter(ele => ele.name === eleName);
    }
    /*
     * 设置完工程编号后，更新原始数据的工程编号
     * 更新原始数据前需要将编号里的特殊字符进行转换
     * @param  {Array}exportData 提取出来的需要导出的数据
     *         {Array}codes 工程编号表中填写的工程编号
     *         {String}EngineeringName 单项工程元素的名称
     *         {String}tenderName 单位工程元素的名称
     *         {String}codeName 编号属性的名称
     * @return {void}
     * */
    function setupCode(exportData, codes, EngineeringName, tenderName, codeName) {
        // 转换xml实体字符
        let parsedCodes = getParsedData(codes);
        // 给导出数据里的单项工程、单位工程填上用户设置的工程编号
        exportData.forEach(orgData => {
            let curIdx = 0;
            let engs = getElementFromSrc(orgData.data, EngineeringName);
            engs.forEach(eng => {
                eng.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
                let tenders = getElementFromSrc(eng, tenderName);
                tenders.forEach(tender => {
                    tender.attrs.find(attr => attr.name === codeName).value = parsedCodes[curIdx++];
                });
            });
        });
    }

    /**
     * 项目属性基本信息、工程特征的自检
     * 有错误的话，将先直接弹窗提示，不会进行其他的自检
     *   */
    function propertyCheck() {
        const type = {
            info: '基本信息',
            feature: '工程特征'
        };
        const failList = ['<span style="font-weight: bold">建设项目下：</span>'];
        let haveHandledProjectData = false;
        const tenderDetailMap = _cache.tenderDetailMap;
        // 按照获取顺序serialNo(根据树结构)排序
        Object.values(tenderDetailMap)
            .sort((a, b) => a.serialNo - b.serialNo)
            .forEach(tenderDetail => {
                if (!haveHandledProjectData) {
                    haveHandledProjectData = true;
                    const basicInformation = tenderDetail.projectInfo.property.basicInformation || [];
                    const infoFailList = itemsCheck(basicInformation, type.info);
                    if (!infoFailList.length) {
                        failList.splice(0, failList.length);
                    } else {
                        failList.push(...infoFailList);
                    }
                }
                failList.push(`<span style="font-weight: bold">单位工程“${tenderDetail.projectInfo.name}”下：</span>`);
                const projectFeature = tenderDetail.projectInfo.property.projectFeature || [];
                const featureFailList = itemsCheck(projectFeature, type.feature);
                if (!featureFailList.length) {
                    failList.splice(-1, 1);
                } else {
                    failList.push(...featureFailList);
                }
            });
        return failList;

        // 条目检查
        function itemsCheck(items, type) {
            const requiredData = commonUtil.getRequired([], items);
            return requiredData
                .filter(item => commonUtil.isEmptyVal(item.value))
                .map(item => `${type}-“${item.dispName}”不能为空。`);
        }
    }

    /**
     * 弱自检自检，有错误非强制不可导的自检
     * failList里存的是导出规定属性时，不符合标准规定的错误，存在这种错误就不允许导出
     * 这里的错误是业务上的提示错误，与标准文件规定无关，存在这种错误是允许导出的
     * @return {Array} - 提示信息数组 
     */
    function softCheck() {
        const tenderDetailMap = _cache.tenderDetailMap;
        // 检查清单综合单价
        function checkPrice(tenderDetail) {
            const rst = [];
            tenderDetail.mainTree.items.forEach(node => {
                const code = node.data.code || '';
                const name = node.data.name || '';
                if (calcTools.unitFeeGTMaxPrice(node, 'common.unitFee')) {
                    rst.push(`第${node.serialNo()}行“${code + name}”，清单综合单价 > 最高限价`);
                }
                if (calcTools.unitFeeLTMinPrice(node, 'common.unitFee')) {
                    rst.push(`第${node.serialNo()}行“${code + name}”，清单综合单价 < 最低限价`);
                }
            });
            return rst;
        }
        const infos = [];
        // 按照获取顺序serialNo(根据树结构)排序
        Object.values(tenderDetailMap)
            .sort((a, b) => a.serialNo - b.serialNo)
            .forEach(tenderDetail => {
                const priceInfos = checkPrice(tenderDetail);
                if (!priceInfos.length) {
                    return;
                }
                infos.push(`<span style="font-weight: bold">单位工程“${tenderDetail.projectInfo.name}”下：</span>`);
                infos.push(...priceInfos);
            });
        return infos;
    }

    const UTIL = Object.freeze({
        deWeightHints,
        isDef,
        hasValue,
        setTimeoutSync,
        getFee,
        getAggregateFee,
        getFeeByFlag,
        getPlainAttrs,
        getValueByKey,
        getRelGLJ,
        generateHardwareId,
        arrayToObj,
        validDepth,
        checkUnique,
        sortByNext,
        getTenderDetail,
        getProjectByGranularity,
        getNormalBase,
        getIDBase,
        transformCalcBase,
        transformCalcBaseState,
        getCodeSheetData,
        getElementFromSrc,
        getAttr,
        setAttr,
        getParsedData,
        setupCode,
        propertyCheck,
        softCheck,
    });

    // 开始标签
    function _startTag(ele) {
        let rst = `<${ele.name}`;
        for (let attr of ele.attrs) {
            rst += ` ${attr.name}="${attr.value}"`;
        }
        rst += ele.children.length > 0 ? '>' : '/>';
        return rst;
    }
    // 结束标签
    function _endTag(ele) {
        return `</${ele.name}>`;
    }
    // 拼接成xml字符串
    function _toXMLStr(eles) {
        let rst = '';
        for (let ele of eles) {
            rst += _startTag(ele);
            if (ele.children.length > 0) {
                rst += _toXMLStr(ele.children);
                rst += _endTag(ele);
            }
        }
        return rst;
    }
    //格式化xml字符串
    function _formatXml(text) {
        //去掉多余的空格
        text = '\n' + text.replace(/>\s*?</g, ">\n<");
        //调整格式
        let rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/mg;
        let nodeStack = [];
        let output = text.replace(rgx, function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
            let isClosed = (isCloseFull1 === '/') || (isCloseFull2 === '/') || (isFull1 === '/') || (isFull2 === '/');
            let prefix = '';
            if (isBegin === '!') {
                prefix = getPrefix(nodeStack.length);
            } else {
                if (isBegin !== '/') {
                    prefix = getPrefix(nodeStack.length);
                    if (!isClosed) {
                        nodeStack.push(name);
                    }
                } else {
                    nodeStack.pop();
                    prefix = getPrefix(nodeStack.length);
                }
            }
            let ret = '\n' + prefix + all;
            return ret;
        });
        let outputText = output.substring(1);
        return outputText;
        function getPrefix(prefixIndex) {
            let span = '    ';
            let output = [];
            for (let i = 0; i < prefixIndex; ++i) {
                output.push(span);
            }
            return output.join('');
        }
    }

    /*
     * 根据各自费用定额的文件结构，导出文件
     * 每个费用定额可能导出的结果文件都不同
     * 比如广东18需要将一个建设项目文件，多个单位工程文件打包成一个zip文件。重庆18就没这种要求
     * @param  {Array}codes 工程编号数据
     *         {Array}extractData 提取的数据
     *         {Function}setCodeFunc 各自费用定额的导出前重设用户输入的工程编号方法
     *         {Function}saveAsFunc 各自费用定额的导出方法
     * @return {void}
     * */
    async function exportFile(codes, extractData, setCodeFunc, saveAsFunc) {
        // 编号重置后将会被导出，需要将编号进行xml字符实体转换
        codes = getParsedData(codes);
        setCodeFunc(codes, extractData);
        // 获取文件数据
        let fileData = extractData.map(extractObj => {
            // 转换成xml字符串
            let xmlStr = _toXMLStr([extractObj.data]);
            // 加上xml声明
            xmlStr = `<?xml version="1.0" encoding="utf-8"?>${xmlStr}`;
            // 格式化
            xmlStr = _formatXml(xmlStr);
            let blob = new Blob([xmlStr], { type: 'text/plain;charset=utf-8' });
            return {
                blob: blob,
                exportKind: extractObj.exportKind,
                fileName: extractObj.fileName
            };
        });
        if (!saveAsFunc) {
            return fileData;
        }
        // 导出
        await saveAsFunc(fileData);
    }

    // 提取xml数据，中间数据（目前为导出指标压缩包用）
    async function getExtractData() {
        const fileKind = projectObj.project.projectInfo.property.fileKind;
        const projectID = projectObj.project.ID();
        const extractData = await extractExportData(XMLStandard.entry, GRANULARITY.PROJECT,
            XMLStandard.summaryObj, fileKind, projectID, userID);
        // projects表数据
        const projectData = _.cloneDeep(_cache.projectData);
        // 错误提示
        const failList = deWeightHints(_.cloneDeep(_cache.failList));
        // 清空缓存
        clear();
        return {
            extractData,
            projectData,
            failList
        }
    }

    /**
     * 提取qtf文件数据（目前为导出指标压缩包用）
     * @param {Array} codes - 设置工程编号窗口获取到的编号数据 
     * @param {Array} extractData - 提取的导出中间数据
     * @return {Array} 
     */
    async function getExtractFile(codes, extractData) {
        return await exportFile(codes, extractData, XMLStandard.resetContentCode);
    }

    return {
        CONFIG,
        CACHE,
        UTIL,
        Element,
        extractExportData,
        exportFile,
        getExtractData,
        getExtractFile,
    };
})();