/* -*- 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 "cellvaluebinding.hxx"
#include <rtl/math.hxx>
#include <com/sun/star/form/binding/IncompatibleTypesException.hpp>
#include <com/sun/star/lang/NotInitializedException.hpp>
#include <com/sun/star/text/XTextRange.hpp>
#include <com/sun/star/table/XCellRange.hpp>
#include <com/sun/star/sheet/FormulaResult.hpp>
#include <com/sun/star/sheet/XCellAddressable.hpp>
#include <com/sun/star/sheet/XCellRangeData.hpp>
#include <com/sun/star/sheet/XSpreadsheetDocument.hpp>
#include <com/sun/star/container/XIndexAccess.hpp>
#include <com/sun/star/beans/PropertyAttribute.hpp>
#include <com/sun/star/beans/NamedValue.hpp>
#include <com/sun/star/util/XNumberFormatsSupplier.hpp>
#include <com/sun/star/util/XNumberFormatTypes.hpp>
#include <com/sun/star/util/NumberFormat.hpp>
#include <cppuhelper/supportsservice.hxx>
#include <comphelper/types.hxx>
#include <comphelper/diagnose_ex.hxx>

namespace calc
{

#define PROP_HANDLE_BOUND_CELL  1

    namespace lang = css::lang;
    using namespace ::com::sun::star::uno;
    using namespace ::com::sun::star::lang;
    using namespace ::com::sun::star::table;
    using namespace ::com::sun::star::text;
    using namespace ::com::sun::star::sheet;
    using namespace ::com::sun::star::container;
    using namespace ::com::sun::star::beans;
    using namespace ::com::sun::star::util;
    using namespace ::com::sun::star::form::binding;

    OCellValueBinding::OCellValueBinding( const Reference< XSpreadsheetDocument >& _rxDocument, bool _bListPos )
        :m_xDocument( _rxDocument )
        ,m_bInitialized( false )
        ,m_bListPos( _bListPos )
    {
        // register our property at the base class
        registerPropertyNoMember(
            u"BoundCell"_ustr,
            PROP_HANDLE_BOUND_CELL,
            PropertyAttribute::BOUND | PropertyAttribute::READONLY,
            cppu::UnoType<CellAddress>::get(),
            css::uno::Any(CellAddress())
        );

        // TODO: implement a ReadOnly property as required by the service,
        // which probably maps to the cell being locked
    }

    OCellValueBinding::~OCellValueBinding( )
    {
        if ( !m_bDisposed )
        {
            acquire();  // prevent duplicate dtor
            dispose();
        }
    }

    IMPLEMENT_FORWARD_XINTERFACE2( OCellValueBinding, OCellValueBinding_Base, OCellValueBinding_PBase )

    IMPLEMENT_FORWARD_XTYPEPROVIDER2( OCellValueBinding, OCellValueBinding_Base, OCellValueBinding_PBase )

    void OCellValueBinding::disposing( std::unique_lock<std::mutex>& rGuard )
    {
        Reference<XModifyBroadcaster> xBroadcaster( m_xCell, UNO_QUERY );
        if ( xBroadcaster.is() )
        {
            xBroadcaster->removeModifyListener( this );
        }

        WeakComponentImplHelperBase::disposing(rGuard);

        // TODO: clean up here whatever you need to clean up (e.g. deregister as XEventListener
        // for the cell)
    }

    Reference< XPropertySetInfo > SAL_CALL OCellValueBinding::getPropertySetInfo(  )
    {
        return createPropertySetInfo( getInfoHelper() ) ;
    }

    ::cppu::IPropertyArrayHelper& OCellValueBinding::getInfoHelper()
    {
        return *OCellValueBinding_PABase::getArrayHelper();
    }

    ::cppu::IPropertyArrayHelper* OCellValueBinding::createArrayHelper( ) const
    {
        Sequence< Property > aProps;
        describeProperties( aProps );
        return new ::cppu::OPropertyArrayHelper(aProps);
    }

    void OCellValueBinding::getFastPropertyValue( std::unique_lock<std::mutex>& /*rGuard*/, Any& _rValue, sal_Int32 _nHandle ) const
    {
        OSL_ENSURE( _nHandle == PROP_HANDLE_BOUND_CELL, "OCellValueBinding::getFastPropertyValue: invalid handle!" );
            // we only have this one property...

        _rValue.clear();
        Reference< XCellAddressable > xCellAddress( m_xCell, UNO_QUERY );
        if ( xCellAddress.is() )
            _rValue <<= xCellAddress->getCellAddress( );
    }

    Sequence< Type > SAL_CALL OCellValueBinding::getSupportedValueTypes(  )
    {
        std::unique_lock<std::mutex> aGuard(m_aMutex);
        throwIfDisposed(aGuard);
        checkInitialized( );
        return getSupportedValueTypes(aGuard);
    }

    Sequence< Type > OCellValueBinding::getSupportedValueTypes( std::unique_lock<std::mutex>& /*rGuard*/  ) const
    {
        sal_Int32 nCount = m_xCellText.is() ? 3 : m_xCell.is() ? 1 : 0;
        if ( m_bListPos )
            ++nCount;

        Sequence< Type > aTypes( nCount );
        if ( m_xCell.is() )
        {
            auto pTypes = aTypes.getArray();

            // an XCell can be used to set/get "double" values
            pTypes[0] = ::cppu::UnoType<double>::get();
            if ( m_xCellText.is() )
            {
                // an XTextRange can be used to set/get "string" values
                pTypes[1] = ::cppu::UnoType<OUString>::get();
                // and additionally, we use it to handle booleans
                pTypes[2] = ::cppu::UnoType<sal_Bool>::get();
            }

            // add sal_Int32 only if constructed as ListPositionCellBinding
            if ( m_bListPos )
                pTypes[nCount-1] = cppu::UnoType<sal_Int32>::get();
        }

        return aTypes;
    }

    sal_Bool SAL_CALL OCellValueBinding::supportsType( const Type& aType )
    {
        std::unique_lock<std::mutex> aGuard(m_aMutex);
        throwIfDisposed(aGuard);
        checkInitialized( );
        return supportsType(aGuard, aType);
    }

    bool OCellValueBinding::supportsType( std::unique_lock<std::mutex>& rGuard, const Type& aType ) const
    {
        // look up in our sequence
        const Sequence< Type > aSupportedTypes( getSupportedValueTypes(rGuard) );
        for ( auto const & i : aSupportedTypes )
            if ( aType == i )
                return true;

        return false;
    }

    Any SAL_CALL OCellValueBinding::getValue( const Type& aType )
    {
        std::unique_lock<std::mutex> aGuard(m_aMutex);
        throwIfDisposed(aGuard);
        checkInitialized( );
        checkValueType( aGuard, aType );

        Any aReturn;
        switch ( aType.getTypeClass() )
        {
        case TypeClass_STRING:
            OSL_ENSURE( m_xCellText.is(), "OCellValueBinding::getValue: don't have a text!" );
            if ( m_xCellText.is() )
                aReturn <<= m_xCellText->getString();
            else
                aReturn <<= OUString();
            break;

        case TypeClass_BOOLEAN:
            OSL_ENSURE( m_xCell.is(), "OCellValueBinding::getValue: don't have a double value supplier!" );
            if ( m_xCell.is() )
            {
                // check if the cell has a numeric value (this might go into a helper function):

                bool bHasValue = false;
                CellContentType eCellType = m_xCell->getType();
                if ( eCellType == CellContentType_VALUE )
                    bHasValue = true;
                else if ( eCellType == CellContentType_FORMULA )
                {
                    // check if the formula result is a value
                    if ( m_xCell->getError() == 0 )
                    {
                        Reference<XPropertySet> xProp( m_xCell, UNO_QUERY );
                        if ( xProp.is() )
                        {
                            sal_Int32 nResultType;
                            if ( (xProp->getPropertyValue(u"FormulaResultType2"_ustr) >>= nResultType)
                                    && nResultType == FormulaResult::VALUE )
                                bHasValue = true;
                        }
                    }
                }

                if ( bHasValue )
                {
                    // 0 is "unchecked", any other value is "checked", regardless of number format
                    double nCellValue = m_xCell->getValue();
                    bool bBoolValue = ( nCellValue != 0.0 );
                    aReturn <<= bBoolValue;
                }
                // empty cells, text cells and text or error formula results: leave return value empty
            }
            break;

        case TypeClass_DOUBLE:
            OSL_ENSURE( m_xCell.is(), "OCellValueBinding::getValue: don't have a double value supplier!" );
            if ( m_xCell.is() )
                aReturn <<= m_xCell->getValue();
            else
                aReturn <<= double(0);
            break;

        case TypeClass_LONG:
            OSL_ENSURE( m_xCell.is(), "OCellValueBinding::getValue: don't have a double value supplier!" );
            if ( m_xCell.is() )
            {
                // The list position value in the cell is 1-based.
                // We subtract 1 from any cell value (no special handling for 0 or negative values).

                sal_Int32 nValue = static_cast<sal_Int32>(rtl::math::approxFloor( m_xCell->getValue() ));
                --nValue;

                aReturn <<= nValue;
            }
            else
                aReturn <<= sal_Int32(0);
            break;

        default:
            OSL_FAIL( "OCellValueBinding::getValue: unreachable code!" );
                // a type other than double and string should never have survived the checkValueType
                // above
        }
        return aReturn;
    }

    void SAL_CALL OCellValueBinding::setValue( const Any& aValue )
    {
        std::unique_lock<std::mutex> aGuard(m_aMutex);
        throwIfDisposed(aGuard);
        checkInitialized( );
        if ( aValue.hasValue() )
            checkValueType( aGuard, aValue.getValueType() );

        switch ( aValue.getValueTypeClass() )
        {
        case TypeClass_STRING:
            {
                OSL_ENSURE( m_xCellText.is(), "OCellValueBinding::setValue: don't have a text!" );

                OUString sText;
                aValue >>= sText;
                if ( m_xCellText.is() )
                {
                    // might call back into us via modified(EventObject&)
                    aGuard.unlock();
                    m_xCellText->setString( sText );
                    aGuard.lock();
                }
            }
            break;

        case TypeClass_BOOLEAN:
            {
                OSL_ENSURE( m_xCell.is(), "OCellValueBinding::setValue: don't have a double value supplier!" );

                // boolean is stored as values 0 or 1
                // TODO: set the number format to boolean if no format is set?

                bool bValue( false );
                aValue >>= bValue;
                double nCellValue = bValue ? 1.0 : 0.0;

                if ( m_xCell.is() )
                {
                    // might call back into us via modified(EventObject&)
                    aGuard.unlock();
                    m_xCell->setValue( nCellValue );
                    aGuard.lock();
                }

                setBooleanFormat();
            }
            break;

        case TypeClass_DOUBLE:
            {
                OSL_ENSURE( m_xCell.is(), "OCellValueBinding::setValue: don't have a double value supplier!" );

                double nValue = 0;
                aValue >>= nValue;
                if ( m_xCell.is() )
                {
                    // might call back into us via modified(EventObject&)
                    aGuard.unlock();
                    m_xCell->setValue( nValue );
                    aGuard.lock();
                }
            }
            break;

        case TypeClass_LONG:
            {
                OSL_ENSURE( m_xCell.is(), "OCellValueBinding::setValue: don't have a double value supplier!" );

                sal_Int32 nValue = 0;
                aValue >>= nValue;      // list index from control layer (0-based)
                ++nValue;               // the list position value in the cell is 1-based
                if ( m_xCell.is() )
                {
                    // might call back into us via modified(EventObject&)
                    aGuard.unlock();
                    m_xCell->setValue( nValue );
                    aGuard.lock();
                }
            }
            break;

        case TypeClass_VOID:
            {
                // #N/A error value can only be set using XCellRangeData

                Reference<XCellRangeData> xData( m_xCell, UNO_QUERY );
                OSL_ENSURE( xData.is(), "OCellValueBinding::setValue: don't have XCellRangeData!" );
                if ( xData.is() )
                {
                    Sequence<Any> aInner(1);                            // one empty element
                    Sequence< Sequence<Any> > aOuter( &aInner, 1 );     // one row
                    // might call back into us via modified(EventObject&)
                    aGuard.unlock();
                    xData->setDataArray( aOuter );
                    aGuard.lock();
                }
            }
            break;

        default:
            OSL_FAIL( "OCellValueBinding::setValue: unreachable code!" );
                // a type other than double and string should never have survived the checkValueType
                // above
        }
    }

    void OCellValueBinding::setBooleanFormat()
    {
        // set boolean number format if not already set

        OUString sPropName( u"NumberFormat"_ustr );
        Reference<XPropertySet> xCellProp( m_xCell, UNO_QUERY );
        Reference<XNumberFormatsSupplier> xSupplier( m_xDocument, UNO_QUERY );
        if ( !(xSupplier.is() && xCellProp.is()) )
            return;

        Reference<XNumberFormats> xFormats(xSupplier->getNumberFormats());
        Reference<XNumberFormatTypes> xTypes( xFormats, UNO_QUERY );
        if ( !xTypes.is() )
            return;

        lang::Locale aLocale;
        bool bWasBoolean = false;

        sal_Int32 nOldIndex = ::comphelper::getINT32( xCellProp->getPropertyValue( sPropName ) );
        Reference<XPropertySet> xOldFormat;
        try
        {
            xOldFormat.set(xFormats->getByKey( nOldIndex ));
        }
        catch ( Exception& )
        {
            // non-existing format - can happen, use defaults
        }
        if ( xOldFormat.is() )
        {
            // use the locale of the existing format
            xOldFormat->getPropertyValue(u"Locale"_ustr) >>= aLocale;

            sal_Int16 nOldType = ::comphelper::getINT16(
                xOldFormat->getPropertyValue(u"Type"_ustr) );
            if ( nOldType & NumberFormat::LOGICAL )
                bWasBoolean = true;
        }

        if ( !bWasBoolean )
        {
            sal_Int32 nNewIndex = xTypes->getStandardFormat( NumberFormat::LOGICAL, aLocale );
            xCellProp->setPropertyValue( sPropName, Any( nNewIndex ) );
        }
    }

    void OCellValueBinding::checkInitialized()
    {
        if ( !m_bInitialized )
            throw NotInitializedException(u"CellValueBinding is not initialized"_ustr, getXWeak());
    }

    void OCellValueBinding::checkValueType( std::unique_lock<std::mutex>& rGuard, const Type& _rType ) const
    {
        if ( !supportsType( rGuard, _rType ) )
        {
            OUString sMessage = "The given type (" +
                _rType.getTypeName() +
                ") is not supported by this binding.";
                // TODO: localize this error message

            throw IncompatibleTypesException( sMessage, const_cast<OCellValueBinding&>(*this) );
                // TODO: alternatively use a type converter service for this?
        }
    }

    OUString SAL_CALL OCellValueBinding::getImplementationName(  )
    {
        return u"com.sun.star.comp.sheet.OCellValueBinding"_ustr;
    }

    sal_Bool SAL_CALL OCellValueBinding::supportsService( const OUString& _rServiceName )
    {
        return cppu::supportsService(this, _rServiceName);
    }

    Sequence< OUString > SAL_CALL OCellValueBinding::getSupportedServiceNames(  )
    {
        Sequence< OUString > aServices( m_bListPos ? 3 : 2 );
        auto pServices = aServices.getArray();
        pServices[ 0 ] = "com.sun.star.table.CellValueBinding";
        pServices[ 1 ] = "com.sun.star.form.binding.ValueBinding";
        if ( m_bListPos )
            pServices[ 2 ] = "com.sun.star.table.ListPositionCellBinding";
        return aServices;
    }

    void SAL_CALL OCellValueBinding::addModifyListener( const Reference< XModifyListener >& _rxListener )
    {
       if ( _rxListener.is() )
       {
           std::unique_lock<std::mutex> aGuard(m_aMutex);
           m_aModifyListeners.addInterface( aGuard, _rxListener );
       }
    }

    void SAL_CALL OCellValueBinding::removeModifyListener( const Reference< XModifyListener >& _rxListener )
    {
       if ( _rxListener.is() )
       {
           std::unique_lock<std::mutex> aGuard(m_aMutex);
           m_aModifyListeners.removeInterface( aGuard, _rxListener );
       }
    }

    void OCellValueBinding::notifyModified()
    {
        EventObject aEvent;
        aEvent.Source.set(*this);

        std::unique_lock<std::mutex> aGuard(m_aMutex);
        m_aModifyListeners.forEach(aGuard,
            [&aEvent] (const css::uno::Reference<css::util::XModifyListener> & l)
            {
                try
                {
                    l->modified( aEvent );
                }
                catch( const RuntimeException& )
                {
                    // silent this
                }
                catch( const Exception& )
                {
                    TOOLS_WARN_EXCEPTION( "sc", "OCellValueBinding::notifyModified: caught a (non-runtime) exception!" );
                }
            });
    }

    void SAL_CALL OCellValueBinding::modified( const EventObject& /* aEvent */ )
    {
        notifyModified();
    }

    void SAL_CALL OCellValueBinding::disposing( const EventObject& aEvent )
    {
        Reference<XInterface> xCellInt( m_xCell, UNO_QUERY );
        if ( xCellInt == aEvent.Source )
        {
            // release references to cell object
            m_xCell.clear();
            m_xCellText.clear();
        }
    }

    void SAL_CALL OCellValueBinding::initialize( const Sequence< Any >& _rArguments )
    {
        if ( m_bInitialized )
            throw RuntimeException(u"CellValueBinding is already initialized"_ustr, getXWeak());

        // get the cell address
        CellAddress aAddress;
        bool bFoundAddress = false;

        for ( const Any& rArg : _rArguments )
        {
            NamedValue aValue;
            if ( rArg >>= aValue )
            {
                if ( aValue.Name == "BoundCell" )
                {
                    if ( aValue.Value >>= aAddress )
                    {
                        bFoundAddress = true;
                        break;
                    }
                }
            }
        }

        if ( !bFoundAddress )
            throw RuntimeException(u"Cell not found"_ustr, getXWeak());

        // get the cell object
        try
        {
            // first the sheets collection
            Reference< XIndexAccess > xSheets;
            if ( m_xDocument.is() )
                xSheets.set(m_xDocument->getSheets( ), css::uno::UNO_QUERY);
            OSL_ENSURE( xSheets.is(), "OCellValueBinding::initialize: could not retrieve the sheets!" );

            if ( xSheets.is() )
            {
                // the concrete sheet
                Reference< XCellRange > xSheet(xSheets->getByIndex( aAddress.Sheet ), UNO_QUERY);
                OSL_ENSURE( xSheet.is(), "OCellValueBinding::initialize: NULL sheet, but no exception!" );

                // the concrete cell
                if ( xSheet.is() )
                {
                    m_xCell.set(xSheet->getCellByPosition( aAddress.Column, aAddress.Row ));
                    Reference< XCellAddressable > xAddressAccess( m_xCell, UNO_QUERY );
                    OSL_ENSURE( xAddressAccess.is(), "OCellValueBinding::initialize: either NULL cell, or cell without address access!" );
                }
            }
        }
        catch( const Exception& )
        {
            TOOLS_WARN_EXCEPTION( "sc", "OCellValueBinding::initialize: caught an exception while retrieving the cell object!" );
        }

        if ( !m_xCell.is() )
            throw RuntimeException(u"Failed to retrieve cell object"_ustr, getXWeak());

        m_xCellText.set(m_xCell, css::uno::UNO_QUERY);

        Reference<XModifyBroadcaster> xBroadcaster( m_xCell, UNO_QUERY );
        if ( xBroadcaster.is() )
        {
            xBroadcaster->addModifyListener( this );
        }

        // TODO: add as XEventListener to the cell, so we get notified when it dies,
        // and can dispose ourself then

        // TODO: somehow add as listener so we get notified when the address of the cell changes
        // We need to forward this as change in our BoundCell property to our property change listeners

        // TODO: be an XModifyBroadcaster, so that changes in our cell can be notified
        // to the BindableValue which is/will be bound to this instance.

        m_bInitialized = true;
        // TODO: place your code here
    }

}   // namespace calc

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