/*
 *  ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one or more
 *    contributor license agreements.  See the NOTICE file distributed with
 *    this work for additional information regarding copyright ownership.
 *    The ASF licenses this file to You under the Apache License, Version 2.0
 *    (the "License"); you may not use this file except in compliance with
 *    the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 * ====================================================================
 */

package org.apache.poi.ss.usermodel;

import java.util.HashMap;
import java.util.Map;
import org.apache.poi.ss.formula.FormulaShifter;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.Beta;

@Beta
public abstract class RangeCopier {
    private Sheet sourceSheet;
    private Sheet destSheet;
    private FormulaShifter horizontalFormulaShifter;
    private FormulaShifter verticalFormulaShifter;

    public RangeCopier(Sheet sourceSheet, Sheet destSheet) {
        this.sourceSheet = sourceSheet;
        this.destSheet = destSheet;
    }

    public RangeCopier(Sheet sheet) {
        this(sheet, sheet);
    }

    /** Uses input pattern to tile destination region, overwriting existing content. Works in following manner :
     * 1.Start from top-left of destination.
     * 2.Paste source but only inside of destination borders.
     * 3.If there is space left on right or bottom side of copy, process it as in step 2.
     * @param tilePatternRange source range which should be copied in tiled manner
     * @param tileDestRange    destination range, which should be overridden
     */
    public void copyRange(CellRangeAddress tilePatternRange, CellRangeAddress tileDestRange) {
        copyRange(tilePatternRange, tileDestRange, false, false);
    }

    /** Uses input pattern to tile destination region, overwriting existing content. Works in following manner :
     * 1.Start from top-left of destination.
     * 2.Paste source but only inside of destination borders.
     * 3.If there is space left on right or bottom side of copy, process it as in step 2.
     * @param tilePatternRange source range which should be copied in tiled manner
     * @param tileDestRange    destination range, which should be overridden
     * @param copyStyles       whether to copy the cell styles
     * @param copyMergedRanges whether to copy merged ranges
     * @since 5.0.0
     */
    public void copyRange(CellRangeAddress tilePatternRange, CellRangeAddress tileDestRange, boolean copyStyles, boolean copyMergedRanges) {
        Sheet sourceCopy = sourceSheet.getWorkbook().cloneSheet(sourceSheet.getWorkbook().getSheetIndex(sourceSheet));
        Map<Integer, CellStyle> styleMap = copyStyles ? new HashMap<Integer, CellStyle>() {} : null;
        int sourceWidthMinus1 = tilePatternRange.getLastColumn() - tilePatternRange.getFirstColumn();
        int sourceHeightMinus1 = tilePatternRange.getLastRow() - tilePatternRange.getFirstRow();
        int rightLimitToCopy;
        int bottomLimitToCopy;

        int nextRowIndexToCopy = tileDestRange.getFirstRow();
        do {
            int nextCellIndexInRowToCopy = tileDestRange.getFirstColumn();
            int heightToCopyMinus1 = Math.min(sourceHeightMinus1, tileDestRange.getLastRow() - nextRowIndexToCopy);
            bottomLimitToCopy = tilePatternRange.getFirstRow() + heightToCopyMinus1;
            do {
                int widthToCopyMinus1 = Math.min(sourceWidthMinus1, tileDestRange.getLastColumn() - nextCellIndexInRowToCopy);
                rightLimitToCopy = tilePatternRange.getFirstColumn() + widthToCopyMinus1;
                CellRangeAddress rangeToCopy = new CellRangeAddress(
                        tilePatternRange.getFirstRow(),     bottomLimitToCopy,
                        tilePatternRange.getFirstColumn(),  rightLimitToCopy
                       );
                copyRange(rangeToCopy, nextCellIndexInRowToCopy - rangeToCopy.getFirstColumn(), nextRowIndexToCopy - rangeToCopy.getFirstRow(), sourceCopy, styleMap);
                nextCellIndexInRowToCopy += widthToCopyMinus1 + 1;
            } while (nextCellIndexInRowToCopy <= tileDestRange.getLastColumn());
            nextRowIndexToCopy += heightToCopyMinus1 + 1;
        } while (nextRowIndexToCopy <= tileDestRange.getLastRow());

        if (copyMergedRanges) {
            sourceSheet.getMergedRegions().forEach((mergedRangeAddress) -> destSheet.addMergedRegion(mergedRangeAddress));
        }

        int tempCopyIndex = sourceSheet.getWorkbook().getSheetIndex(sourceCopy);
        sourceSheet.getWorkbook().removeSheetAt(tempCopyIndex);
    }

