/** @copyright 2018 Planning Village Corporation Co., Ltd. */
(function () {
    'use strict';

    // [IE11] Polyfill of Object.values
    if (!Object.values) {
        const ownKeys = (
            (typeof Reflect === 'object' && typeof Reflect.ownKeys === 'function') ? Reflect.ownKeys :
            // @ts-ignore
            (typeof Object.getOwnPropertySymbols === 'function') ? (function (O) {
                // @ts-ignore
                return Object.getOwnPropertyNames(O).concat(Object.getOwnPropertySymbols(O));
            }) :
            Object.getOwnPropertyNames);
        // @ts-ignore
        const reduce = Function.bind.call(Function.call, Array.prototype.reduce);
        // @ts-ignore
        const isEnumerable = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
        // @ts-ignore
        const concat = Function.bind.call(Function.call, Array.prototype.concat);
        //@ts-ignore
        Object.values = function values(O) {
            //@ts-ignore
            return reduce(ownKeys(O), function (v, k) {
                return concat(v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : [])
            }, [])
        };
    }

    // [IE11] Polyfill of Number.isNaN 
    if (!Number.isNaN) {
        Number.isNaN = function (value) {
            return value !== null && (value != value || +value != value);
        };
    }

    // [IE11] Polyfill of String.prototype.startsWith
    if (!String.prototype.startsWith) {
        Object.defineProperty(String.prototype, 'startsWith', {
            //@ts-ignore
            value: function (search, rawPos) {
                var pos = rawPos > 0 ? rawPos | 0 : 0;
                return this.substring(pos, pos + search.length) === search;
            }
        });
    }

    // [IE11] Polyfill of String.prototype.includes
    if (!String.prototype.includes) {
        String.prototype.includes = function (search, start) {
            'use strict';
            //@ts-ignore
            if (search instanceof RegExp) {
                throw TypeError('first argument must not be a RegExp');
            }
            if (start === undefined) {
                start = 0;
            }
            return this.indexOf(search, start) !== -1;
        };
    }

    // [IE11] Polyfill of Array.prototype.findIndex
    if (!Array.prototype.findIndex) {
        // @ts-ignore
        Array.prototype.findIndex = function (predicate) {
            if (this === null) {
                throw new TypeError('Array.prototype.findIndex called on null or undefined');
            }
            if (typeof predicate !== 'function') {
                throw new TypeError('predicate must be a function');
            }
            var list = Object(this);
            var length = list.length >>> 0;
            var thisArg = arguments[1];
            var value;

            for (var i = 0; i < length; i++) {
                value = list[i];
                if (predicate.call(thisArg, value, i, list)) {
                    return i;
                }
            }
            return -1;
        };
    }

    //@ts-ignore
    window.pvc = window.pvc || {};
    //@ts-ignore
    window.pvc.lib = window.pvc.lib || {};
    //@ts-ignore
    window.pvc.lib.exceljsUtil = (function () {
        const exceljs = {
            /**
             * ワークブックの blob を読み取ります。
             * @param {Blob} blob Blob
             * @return {Promise<ExcelJS.Workbook>} ワークブック
             */
            loadWorkbookBlob: function (blob) {
                return new Promise(function (resolve, reject) {
                    var reader = new FileReader();
                    reader.onload = function (e) {
                        // @ts-ignore
                        resolve(e.target.result);
                    };
                    reader.onerror = function (e) {
                        reject(e);
                    }
                    reader.onabort = function (e) {
                        reject(e);
                    }
                    reader.readAsArrayBuffer(blob);
                }).then(function (arrayBuffer) {
                    // 非公式のメンバー load を使用
                    // @ts-ignore
                    return new ExcelJS.Workbook().xlsx.load(arrayBuffer);
                });
            },

            /**
             * ワークブックを Blob に変換します。
             * @param {ExcelJS.Workbook} workbook ワークブック
             * @return {Promise<Blob>} Blob
             */
            saveWorkbookToBlob: function (workbook) {
                // writeBuffer の戻り値 ExcelJS.Buffer はブラウザ環境では Uint8Array となる
                return workbook.xlsx.writeBuffer().then(function (ua) {
                    return new Blob([ua], {
                        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                    });
                });
            },

            /**
             * 行をコピーします。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number} topRowNumber コピー元の開始行番号(1から開始)
             * @param {number} [bottomRowNumber] コピー元の終了行番号(1から開始)。省略時は開始行番号と同じ値となります。
             * @returns {pvc.lib.exceljsUtil.CopiedRowsInfo} 行のコピー情報
             */
            copyRows: function (worksheet, topRowNumber, bottomRowNumber) {
                const _bottomRowNumber = (bottomRowNumber == null ? topRowNumber : bottomRowNumber);

                if (topRowNumber < 0 || _bottomRowNumber < topRowNumber) {
                    throw new Error('コピー元の領域とが不正です。');
                }

                // コピー元の領域の情報を収集
                const rows = [];
                for (let r = topRowNumber; r <= _bottomRowNumber; r++) {
                    const row = worksheet.getRow(r);
                    const cells = [];
                    for (let c = 1; c <= worksheet.columnCount; c++) {
                        const cell = row.getCell(c);
                        cells.push({
                            value: cell.value,
                            style: cell.style,
                        });
                    }
                    rows.push({
                        cells: cells,
                        height: row.height,
                        // 非公式のメンバー style を使用
                        // @ts-ignore
                        style: row.style,
                    });
                }

                // コピー元の領域の内部に収まるマージを収集
                const merges = deepCopyObject(getWorksheetMerges(worksheet).filter(function (merge) {
                    return (topRowNumber <= merge.top && merge.bottom <= _bottomRowNumber);
                }).map(function (merge) {
                    return {
                        top: merge.top - topRowNumber,
                        right: merge.right,
                        bottom: merge.bottom - topRowNumber,
                        left: merge.left,
                    };
                }));

                return deepCopyObject({
                    rows: rows,
                    merges: merges
                });
            },

            /**
             * 指定された開始行を起点に、コピーした行を貼り付けます。
             * コピー元の領域と領域外の両方にまたがるセルのマージは貼り付けされません。
             * コピー先の領域を一部でも含むマージは全て解除されます。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number} topRowNumber コピー先の開始行番号(1から開始)
             * @param {pvc.lib.exceljsUtil.CopiedRowsInfo} copiedRowsInfo 行のコピー情報
             */
            pasteRows: function (worksheet, topRowNumber, copiedRowsInfo) {
                const bottomRowNumber = topRowNumber + copiedRowsInfo.rows.length - 1;

                if (topRowNumber < 0) {
                    throw new Error('コピー先の領域とが不正です。');
                }

                // コピー先の行のマージを解除（コピー先の領域を一部でも含むマージは全て解除）
                getWorksheetMerges(worksheet).filter(function (merge) {
                    return (topRowNumber <= merge.bottom && merge.top <= bottomRowNumber);
                }).forEach(function (merge) {
                    worksheet.unMergeCells(merge.range);
                });

                // マージをペースト
                copiedRowsInfo.merges.forEach(function (mergeInfo) {
                    worksheet.mergeCells(
                        topRowNumber + mergeInfo.top,
                        mergeInfo.left,
                        topRowNumber + mergeInfo.bottom,
                        mergeInfo.right);
                });

                // セルをペースト
                copiedRowsInfo.rows.forEach(function (rowInfo, i) {
                    const row = worksheet.getRow(topRowNumber + i);
                    row.height = rowInfo.height;
                    // 非公式のメンバー style を使用
                    // @ts-ignore
                    row.style = rowInfo.style;
                    for (let c = 1; c <= worksheet.columnCount; c++) {
                        const cell = row.getCell(c);
                        const cellInfo = rowInfo.cells[c - 1];
                        if (cellInfo) {
                            cell.value = cellInfo.value;
                            cell.style = cellInfo.style;
                        } else {
                            exceljs.clearCell(cell);
                        }
                    }
                });
            },

            /**
             * 指定された開始行を起点に、コピーした行を挿入します。
             * コピー元の領域と領域外の両方にまたがるセルのマージは貼り付けされません。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number} topRowNumber コピー先の開始行番号(1から開始)
             * @param {pvc.lib.exceljsUtil.CopiedRowsInfo} copiedRowsInfo 行のコピー情報
             */
            insertRows: function (worksheet, topRowNumber, copiedRowsInfo) {
                const bottomRows = (
                    topRowNumber <= worksheet.rowCount ?
                    exceljs.copyRows(worksheet, topRowNumber, worksheet.rowCount) :
                    null);
                exceljs.pasteRows(worksheet, topRowNumber, copiedRowsInfo);
                if (bottomRows) {
                    exceljs.pasteRows(worksheet, topRowNumber + copiedRowsInfo.rows.length, bottomRows);
                }
            },

            /**
             * 列をコピーします。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number | string} leftColumn コピー元の開始列の列番号(1から開始)または列ラベル。終了列の引数を省略して、代わりに "A:B" の形式でコピー元の列の範囲を指定することも可能。
             * @param {number | string} [rightColumn] コピー元の終了列の列番号(1から開始)または列ラベル。省略時は開始列と同じとみなされます(開始列を "A:B" の形式で指定された場合を除く)。
             * @returns {pvc.lib.exceljsUtil.CopiedColumnsInfo} 列のコピー情報
             */
            copyColumns: function (worksheet, leftColumn, rightColumn) {
                if (rightColumn == null) {
                    rightColumn = leftColumn;
                    if (typeof leftColumn === 'string') {
                        const sp = leftColumn.split(':');
                        if (sp.length === 2) {
                            leftColumn = sp[0];
                            rightColumn = sp[1];
                        }
                    }
                }

                const leftColumnNumber = ((typeof leftColumn === 'number') ? leftColumn : exceljs.columnLetterToColumnNumber(leftColumn));
                const rightColumnNumber = ((typeof rightColumn === 'number') ? rightColumn : exceljs.columnLetterToColumnNumber(rightColumn));

                if (leftColumnNumber < 0 || rightColumnNumber < leftColumnNumber) {
                    throw new Error('コピー元の領域とが不正です。');
                }

                // コピー元の領域の情報を収集
                const columns = [];
                for (let c = leftColumnNumber; c <= rightColumnNumber; c++) {
                    const column = worksheet.getColumn(c);
                    const cells = [];
                    for (let r = 1; r <= worksheet.rowCount; r++) {
                        const cell = worksheet.getRow(r).getCell(c);
                        cells.push({
                            value: cell.value,
                            style: cell.style,
                        });
                    }
                    columns.push({
                        cells: cells,
                        width: column.width,
                        style: column.style,
                    });
                }

                // コピー元の領域の内部に収まるマージを収集
                const merges = deepCopyObject(getWorksheetMerges(worksheet).filter(function (merge) {
                    return (leftColumnNumber <= merge.left && merge.right <= rightColumnNumber);
                }).map(function (merge) {
                    return {
                        top: merge.top,
                        right: merge.right - leftColumnNumber,
                        bottom: merge.bottom,
                        left: merge.left - leftColumnNumber,
                    };
                }));

                return {
                    columns: columns,
                    merges: merges
                };
            },

            /**
             * 指定された開始列を起点に、コピーした列を貼り付けます。
             * コピー元の領域と領域外の両方にまたがるセルのマージは貼り付けされません。
             * コピー先の領域を一部でも含むマージは全て解除されます。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number | string} leftColumn コピー先の開始列の列番号(1から開始)または列ラベル
             * @param {pvc.lib.exceljsUtil.CopiedColumnsInfo} copiedColumnsInfo 列のコピー情報
             */
            pasteColumns: function (worksheet, leftColumn, copiedColumnsInfo) {
                const leftColumnNumber = ((typeof leftColumn === 'number') ? leftColumn : exceljs.columnLetterToColumnNumber(leftColumn));
                const rightColumnNumber = leftColumnNumber + copiedColumnsInfo.columns.length - 1;

                if (leftColumnNumber < 0) {
                    throw new Error('コピー先の領域とが不正です。');
                }

                // コピー先の列のマージを解除（コピー先の領域を一部でも含むマージは全て解除）
                getWorksheetMerges(worksheet).filter(function (merge) {
                    return (leftColumnNumber <= merge.right && merge.left <= rightColumnNumber);
                }).forEach(function (merge) {
                    worksheet.unMergeCells(merge.range);
                });

                // マージをペースト
                copiedColumnsInfo.merges.forEach(function (mergeInfo) {
                    worksheet.mergeCells(
                        mergeInfo.top,
                        leftColumnNumber + mergeInfo.left,
                        mergeInfo.bottom,
                        leftColumnNumber + mergeInfo.right);
                });

                // セルをペースト
                copiedColumnsInfo.columns.forEach(function (columnInfo, i) {
                    const column = worksheet.getColumn(leftColumnNumber + i);
                    column.width = columnInfo.width;
                    column.style = columnInfo.style;
                    for (let r = 1; r <= worksheet.rowCount; r++) {
                        const cell = worksheet.getRow(r).getCell(leftColumnNumber + i);
                        const cellInfo = columnInfo.cells[r - 1];
                        if (cellInfo) {
                            cell.value = cellInfo.value;
                            cell.style = cellInfo.style;
                        } else {
                            exceljs.clearCell(cell);
                        }
                    }
                });
            },

            /**
             * 指定された開始列を起点に、コピーした列を挿入します。
             * コピー元の領域と領域外の両方にまたがるセルのマージは貼り付けされません。
             * @param {ExcelJS.Worksheet} worksheet ワークシート
             * @param {number | string} leftColumn コピー先の開始列の列番号(1から開始)または列ラベル
             * @param {pvc.lib.exceljsUtil.CopiedColumnsInfo} copiedRowsInfo 列のコピー情報
             */
            insertColumns: function (worksheet, leftColumn, copiedRowsInfo) {
                const leftColumnNumber = ((typeof leftColumn === 'number') ? leftColumn : exceljs.columnLetterToColumnNumber(leftColumn));
                const rightColumns = (
                    leftColumnNumber <= worksheet.columnCount ?
                    exceljs.copyColumns(worksheet, leftColumnNumber, worksheet.columnCount) :
                    null);
                exceljs.pasteColumns(worksheet, leftColumnNumber, copiedRowsInfo);
                if (rightColumns) {
                    exceljs.pasteColumns(worksheet, leftColumnNumber + copiedRowsInfo.columns.length, rightColumns);
                }
            },

            /**
             * セルをクリアします。
             * @param {ExcelJS.Cell} cell セル
             */
            clearCell: function (cell) {
                cell.value = null;
                /// @ts-ignore
                cell.style = cell._mergeStyle(cell._row.style, cell._column.style, {});
            },

            /**
             * 列ラベルから列番号(1から開始)を取得します。
             * @param {string} columnLetter  列ラベル
             * @return {number} 列番号(1から開始)
             */
            columnLetterToColumnNumber: function (columnLetter) {
                const letter = columnLetter.toUpperCase();
                const l = letter.length;
                const charCodeA = 65;
                let result = 0;
                for (let i = 0, j = l - 1; i < l; i++, j--) {
                    result += (letter.charCodeAt(j) - charCodeA + 1) * (i === 0 ? 1 : Math.pow(26, i));
                }
                return result;
            },

            /**
             * 日付のセルの値を作成します。
             * @param {Date} date 
             * @returns {any}
             */
            createDateCellValue: function (date) {
                // UTC にしないと正しく Excel に反映されない
                return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
            },

            deepCopyObject: deepCopyObject,
        };

        /**
         * ワークシートのマージを取得します。
         * @param {ExcelJS.Worksheet} worksheet ワークシート
         */
        function getWorksheetMerges(worksheet) {
            // 非公式のメンバー _merges を使用
            // @ts-ignore
            const mergesDic = worksheet._merges;
            return Object.keys(mergesDic).map(function (address) {
                return mergesDic[address];
            });
        }

        /**
         * オブジェクのコピーします。ディープコピーとなります。
         * @template T
         * @param {T} obj 
         * @returns {T}
         */
        function deepCopyObject(obj) {
            return copyObject(obj);

            //@ts-ignore
            function copyObject(obj) {
                return (
                    obj == null ? null :
                    Array.isArray(obj) ? copyArray(obj) :
                    isDate(obj) ? copyDate(obj) :
                    isDic(obj) ? copyDic(obj) :
                    obj);
            }

            //@ts-ignore
            function copyArray(array) {
                //@ts-ignore
                return array.map(function (item) {
                    return copyObject(item);
                });
            }

            //@ts-ignore
            function isDate(obj) {
                return (obj != null && Object.prototype.toString.call(obj) === '[object Date]');
            }

            //@ts-ignore
            function copyDate(date) {
                return (isNaN(date) ? null : new Date(date.getTime()));
            }

            //@ts-ignore
            function isDic(obj) {
                return (obj != null && typeof obj === 'object');
            }

            //@ts-ignore
            function copyDic(dic) {
                let result = {};
                for (let key in dic) {
                    //@ts-ignore
                    result[key] = copyObject(dic[key]);
                }
                return result;
            }
        }

        return exceljs;
    }());
})();