/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */

#include <sal/config.h>

#include <com/sun/star/lang/NoSupportException.hpp>
#include <svx/svdotable.hxx>
#include "cellcursor.hxx"
#include "tablelayouter.hxx"
#include <cell.hxx>
#include <svx/svdmodel.hxx>
#include <svx/strings.hrc>
#include <svx/dialmgr.hxx>
#include <tools/debug.hxx>
#include <comphelper/diagnose_ex.hxx>


using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::lang;
using namespace ::com::sun::star::container;
using namespace ::com::sun::star::beans;
using namespace ::com::sun::star::table;


namespace sdr::table {

CellCursor::CellCursor( const TableModelRef & xTable, sal_Int32 nLeft, sal_Int32 nTop, sal_Int32 nRight, sal_Int32 nBottom )
: CellCursorBase( xTable, nLeft, nTop, nRight, nBottom )
{
}


CellCursor::~CellCursor()
{
}


// XCellCursor


Reference< XCell > SAL_CALL CellCursor::getCellByPosition( sal_Int32 nColumn, sal_Int32 nRow )
{
    return CellRange::getCellByPosition( nColumn, nRow );
}


Reference< XCellRange > SAL_CALL CellCursor::getCellRangeByPosition( sal_Int32 nLeft, sal_Int32 nTop, sal_Int32 nRight, sal_Int32 nBottom )
{
    return CellRange::getCellRangeByPosition( nLeft, nTop, nRight, nBottom );
}


Reference< XCellRange > SAL_CALL CellCursor::getCellRangeByName( const OUString& aRange )
{
    return CellRange::getCellRangeByName( aRange );
}


// XCellCursor


void SAL_CALL CellCursor::gotoStart(  )
{
    mnRight = mnLeft;
    mnBottom = mnTop;
}


void SAL_CALL CellCursor::gotoEnd(  )
{
    mnLeft = mnRight;
    mnTop = mnBottom;
}


void SAL_CALL CellCursor::gotoNext(  )
{
    if( mxTable.is() )
    {
        mnRight++;
        if( mnRight >= mxTable->getColumnCount() )
        {
            // if we past the last column, try skip to the row line
            mnTop++;
            if( mnTop >= mxTable->getRowCount() )
            {
                // if we past the last row, do not move cursor at all
                mnTop--;
                mnRight--;
            }
            else
            {
                // restart at the first column on the next row
                mnRight = 0;
            }
        }
    }

    mnLeft = mnRight;
    mnTop = mnBottom;
}


void SAL_CALL CellCursor::gotoPrevious(  )
{
    if( mxTable.is() )
    {
        if( mnLeft > 0 )
        {
            --mnLeft;
        }
        else if( mnTop > 0 )
        {
            --mnTop;
            mnLeft = mxTable->getColumnCount() - 1;
        }
    }

    mnRight = mnLeft;
    mnBottom = mnTop;
}


void SAL_CALL CellCursor::gotoOffset( ::sal_Int32 nColumnOffset, ::sal_Int32 nRowOffset )
{
    if( mxTable.is() )
    {
        const sal_Int32 nLeft = mnLeft + nColumnOffset;
        if( (nLeft >= 0) && (nLeft < mxTable->getColumnCount() ) )
            mnRight = mnLeft = nLeft;

        const sal_Int32 nTop = mnTop + nRowOffset;
        if( (nTop >= 0) && (nTop < mxTable->getRowCount()) )
            mnTop = mnBottom = nTop;
    }
}


// XMergeableCellCursor


/** returns true and the merged cell positions if a merge is valid or false if a merge is
    not valid for that range */
bool CellCursor::GetMergedSelection( CellPos& rStart, CellPos& rEnd )
{
    rStart.mnCol = mnLeft; rStart.mnRow = mnTop;
    rEnd.mnCol = mnRight; rEnd.mnRow = mnBottom;

    // single cell merge is never valid
    if( mxTable.is() && ((mnLeft != mnRight) || (mnTop != mnBottom)) ) try
    {
        CellRef xCell = mxTable->getCell( mnLeft, mnTop );

        // check if first cell is merged
        if( xCell.is() && xCell->isMerged() )
            findMergeOrigin( mxTable, mnLeft, mnTop, rStart.mnCol, rStart.mnRow );

        // check if last cell is merged
        xCell = mxTable->getCell( mnRight, mnBottom );
        if( xCell.is() )
        {
            if( xCell->isMerged() )
            {
                findMergeOrigin( mxTable, mnRight, mnBottom, rEnd.mnCol, rEnd.mnRow );
                // merge not possible if selection is only one cell and all its merges
                if( rEnd == rStart )
                    return false;
                xCell = mxTable->getCell( rEnd.mnCol, rEnd.mnRow );
            }
        }
        if( xCell.is() )
        {
            rEnd.mnCol += xCell->getColumnSpan()-1;
            rEnd.mnRow += xCell->getRowSpan()-1;
        }

        // now check if everything is inside the given bounds
        sal_Int32 nRow, nCol;
        for( nRow = rStart.mnRow; nRow <= rEnd.mnRow; nRow++ )
        {
            for( nCol = rStart.mnCol; nCol <= rEnd.mnCol; nCol++ )
            {
                xCell = mxTable->getCell( nCol, nRow );
                if( !xCell.is() )
                    continue;

                if( xCell->isMerged() )
                {
                    sal_Int32 nOriginCol, nOriginRow;
                    if( findMergeOrigin( mxTable, nCol, nRow, nOriginCol, nOriginRow ) )
                    {
                        if( (nOriginCol < rStart.mnCol) || (nOriginRow < rStart.mnRow) )
                            return false;

                        xCell = mxTable->getCell( nOriginCol, nOriginRow );
                        if( xCell.is() )
                        {
                            nOriginCol += xCell->getColumnSpan()-1;
                            nOriginRow += xCell->getRowSpan()-1;

                            if( (nOriginCol > rEnd.mnCol) || (nOriginRow > rEnd.mnRow) )
                                return false;
                        }
                    }
                }
                else if( ((nCol + xCell->getColumnSpan() - 1) > rEnd.mnCol) || ((nRow + xCell->getRowSpan() - 1 ) > rEnd.mnRow) )
                {
                    return false;
                }
            }
        }
        return true;
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION("svx.table", "");
    }
    return false;
}


void SAL_CALL CellCursor::merge(  )
{
    CellPos aStart, aEnd;
    if( !GetMergedSelection( aStart, aEnd ) )
        throw NoSupportException();

    if( !mxTable.is() || (mxTable->getSdrTableObj() == nullptr) )
        throw DisposedException();

    SdrModel& rModel(mxTable->getSdrTableObj()->getSdrModelFromSdrObject());
    const bool bUndo(mxTable->getSdrTableObj()->IsInserted() && rModel.IsUndoEnabled());

    if( bUndo )
        rModel.BegUndo( SvxResId(STR_TABLE_MERGE) );

    try
    {
        mxTable->merge( aStart.mnCol, aStart.mnRow, aEnd.mnCol - aStart.mnCol + 1, aEnd.mnRow - aStart.mnRow + 1 );
        mxTable->optimize();
        mxTable->setModified(true);
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION("svx.table", "");
    }

    if( bUndo )
        rModel.EndUndo();

    rModel.SetChanged();
}


void CellCursor::split_column( sal_Int32 nCol, sal_Int32 nColumns, std::vector< sal_Int32 >& rLeftOvers )
{
    const sal_Int32 nRowCount = mxTable->getRowCount();

    sal_Int32 nNewCols = 0, nRow;

    // first check how many columns we need to add
    for( nRow = mnTop; nRow <= mnBottom; ++nRow )
    {
        CellRef xCell = mxTable->getCell( nCol, nRow );
        if( xCell.is() && !xCell->isMerged() )
            nNewCols = std::max( nNewCols, nColumns - xCell->getColumnSpan() + 1 - rLeftOvers[nRow] );
    }

    if( nNewCols > 0 )
    {
        static constexpr OUString sWidth(u"Width"_ustr);
        Reference< XTableColumns > xCols( mxTable->getColumns(), UNO_SET_THROW );
        Reference< XPropertySet > xRefColumn( xCols->getByIndex( nCol ), UNO_QUERY_THROW );
        sal_Int32 nWidth = 0;
        xRefColumn->getPropertyValue( sWidth ) >>= nWidth;
        const sal_Int32 nNewWidth = nWidth / (nNewCols + 1);

        // reference column gets new width + rounding errors
        xRefColumn->setPropertyValue( sWidth, Any( nWidth - (nNewWidth * nNewCols) ) );

        xCols->insertByIndex( nCol + 1, nNewCols );
        mnRight += nNewCols;

        // distribute new width
        for( sal_Int32 nNewCol = nCol + nNewCols; nNewCol > nCol; --nNewCol )
        {
            Reference< XPropertySet > xNewCol( xCols->getByIndex( nNewCol ), UNO_QUERY_THROW );
            xNewCol->setPropertyValue( sWidth, Any( nNewWidth ) );
        }
    }

    for( nRow = 0; nRow < nRowCount; ++nRow )
    {
        CellRef xCell = mxTable->getCell( nCol, nRow );
        if( !xCell.is() || xCell->isMerged() )
        {
            if( nNewCols > 0 )
            {
                // merged cells are ignored, but newly added columns will be added to leftovers
                xCell = mxTable->getCell( nCol+1, nRow );
                if( !xCell.is() || !xCell->isMerged() )
                    rLeftOvers[nRow] += nNewCols;
            }
        }
        else
        {
            sal_Int32 nRowSpan = xCell->getRowSpan() - 1;
            sal_Int32 nColSpan = xCell->getColumnSpan() - 1;

            if( (nRow >= mnTop) && (nRow <= mnBottom) )
            {
                sal_Int32 nCellsAvailable = 1 + nColSpan + rLeftOvers[nRow];
                if( nColSpan == 0 )
                    nCellsAvailable += nNewCols;

                DBG_ASSERT( nCellsAvailable > nColumns, "sdr::table::CellCursor::split_column(), somethings wrong" );

                sal_Int32 nSplitSpan = (nCellsAvailable / (nColumns + 1)) - 1;

                sal_Int32 nSplitCol = nCol;
                sal_Int32 nSplits = nColumns + 1;
                while( nSplits-- )
                {
                    // last split eats rounding cells
                    if( nSplits == 0 )
                        nSplitSpan = nCellsAvailable - ((nSplitSpan+1) * nColumns) - 1;

                    mxTable->merge( nSplitCol, nRow, nSplitSpan + 1, nRowSpan + 1);
                    if( nSplits > 0 )
                        nSplitCol += nSplitSpan + 1;
                }

                do
                {
                    rLeftOvers[nRow++] = 0;
                }
                while( nRowSpan-- );
                --nRow;
            }
            else
            {
                // cope with outside cells, merge if needed
                if( nColSpan < (rLeftOvers[nRow] + nNewCols) )
                    mxTable->merge( nCol, nRow, (rLeftOvers[nRow] + nNewCols) + 1, nRowSpan + 1 );

                do
                {
                    rLeftOvers[nRow++] = 0; // consumed
                }
                while( nRowSpan-- );
                --nRow;
            }
        }
    }
}


void CellCursor::split_horizontal( sal_Int32 nColumns )
{
    const sal_Int32 nRowCount = mxTable->getRowCount();

    std::vector< sal_Int32 > aLeftOvers( nRowCount );

    for( sal_Int32 nCol = mnRight; nCol >= mnLeft; --nCol )
        split_column( nCol, nColumns, aLeftOvers );
}


void CellCursor::split_row( sal_Int32 nRow, sal_Int32 nRows, std::vector< sal_Int32 >& rLeftOvers )
{
    const sal_Int32 nColCount = mxTable->getColumnCount();

    sal_Int32 nNewRows = 0, nCol;

    // first check how many columns we need to add
    for( nCol = mnLeft; nCol <= mnRight; ++nCol )
    {
        CellRef xCell = mxTable->getCell( nCol, nRow );
        if( xCell.is() && !xCell->isMerged() )
            nNewRows = std::max( nNewRows, nRows - xCell->getRowSpan() + 1 - rLeftOvers[nCol] );
    }

    if( nNewRows > 0 )
    {
        static constexpr OUString sHeight(u"Height"_ustr);
        Reference< XTableRows > xRows( mxTable->getRows(), UNO_SET_THROW );
        Reference< XPropertySet > xRefRow( xRows->getByIndex( nRow ), UNO_QUERY_THROW );
        sal_Int32 nHeight = 0;
        xRefRow->getPropertyValue( sHeight ) >>= nHeight;
        const sal_Int32 nNewHeight = nHeight / (nNewRows + 1);

        // reference row gets new height + rounding errors
        xRefRow->setPropertyValue( sHeight, Any( nHeight - (nNewHeight * nNewRows) ) );

        xRows->insertByIndex( nRow + 1, nNewRows );
        mnBottom += nNewRows;

        // distribute new width
        for( sal_Int32 nNewRow = nRow + nNewRows; nNewRow > nRow; --nNewRow )
        {
            Reference< XPropertySet > xNewRow( xRows->getByIndex( nNewRow ), UNO_QUERY_THROW );
            xNewRow->setPropertyValue( sHeight, Any( nNewHeight ) );
        }
    }

    for( nCol = 0; nCol < nColCount; ++nCol )
    {
        CellRef xCell = mxTable->getCell( nCol, nRow );
        if( !xCell.is() || xCell->isMerged() )
        {
            if( nNewRows )
            {
                // merged cells are ignored, but newly added columns will be added to leftovers
                xCell = mxTable->getCell( nCol, nRow+1 );
                if( !xCell.is() || !xCell->isMerged() )
                    rLeftOvers[nCol] += nNewRows;
            }
        }
        else
        {
            sal_Int32 nRowSpan = xCell->getRowSpan() - 1;
            sal_Int32 nColSpan = xCell->getColumnSpan() - 1;

            if( (nCol >= mnLeft) && (nCol <= mnRight) )
            {
                sal_Int32 nCellsAvailable = 1 + nRowSpan + rLeftOvers[nCol];
                if( nRowSpan == 0 )
                    nCellsAvailable += nNewRows;

                DBG_ASSERT( nCellsAvailable > nRows, "sdr::table::CellCursor::split_row(), somethings wrong" );

                sal_Int32 nSplitSpan = (nCellsAvailable / (nRows + 1)) - 1;

                sal_Int32 nSplitRow = nRow;
                sal_Int32 nSplits = nRows + 1;
                while( nSplits-- )
                {
                    // last split eats rounding cells
                    if( nSplits == 0 )
                        nSplitSpan = nCellsAvailable - ((nSplitSpan+1) * nRows) - 1;

                    mxTable->merge( nCol, nSplitRow, nColSpan + 1, nSplitSpan + 1 );
                    if( nSplits > 0 )
                        nSplitRow += nSplitSpan + 1;
                }

                do
                {
                    rLeftOvers[nCol++] = 0;
                }
                while( nColSpan-- );
                --nCol;
            }
            else
            {
                // cope with outside cells, merge if needed
                if( nRowSpan < (rLeftOvers[nCol] + nNewRows) )
                    mxTable->merge( nCol, nRow, nColSpan + 1, (rLeftOvers[nCol] + nNewRows) + 1 );

                do
                {
                    rLeftOvers[nCol++] = 0; // consumed
                }
                while( nColSpan-- );
                --nCol;
            }
        }
    }
}


void CellCursor::split_vertical( sal_Int32 nRows )
{
    const sal_Int32 nColCount = mxTable->getColumnCount();

    std::vector< sal_Int32 > aLeftOvers( nColCount );

    for( sal_Int32 nRow = mnBottom; nRow >= mnTop; --nRow )
        split_row( nRow, nRows, aLeftOvers );
}


void SAL_CALL CellCursor::split( sal_Int32 nColumns, sal_Int32 nRows )
{
    if( (nColumns < 0) || (nRows < 0) )
        throw IllegalArgumentException();

    if( !mxTable.is() || (mxTable->getSdrTableObj() == nullptr) )
        throw DisposedException();

    SdrModel& rModel(mxTable->getSdrTableObj()->getSdrModelFromSdrObject());
    const bool bUndo(mxTable->getSdrTableObj()->IsInserted() && rModel.IsUndoEnabled());

    if( bUndo )
        rModel.BegUndo( SvxResId(STR_TABLE_SPLIT) );

    try
    {
        if( nColumns > 0 )
            split_horizontal( nColumns );

        if( nRows > 0 )
            split_vertical( nRows );

        if( nColumns > 0 ||nRows > 0 )
            mxTable->setModified(true);
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION("svx.table", "");
        throw NoSupportException();
    }

    if( bUndo )
        rModel.EndUndo();

    rModel.SetChanged();
}


sal_Bool SAL_CALL CellCursor::isMergeable(  )
{
    CellPos aStart, aEnd;
    return GetMergedSelection( aStart, aEnd );
}


}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
