/* -*- 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 "subcomponentmanager.hxx"
#include "AppController.hxx"
#include <strings.hxx>

#include <com/sun/star/frame/XFrame.hpp>
#include <com/sun/star/frame/XModel.hpp>
#include <com/sun/star/frame/XModel2.hpp>
#include <com/sun/star/util/XCloseable.hpp>
#include <com/sun/star/awt/XTopWindow.hpp>
#include <com/sun/star/embed/XComponentSupplier.hpp>
#include <com/sun/star/ucb/XCommandProcessor.hpp>
#include <com/sun/star/document/XDocumentEventBroadcaster.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>

#include <comphelper/diagnose_ex.hxx>
#include <dbaccess/dataview.hxx>
#include <utility>
#include <vcl/svapp.hxx>
#include <osl/mutex.hxx>

#include <algorithm>

namespace dbaui
{

    using ::com::sun::star::uno::Reference;
    using ::com::sun::star::uno::UNO_QUERY;
    using ::com::sun::star::uno::UNO_QUERY_THROW;
    using ::com::sun::star::uno::UNO_SET_THROW;
    using ::com::sun::star::uno::Exception;
    using ::com::sun::star::uno::RuntimeException;
    using ::com::sun::star::uno::Any;
    using ::com::sun::star::uno::Sequence;
    using ::com::sun::star::frame::XFrame;
    using ::com::sun::star::frame::XController;
    using ::com::sun::star::frame::XModel;
    using ::com::sun::star::lang::EventObject;
    using ::com::sun::star::lang::XComponent;
    using ::com::sun::star::frame::XModel2;
    using ::com::sun::star::container::XEnumeration;
    using ::com::sun::star::util::XCloseable;
    using ::com::sun::star::awt::XTopWindow;
    using ::com::sun::star::embed::XComponentSupplier;
    using ::com::sun::star::ucb::XCommandProcessor;
    using ::com::sun::star::ucb::Command;
    using ::com::sun::star::document::XDocumentEventBroadcaster;
    using ::com::sun::star::beans::XPropertySet;
    using ::com::sun::star::beans::PropertyChangeEvent;

    // helper structs
    namespace
    {
        struct SubComponentDescriptor
        {
            /// the name of the sub component, empty if it is yet unsaved
            OUString sName;
            /// type of the component - an ElementType value, except for relation design
            sal_Int32       nComponentType;
            /// the mode in which the sub component has been opened
            ElementOpenMode eOpenMode;
            /// the frame which the component resides in. Must not be <NULL/>
            Reference< XFrame >             xFrame;
            /// the controller of the sub component. Must not be <NULL/>
            Reference< XController >        xController;
            /// the model of the sub component. Might be <NULL/>
            Reference< XModel >             xModel;
            /// the document definition which holds the component, if any; as CommandProcessor
            Reference< XCommandProcessor >  xComponentCommandProcessor;
            /// the document definition which holds the component, if any; as PropertySet
            Reference< XPropertySet >       xDocumentDefinitionProperties;

            SubComponentDescriptor()
                :nComponentType( -1 )
                ,eOpenMode( ElementOpenMode::Normal )
            {
            }

            SubComponentDescriptor( OUString i_sName, const sal_Int32 i_nComponentType,
                    const ElementOpenMode i_eOpenMode, const Reference< XComponent >& i_rComponent )
                :sName(std::move( i_sName ))
                ,nComponentType( i_nComponentType )
                ,eOpenMode( i_eOpenMode )
            {
                if ( !impl_constructFrom( i_rComponent ) )
                {
                    // i_rComponent is neither a model, nor a controller, nor a frame
                    // => it must be a css.sdb.DocumentDefinition
                    Reference< XComponentSupplier > xCompSupp( i_rComponent, UNO_QUERY_THROW );
                    Reference< XComponent > xComponent( xCompSupp->getComponent(), UNO_QUERY_THROW );
                    if ( !impl_constructFrom( xComponent ) )
                        throw RuntimeException(u"Illegal component type."_ustr );
                    xComponentCommandProcessor.set( i_rComponent, UNO_QUERY_THROW );
                    xDocumentDefinitionProperties.set( i_rComponent, UNO_QUERY_THROW );
                }
            }

            bool is() const { return xFrame.is(); }

        private:
            bool impl_constructFrom( const Reference< XComponent >& _rxComponent )
            {
                // is it a model?
                xModel.set( _rxComponent, UNO_QUERY );
                if ( xModel.is() )
                {
                    xController.set( xModel->getCurrentController() );
                    if ( xController.is() )
                        xFrame.set( xController->getFrame(), UNO_SET_THROW );
                }
                else
                {
                    // is it a controller?
                    xController.set( _rxComponent, UNO_QUERY );
                    if ( xController.is() )
                    {
                        xFrame.set( xController->getFrame(), UNO_SET_THROW );
                    }
                    else
                    {
                        // is it a frame?
                        xFrame.set( _rxComponent, UNO_QUERY );
                        if ( !xFrame.is() )
                            return false;

                        // ensure we have a controller
                        xController.set( xFrame->getController(), UNO_SET_THROW );
                    }

                    // check whether there is a model (not required)
                    xModel.set( xController->getModel() );
                }

                return true;
            }
        };

        struct SelectSubComponent
        {
            Reference< XComponent > operator()( const SubComponentDescriptor &_desc ) const
            {
                if ( _desc.xModel.is() )
                    return _desc.xModel;
                OSL_ENSURE( _desc.xController.is(), "SelectSubComponent::operator(): illegal component!" );
                return _desc.xController;
            }
        };

        typedef std::vector< SubComponentDescriptor > SubComponents;

        struct SubComponentMatch
        {
        public:
            SubComponentMatch( OUString i_sName, const sal_Int32 i_nComponentType,
                    const ElementOpenMode i_eOpenMode )
                :m_sName(std::move( i_sName ))
                ,m_nComponentType( i_nComponentType )
                ,m_eOpenMode( i_eOpenMode )
            {
            }

            bool operator()( const SubComponentDescriptor& i_rCompareWith ) const
            {
                return  (   m_sName             ==  i_rCompareWith.sName          )
                    &&  (   m_nComponentType    ==  i_rCompareWith.nComponentType )
                    &&  (   m_eOpenMode         ==  i_rCompareWith.eOpenMode      );
            }
        private:
            const OUString   m_sName;
            const sal_Int32         m_nComponentType;
            const ElementOpenMode   m_eOpenMode;
        };
    }

    // SubComponentManager_Data
    struct SubComponentManager_Data
    {
        SubComponentManager_Data( OApplicationController& _rController, ::comphelper::SharedMutex  _aMutex )
            :m_rController( _rController )
            ,m_aMutex(std::move( _aMutex ))
        {
        }

        OApplicationController&             m_rController;
        mutable ::comphelper::SharedMutex   m_aMutex;
        SubComponents                       m_aComponents;

        ::osl::Mutex&   getMutex() const { return m_aMutex; }
    };

    // SubComponentManager
    SubComponentManager::SubComponentManager( OApplicationController& _rController, const ::comphelper::SharedMutex& _rMutex )
        :m_pData( new SubComponentManager_Data( _rController, _rMutex ) )
    {
    }

    SubComponentManager::~SubComponentManager()
    {
    }

    void SubComponentManager::disposing()
    {
        ::osl::MutexGuard aGuard( m_pData->getMutex() );
        m_pData->m_aComponents.clear();
    }

    namespace
    {
        bool lcl_fallbackToAnotherController( SubComponentDescriptor& _rCompDesc )
        {
            Reference< XController > xFallback;
            OSL_PRECOND( _rCompDesc.xModel.is(), "lcl_fallbackToAnotherController: illegal call!" );
            if ( !_rCompDesc.xModel.is() )
                return false;

            xFallback.set( _rCompDesc.xModel->getCurrentController() );
            if ( xFallback == _rCompDesc.xController )
                // don't accept the very same controller as fallback
                xFallback.clear();

            if ( !xFallback.is() )
            {
                // perhaps XModel2 can be of help here
                Reference< XModel2 > xModel2( _rCompDesc.xModel, UNO_QUERY );
                Reference< XEnumeration > xControllerEnum;
                if ( xModel2.is() )
                    xControllerEnum = xModel2->getControllers();
                while ( xControllerEnum.is() && xControllerEnum->hasMoreElements() )
                {
                    xFallback.set( xControllerEnum->nextElement(), UNO_QUERY );
                    if ( xFallback == _rCompDesc.xController )
                        xFallback.clear();
                }
            }

            if ( xFallback.is() )
            {
                _rCompDesc.xController = xFallback;
                _rCompDesc.xFrame.set( xFallback->getFrame(), UNO_SET_THROW );
                return true;
            }

            return false;
        }

        bool lcl_closeComponent( const Reference< XCommandProcessor >& _rxCommandProcessor )
        {
            bool bSuccess = false;
            try
            {
                sal_Int32 nCommandIdentifier = _rxCommandProcessor->createCommandIdentifier();

                Command aCommand;
                aCommand.Name = "close";
                _rxCommandProcessor->execute( aCommand, nCommandIdentifier, nullptr );
                bSuccess = true;
            }
            catch( const Exception& )
            {
                DBG_UNHANDLED_EXCEPTION("dbaccess");
            }
            return bSuccess;
        }

        bool lcl_closeComponent( const SubComponentDescriptor& _rComponent )
        {
            if ( _rComponent.xComponentCommandProcessor.is() )
                return lcl_closeComponent( _rComponent.xComponentCommandProcessor );

            Reference< XController > xController( _rComponent.xController );
            OSL_ENSURE( xController.is(), "lcl_closeComponent: invalid controller!" );

            // suspend the controller in the document
            if ( xController.is() )
                if ( !xController->suspend( true ) )
                    return false;

            bool bSuccess = false;
            try
            {
                Reference< XCloseable > xCloseable( _rComponent.xFrame, UNO_QUERY_THROW );
                xCloseable->close( true );
                bSuccess = true;
            }
            catch( const Exception& )
            {
                DBG_UNHANDLED_EXCEPTION("dbaccess");
            }
            return bSuccess;
        }

        void lcl_notifySubComponentEvent( const SubComponentManager_Data& _rData, const OUString& _rAsciiEventName,
                const SubComponentDescriptor& _rComponent )
        {
            try
            {
                Reference< XDocumentEventBroadcaster > xBroadcaster( _rData.m_rController.getModel(), UNO_QUERY_THROW );
                xBroadcaster->notifyDocumentEvent(
                    _rAsciiEventName,
                    &_rData.m_rController,
                    Any( _rComponent.xFrame )
                );
            }
            catch( const Exception& )
            {
                DBG_UNHANDLED_EXCEPTION("dbaccess");
            }
        }
    }

    void SAL_CALL SubComponentManager::propertyChange( const PropertyChangeEvent& i_rEvent )
    {
        if ( i_rEvent.PropertyName != PROPERTY_NAME )
            // by definition, it's allowed to broadcast more than what we've registered for
            return;

        // find the sub component whose name changed
        for (auto & component : m_pData->m_aComponents)
        {
            if ( component.xDocumentDefinitionProperties != i_rEvent.Source )
                continue;

            OUString sNewName;
            OSL_VERIFY( i_rEvent.NewValue >>= sNewName );

        #if OSL_DEBUG_LEVEL > 0
            OUString sOldKnownName( component.sName );
            OUString sOldName;
            OSL_VERIFY( i_rEvent.OldValue >>= sOldName );
            OSL_ENSURE( sOldName == sOldKnownName, "SubComponentManager::propertyChange: inconsistency in the old names!" );
        #endif

            component.sName = sNewName;
            break;
        }
    }

    void SAL_CALL SubComponentManager::disposing( const EventObject& _rSource )
    {
        ::osl::ClearableMutexGuard aGuard( m_pData->getMutex() );

        SubComponentDescriptor aClosedComponent;

        for (   SubComponents::iterator comp = m_pData->m_aComponents.begin();
                comp != m_pData->m_aComponents.end();
                ++comp
            )
        {
            bool bRemove = false;

            if ( comp->xController == _rSource.Source )
            {
                if ( !comp->xModel.is() )
                {
                    bRemove = true;
                }
                else
                {
                    // maybe this is just one view to the sub document, and only this view is closed
                    if ( !lcl_fallbackToAnotherController( *comp ) )
                    {
                        bRemove = true;
                    }
                }
            }
            else if ( comp->xModel == _rSource.Source )
            {
                bRemove = true;
            }

            if ( bRemove )
            {
                aClosedComponent = *comp;
                m_pData->m_aComponents.erase( comp );
                break;
            }
        }

        if ( aClosedComponent.is() )
        {
            aGuard.clear();
            lcl_notifySubComponentEvent( *m_pData, u"OnSubComponentClosed"_ustr, aClosedComponent );
        }
    }

    Sequence< Reference< XComponent> > SubComponentManager::getSubComponents() const
    {
        ::osl::MutexGuard aGuard( m_pData->getMutex() );

        Sequence< Reference< XComponent > > aComponents( m_pData->m_aComponents.size() );
        std::transform(
            m_pData->m_aComponents.begin(),
            m_pData->m_aComponents.end(),
            aComponents.getArray(),
            SelectSubComponent()
        );
        return aComponents;
    }

    bool SubComponentManager::closeSubComponents()
    {
        SolarMutexGuard aSolarGuard;
        ::osl::MutexGuard aGuard( m_pData->getMutex() );

        try
        {
            SubComponents aWorkingCopy( m_pData->m_aComponents );
            for (auto const& elem : aWorkingCopy)
            {
                lcl_closeComponent(elem);
            }
        }
        catch ( const Exception& )
        {
            DBG_UNHANDLED_EXCEPTION("dbaccess");
        }

        return empty();
    }

    bool SubComponentManager::empty() const
    {
        ::osl::MutexGuard aGuard( m_pData->getMutex() );
        return m_pData->m_aComponents.empty();
    }

    void SubComponentManager::onSubComponentOpened( const OUString&  _rName, const sal_Int32 _nComponentType,
        const ElementOpenMode _eOpenMode, const Reference< XComponent >& _rxComponent )
    {
        ::osl::ClearableMutexGuard aGuard( m_pData->getMutex() );

#if OSL_DEBUG_LEVEL > 0
        if ( !_rName.isEmpty() )
        {
            // check there does not already exist such a component
            auto subComponentNotExists = std::none_of(
                m_pData->m_aComponents.begin(),
                m_pData->m_aComponents.end(),
                SubComponentMatch( _rName, _nComponentType, _eOpenMode )
            );
            OSL_ENSURE( subComponentNotExists, "already existent!" );
        }
#endif
        SubComponentDescriptor aElement( _rName, _nComponentType, _eOpenMode, _rxComponent );
        ENSURE_OR_THROW( aElement.xModel.is() || aElement.xController.is(), "illegal component" );

        m_pData->m_aComponents.push_back( aElement );

        // add as listener
        if ( aElement.xController.is() )
            aElement.xController->addEventListener( this );
        if ( aElement.xModel.is() )
            aElement.xModel->addEventListener( this );
        if ( aElement.xDocumentDefinitionProperties.is() )
            aElement.xDocumentDefinitionProperties->addPropertyChangeListener( PROPERTY_NAME, this );

        // notify this to interested parties
        aGuard.clear();
        lcl_notifySubComponentEvent( *m_pData, u"OnSubComponentOpened"_ustr, aElement );
    }

    bool SubComponentManager::activateSubFrame( const OUString& _rName, const sal_Int32 _nComponentType,
        const ElementOpenMode _eOpenMode, Reference< XComponent >& o_rComponent ) const
    {
        ::osl::MutexGuard aGuard( m_pData->getMutex() );

        SubComponents::const_iterator pos = std::find_if(
            m_pData->m_aComponents.begin(),
            m_pData->m_aComponents.end(),
            SubComponentMatch( _rName, _nComponentType, _eOpenMode )
        );
        if ( pos == m_pData->m_aComponents.end() )
            // no component with this name/type/open mode
            return false;

        const Reference< XFrame > xFrame( pos->xFrame, UNO_SET_THROW );
        const Reference< XTopWindow > xTopWindow( xFrame->getContainerWindow(), UNO_QUERY_THROW );
        xTopWindow->toFront();

        if ( pos->xModel.is() )
            o_rComponent = pos->xModel.get();
        else if ( pos->xController.is() )
            o_rComponent = pos->xController.get();
        else
            o_rComponent = pos->xFrame.get();

        return true;
    }

    bool SubComponentManager::closeSubFrames( std::u16string_view i_rName, const sal_Int32 _nComponentType )
    {
        ::osl::MutexGuard aGuard( m_pData->getMutex() );
        ENSURE_OR_RETURN_FALSE( !i_rName.empty(), "SubComponentManager::closeSubFrames: illegal name!" );

        SubComponents aWorkingCopy( m_pData->m_aComponents );
        for (auto const& elem : aWorkingCopy)
        {
            if ( ( elem.sName != i_rName ) || ( elem.nComponentType != _nComponentType ) )
                continue;

            if ( !lcl_closeComponent(elem) )
                return false;
        }

        return true;
    }

    bool SubComponentManager::lookupSubComponent( const Reference< XComponent >& i_rComponent,
            OUString& o_rName, sal_Int32& o_rComponentType )
    {
        for (auto const& component : m_pData->m_aComponents)
        {
            if  (   (   component.xModel.is()
                    &&  ( component.xModel == i_rComponent )
                    )
                ||  (   component.xController.is()
                    &&  ( component.xController == i_rComponent )
                    )
                ||  (   component.xFrame.is()
                    &&  ( component.xFrame == i_rComponent )
                    )
                )
            {
                o_rName = component.sName;
                o_rComponentType = component.nComponentType;
                return true;
            }
        }
        return false;
    }

} // namespace dbaui

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