    private void copyRange(CellRangeAddress sourceRange, int deltaX, int deltaY, Sheet sourceClone, Map<Integer, CellStyle> styleMap) { //NOSONAR, it's a bit complex but monolith method, does not make much sense to divide it
        if(deltaX != 0)
            horizontalFormulaShifter = FormulaShifter.createForColumnCopy(sourceSheet.getWorkbook().getSheetIndex(sourceSheet),
                    sourceSheet.getSheetName(), sourceRange.getFirstColumn(), sourceRange.getLastColumn(), deltaX, sourceSheet.getWorkbook().getSpreadsheetVersion());
        if(deltaY != 0)
            verticalFormulaShifter = FormulaShifter.createForRowCopy(sourceSheet.getWorkbook().getSheetIndex(sourceSheet),
                    sourceSheet.getSheetName(), sourceRange.getFirstRow(), sourceRange.getLastRow(), deltaY, sourceSheet.getWorkbook().getSpreadsheetVersion());

        for(int rowNo = sourceRange.getFirstRow(); rowNo <= sourceRange.getLastRow(); rowNo++) {
            // copy from source copy, original source might be overridden in process!
            Row sourceRow = sourceClone.getRow(rowNo);
            if(sourceRow == null) {
                continue;
            }

            for (int columnIndex = sourceRange.getFirstColumn(); columnIndex <= sourceRange.getLastColumn(); columnIndex++) {
                Cell sourceCell = sourceRow.getCell(columnIndex);
                if(sourceCell == null)
                    continue;
                Row destRow = destSheet.getRow(rowNo + deltaY);
                if(destRow == null)
                    destRow = destSheet.createRow(rowNo + deltaY);

                Cell newCell = destRow.getCell(columnIndex + deltaX);
                if(newCell == null) {
                    newCell = destRow.createCell(columnIndex + deltaX);
                }

                cloneCellContent(sourceCell, newCell, styleMap);
                if(newCell.getCellType() == CellType.FORMULA)
                    adjustCellReferencesInsideFormula(newCell, destSheet, deltaX, deltaY);
            }
        }
    }

    protected abstract void adjustCellReferencesInsideFormula(Cell cell, Sheet destSheet, int deltaX, int deltaY); // this part is different for HSSF and XSSF

    protected boolean adjustInBothDirections(Ptg[] ptgs, int sheetIndex, int deltaX, int deltaY) {
        boolean adjustSucceeded = true;
        if(deltaY != 0)
            adjustSucceeded = verticalFormulaShifter.adjustFormula(ptgs, sheetIndex);
        if(deltaX != 0)
            adjustSucceeded = adjustSucceeded && horizontalFormulaShifter.adjustFormula(ptgs, sheetIndex);
        return adjustSucceeded;
    }

    // TODO clone some more properties ?
    public static void cloneCellContent(Cell srcCell, Cell destCell, Map<Integer, CellStyle> styleMap) {
         if(styleMap != null) {
             if(srcCell.getSheet().getWorkbook() == destCell.getSheet().getWorkbook()){
                 destCell.setCellStyle(srcCell.getCellStyle());
             } else {
                 int stHashCode = srcCell.getCellStyle().hashCode();
                 CellStyle newCellStyle = styleMap.get(stHashCode);
                 if(newCellStyle == null){
                     newCellStyle = destCell.getSheet().getWorkbook().createCellStyle();
                     newCellStyle.cloneStyleFrom(srcCell.getCellStyle());
                     styleMap.put(stHashCode, newCellStyle);
                 }
                 destCell.setCellStyle(newCellStyle);
             }
         }
         switch(srcCell.getCellType()) {
             case STRING:
                 destCell.setCellValue(srcCell.getStringCellValue());
                 break;
             case NUMERIC:
                 destCell.setCellValue(srcCell.getNumericCellValue());
                 break;
             case BLANK:
                 destCell.setBlank();
                 break;
             case BOOLEAN:
                 destCell.setCellValue(srcCell.getBooleanCellValue());
                 break;
             case ERROR:
                 destCell.setCellErrorValue(srcCell.getErrorCellValue());
                 break;
             case FORMULA:
                 String oldFormula = srcCell.getCellFormula();
                 destCell.setCellFormula(oldFormula);
                 break;
             default:
                 break;
         }
     }
}
