/* -*- 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 <sal/log.hxx>

#include <connectivity/TTableHelper.hxx>
#include <com/sun/star/sdb/tools/XTableRename.hpp>
#include <com/sun/star/sdb/tools/XTableAlteration.hpp>
#include <com/sun/star/sdb/tools/XKeyAlteration.hpp>
#include <com/sun/star/sdb/tools/XIndexAlteration.hpp>
#include <sdbcx/VKey.hxx>
#include <com/sun/star/sdbc/XRow.hpp>
#include <com/sun/star/sdbc/XResultSet.hpp>
#include <com/sun/star/sdbcx/KeyType.hpp>
#include <cppuhelper/implbase.hxx>
#include <com/sun/star/lang/XMultiServiceFactory.hpp>
#include <comphelper/types.hxx>
#include <connectivity/dbtools.hxx>
#include <connectivity/sdbcx/VCollection.hxx>
#include <unotools/sharedunocomponent.hxx>
#include <TConnection.hxx>

#include <o3tl/functional.hxx>

#include <iterator>
#include <set>

using namespace ::comphelper;
using namespace connectivity;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::beans;
using namespace ::com::sun::star::sdbcx;
using namespace ::com::sun::star::sdbc;
using namespace ::com::sun::star::container;
using namespace ::com::sun::star::lang;

namespace
{
    /// helper class for column property change events which holds the OComponentDefinition weak
class OTableContainerListener:
    public ::cppu::WeakImplHelper< XContainerListener >
{
    OTableHelper* m_pComponent;
    std::map< OUString,bool> m_aRefNames;

protected:
    virtual ~OTableContainerListener() override {}
public:
    explicit OTableContainerListener(OTableHelper* _pComponent) : m_pComponent(_pComponent){}
    // noncopyable
    OTableContainerListener(const OTableContainerListener&) = delete;
    const OTableContainerListener& operator=(const OTableContainerListener&) = delete;
    virtual void SAL_CALL elementInserted( const css::container::ContainerEvent& /*Event*/ ) override
    {
    }
    virtual void SAL_CALL elementRemoved( const css::container::ContainerEvent& Event ) override
    {
        // tdf#137745, perhaps connectivity::OTableHelper::disposing() has been called
        // which called OTableContainerListener::clear(), so m_pComponent may be null
        if (m_pComponent == nullptr)
            return;

        OUString sName;
        Event.Accessor  >>= sName;
        if ( m_aRefNames.find(sName) != m_aRefNames.end() )
            m_pComponent->refreshKeys();
    }
    virtual void SAL_CALL elementReplaced( const css::container::ContainerEvent& Event ) override
    {
        OUString sOldComposedName,sNewComposedName;
        Event.ReplacedElement   >>= sOldComposedName;
        Event.Accessor          >>= sNewComposedName;
        if ( sOldComposedName != sNewComposedName && m_aRefNames.find(sOldComposedName) != m_aRefNames.end() )
            m_pComponent->refreshKeys();
    }
    // XEventListener
    virtual void SAL_CALL disposing( const EventObject& /*_rSource*/ ) override
    {
    }
    void clear() { m_pComponent = nullptr; }
    void add(const OUString& _sRefName) { m_aRefNames.emplace(_sRefName,true); }
};
}
namespace connectivity
{
    static OUString lcl_getServiceNameForSetting(const Reference< css::sdbc::XConnection >& _xConnection,const OUString& i_sSetting)
    {
        OUString sSupportService;
        Any aValue;
        if ( ::dbtools::getDataSourceSetting(_xConnection,i_sSetting,aValue) )
        {
            aValue >>= sSupportService;
        }
        return sSupportService;
    }
    struct OTableHelperImpl
    {
        TKeyMap  m_aKeys;
        // helper services which can be provided by extensions
        Reference< css::sdb::tools::XTableRename>      m_xRename;
        Reference< css::sdb::tools::XTableAlteration>  m_xAlter;
        Reference< css::sdb::tools::XKeyAlteration>    m_xKeyAlter;
        Reference< css::sdb::tools::XIndexAlteration>  m_xIndexAlter;

        Reference< css::sdbc::XDatabaseMetaData >      m_xMetaData;
        Reference< css::sdbc::XConnection >            m_xConnection;
        rtl::Reference<OTableContainerListener>        m_xTablePropertyListener;
        std::vector< ColumnDesc >                    m_aColumnDesc;
        explicit OTableHelperImpl(const Reference< css::sdbc::XConnection >& _xConnection)
            : m_xConnection(_xConnection)
        {
            try
            {
                m_xMetaData = m_xConnection->getMetaData();
                Reference<XMultiServiceFactory> xFac(_xConnection,UNO_QUERY);
                if ( xFac.is() )
                {
                    m_xRename.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,u"TableRenameServiceName"_ustr)),UNO_QUERY);
                    m_xAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,u"TableAlterationServiceName"_ustr)),UNO_QUERY);
                    m_xKeyAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,u"KeyAlterationServiceName"_ustr)),UNO_QUERY);
                    m_xIndexAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,u"IndexAlterationServiceName"_ustr)),UNO_QUERY);
                }
            }
            catch(const Exception&)
            {
            }
        }
    };
}

OTableHelper::OTableHelper( sdbcx::OCollection* _pTables,
                           const Reference< XConnection >& _xConnection,
                           bool _bCase)
    :OTable_TYPEDEF(_pTables,_bCase)
    ,m_pImpl(new OTableHelperImpl(_xConnection))
{
}

OTableHelper::OTableHelper( sdbcx::OCollection* _pTables,
                            const Reference< XConnection >& _xConnection,
                            bool _bCase,
                            const OUString& Name,
                            const OUString& Type,
                            const OUString& Description ,
                            const OUString& SchemaName,
                            const OUString& CatalogName
                        ) : OTable_TYPEDEF(_pTables,
                                            _bCase,
                                            Name,
                                          Type,
                                          Description,
                                          SchemaName,
                                          CatalogName)
                        ,m_pImpl(new OTableHelperImpl(_xConnection))
{
}

OTableHelper::~OTableHelper()
{
}

void SAL_CALL OTableHelper::disposing()
{
    ::osl::MutexGuard aGuard(m_aMutex);
    if ( m_pImpl->m_xTablePropertyListener.is() )
    {
        m_pTables->removeContainerListener(m_pImpl->m_xTablePropertyListener);
        m_pImpl->m_xTablePropertyListener->clear();
        m_pImpl->m_xTablePropertyListener.clear();
    }
    OTable_TYPEDEF::disposing();

    m_pImpl->m_xConnection  = nullptr;
    m_pImpl->m_xMetaData    = nullptr;

}


namespace
{
    /** collects ColumnDesc's from a resultset produced by XDatabaseMetaData::getColumns
    */
    void lcl_collectColumnDescs_throw( const Reference< XResultSet >& _rxResult, std::vector< ColumnDesc >& _out_rColumns )
    {
        Reference< XRow > xRow( _rxResult, UNO_QUERY_THROW );
        while ( _rxResult->next() )
        {
            // tdf#162227: ODBC SQLGetData requires that data must be retrieved in increasing
            // column number order, unless the driver supports SQL_GD_ANY_ORDER extension (see
            // https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgetdata-function).
            // We can't emplace_back(getString(4), getInt(5), ..., getString(12), getInt(17)),
            // because MSVC would reorder calls into getInt(17) -> getString(12) -> ..., and then
            // MS SQL Server ODBC driver will give error on access of column 12 after column 17.
            OUString sName = xRow->getString(4); // COLUMN_NAME
            sal_Int32 nField5 = xRow->getInt(5);
            OUString aField6 = xRow->getString(6);
            sal_Int32 nField7 = xRow->getInt(7);
            sal_Int32 nField9 = xRow->getInt(9);
            sal_Int32 nField11 = xRow->getInt(11);
            OUString sField12 = xRow->getString(12);
            OUString sField13 = xRow->getString(13);
            OrdinalPosition nOrdinalPosition = xRow->getInt(17); // ORDINAL_POSITION
            _out_rColumns.emplace_back(sName, nField5, aField6, nField7, nField9, nField11,
                                       sField12, sField13, nOrdinalPosition);
        }
    }

    /** checks a given array of ColumnDesc's whether it has reasonable ordinal positions. If not,
        they will be normalized to be the array index.
    */
    void lcl_sanitizeColumnDescs( std::vector< ColumnDesc >& _rColumns )
    {
        if ( _rColumns.empty() )
            return;

        // collect all used ordinals
        std::set< OrdinalPosition > aUsedOrdinals;
        for ( const auto& collect : _rColumns )
            aUsedOrdinals.insert( collect.nOrdinalPosition );

        // we need to have as much different ordinals as we have different columns
        bool bDuplicates = aUsedOrdinals.size() != _rColumns.size();
        // and it needs to be a continuous range
        size_t nOrdinalsRange = *aUsedOrdinals.rbegin() - *aUsedOrdinals.begin() + 1;
        bool bGaps = nOrdinalsRange != _rColumns.size();

        // if that's not the case, normalize it
        if ( bGaps || bDuplicates )
        {
            OSL_FAIL( "lcl_sanitizeColumnDescs: database did provide invalid ORDINAL_POSITION values!" );

            OrdinalPosition nNormalizedPosition = 1;
            for ( auto& normalize : _rColumns )
                normalize.nOrdinalPosition = nNormalizedPosition++;
            return;
        }

        // what's left is that the range might not be from 1 to <column count>, but for instance
        // 0 to <column count>-1.
        size_t nOffset = *aUsedOrdinals.begin() - 1;
        for ( auto& offset : _rColumns )
            offset.nOrdinalPosition -= nOffset;
    }
}


void OTableHelper::refreshColumns()
{
    ::std::vector< OUString> aVector;
    if(!isNew())
    {
        Any aCatalog;
        if ( !m_CatalogName.isEmpty() )
            aCatalog <<= m_CatalogName;

        ::utl::SharedUNOComponent< XResultSet > xResult( getMetaData()->getColumns(
            aCatalog,
            m_SchemaName,
            m_Name,
            u"%"_ustr
        ) );

        // collect the column names, together with their ordinal position
        m_pImpl->m_aColumnDesc.clear();
        lcl_collectColumnDescs_throw( xResult, m_pImpl->m_aColumnDesc );

        // ensure that the ordinal positions as obtained from the meta data do make sense
        lcl_sanitizeColumnDescs( m_pImpl->m_aColumnDesc );

        // sort by ordinal position
        std::map< OrdinalPosition, OUString > aSortedColumns;
        for (const auto& copy : m_pImpl->m_aColumnDesc)
            aSortedColumns[ copy.nOrdinalPosition ] = copy.sName;

        // copy them to aVector, now that we have the proper ordering
        std::transform(
            aSortedColumns.begin(),
            aSortedColumns.end(),
            std::insert_iterator< ::std::vector< OUString> >( aVector, aVector.begin() ),
            ::o3tl::select2nd< std::map< OrdinalPosition, OUString >::value_type >()
            );
    }

    if(m_xColumns)
        m_xColumns->reFill(aVector);
    else
        m_xColumns.reset(createColumns(aVector));
}

const ColumnDesc* OTableHelper::getColumnDescription(const OUString& _sName) const
{
    const ColumnDesc* pRet = nullptr;
    auto aIter = std::find_if(m_pImpl->m_aColumnDesc.begin(), m_pImpl->m_aColumnDesc.end(),
        [&_sName](const ColumnDesc& rColumnDesc) { return rColumnDesc.sName == _sName; });
    if (aIter != m_pImpl->m_aColumnDesc.end())
        pRet = &*aIter;
    return pRet;
}

void OTableHelper::refreshPrimaryKeys(::std::vector< OUString>& _rNames)
{
    Any aCatalog;
    if ( !m_CatalogName.isEmpty() )
        aCatalog <<= m_CatalogName;
    Reference< XResultSet > xResult = getMetaData()->getPrimaryKeys(aCatalog,m_SchemaName,m_Name);

    if ( xResult.is() )
    {
        auto pKeyProps = std::make_shared<sdbcx::KeyProperties>(OUString(),KeyType::PRIMARY,0,0);
        OUString aPkName;
        bool bAlreadyFetched = false;
        const Reference< XRow > xRow(xResult,UNO_QUERY);
        while ( xResult->next() )
        {
            pKeyProps->m_aKeyColumnNames.push_back(xRow->getString(4));
            if ( !bAlreadyFetched )
            {
                aPkName = xRow->getString(6);
                SAL_WARN_IF(xRow->wasNull(),"connectivity.commontools", "NULL Primary Key name");
                SAL_WARN_IF(aPkName.isEmpty(),"connectivity.commontools", "empty Primary Key name");
                bAlreadyFetched = true;
            }
        }

        if(bAlreadyFetched)
        {
            SAL_WARN_IF(aPkName.isEmpty(),"connectivity.commontools", "empty Primary Key name");
            SAL_WARN_IF(pKeyProps->m_aKeyColumnNames.empty(),"connectivity.commontools", "Primary Key has no columns");
            m_pImpl->m_aKeys.emplace(aPkName,pKeyProps);
            _rNames.push_back(aPkName);
        }
    } // if ( xResult.is() && xResult->next() )
    ::comphelper::disposeComponent(xResult);
}

void OTableHelper::refreshForeignKeys(::std::vector< OUString>& _rNames)
{
    Any aCatalog;
    if ( !m_CatalogName.isEmpty() )
        aCatalog <<= m_CatalogName;
    Reference< XResultSet > xResult = getMetaData()->getImportedKeys(aCatalog,m_SchemaName,m_Name);
    Reference< XRow > xRow(xResult,UNO_QUERY);

    if ( !xRow.is() )
        return;

    std::shared_ptr<sdbcx::KeyProperties> pKeyProps;
    OUString aName,sCatalog,aSchema,sOldFKName;
    while( xResult->next() )
    {
        // this must be outside the "if" because we have to call in a right order
        sCatalog    = xRow->getString(1);
        if ( xRow->wasNull() )
            sCatalog.clear();
        aSchema     = xRow->getString(2);
        aName       = xRow->getString(3);

        const OUString sForeignKeyColumn = xRow->getString(8);
        const sal_Int32 nUpdateRule = xRow->getInt(10);
        const sal_Int32 nDeleteRule = xRow->getInt(11);
        const OUString sFkName = xRow->getString(12);

        if ( !sFkName.isEmpty() && !xRow->wasNull() )
        {
            if ( sOldFKName != sFkName )
            {
                if ( pKeyProps )
                    m_pImpl->m_aKeys.emplace(sOldFKName,pKeyProps);

                const OUString sReferencedName = ::dbtools::composeTableName(getMetaData(),sCatalog,aSchema,aName,false,::dbtools::EComposeRule::InDataManipulation);
                pKeyProps = std::make_shared<sdbcx::KeyProperties>(sReferencedName,KeyType::FOREIGN,nUpdateRule,nDeleteRule);
                pKeyProps->m_aKeyColumnNames.push_back(sForeignKeyColumn);
                _rNames.push_back(sFkName);
                if ( m_pTables->hasByName(sReferencedName) )
                {
                    if ( !m_pImpl->m_xTablePropertyListener.is() )
                        m_pImpl->m_xTablePropertyListener = new OTableContainerListener(this);
                    m_pTables->addContainerListener(m_pImpl->m_xTablePropertyListener);
                    m_pImpl->m_xTablePropertyListener->add(sReferencedName);
                } // if ( m_pTables->hasByName(sReferencedName) )
                sOldFKName = sFkName;
            } // if ( sOldFKName != sFkName )
            else if ( pKeyProps )
            {
                pKeyProps->m_aKeyColumnNames.push_back(sForeignKeyColumn);
            }
        }
    } // while( xResult->next() )
    if ( pKeyProps )
        m_pImpl->m_aKeys.emplace(sOldFKName,pKeyProps);
    ::comphelper::disposeComponent(xResult);
}

void OTableHelper::refreshKeys()
{
    m_pImpl->m_aKeys.clear();

    ::std::vector< OUString> aNames;

    if(!isNew())
    {
        refreshPrimaryKeys(aNames);
        refreshForeignKeys(aNames);
        m_xKeys.reset(createKeys(aNames));
    } // if(!isNew())
    else if (!m_xKeys )
        m_xKeys.reset(createKeys(aNames));
    /*if(m_pKeys)
        m_pKeys->reFill(aVector);
    else*/

}

void OTableHelper::refreshIndexes()
{
    ::std::vector< OUString> aVector;
    if(!isNew())
    {
        // fill indexes
        Any aCatalog;
        if ( !m_CatalogName.isEmpty() )
            aCatalog <<= m_CatalogName;
        Reference< XResultSet > xResult = getMetaData()->getIndexInfo(aCatalog,m_SchemaName,m_Name,false,false);

        if(xResult.is())
        {
            Reference< XRow > xRow(xResult,UNO_QUERY);
            OUString sCatalogSep = getMetaData()->getCatalogSeparator();
            OUString sPreviousRoundName;
            while( xResult->next() )
            {
                OUString aName = xRow->getString(5);
                if(!aName.isEmpty())
                    aName += sCatalogSep;
                aName += xRow->getString(6);
                if ( !aName.isEmpty() )
                {
                    // don't insert the name if the last one we inserted was the same
                    if (sPreviousRoundName != aName)
                        aVector.push_back(aName);
                }
                sPreviousRoundName = aName;
            }
            ::comphelper::disposeComponent(xResult);
        }
    }

    if(m_xIndexes)
        m_xIndexes->reFill(aVector);
    else
        m_xIndexes.reset(createIndexes(aVector));
}

OUString OTableHelper::getRenameStart() const
{
    OUString sSql(u"RENAME "_ustr);
    if ( m_Type == "VIEW" )
        sSql += " VIEW ";
    else
        sSql += " TABLE ";

    return sSql;
}

// XRename
void SAL_CALL OTableHelper::rename( const OUString& newName )
{
    ::osl::MutexGuard aGuard(m_aMutex);
    checkDisposed(
#ifdef __GNUC__
        ::connectivity::sdbcx::OTableDescriptor_BASE::rBHelper.bDisposed
#else
        rBHelper.bDisposed
#endif
        );

    if(!isNew())
    {
        if ( m_pImpl->m_xRename.is() )
        {
            m_pImpl->m_xRename->rename(this,newName);
        }
        else
        {
            OUString sSql = getRenameStart();

            OUString sCatalog,sSchema,sTable;
            ::dbtools::qualifiedNameComponents(getMetaData(),newName,sCatalog,sSchema,sTable,::dbtools::EComposeRule::InDataManipulation);

            OUString sComposedName;
            sComposedName = ::dbtools::composeTableName(getMetaData(),m_CatalogName,m_SchemaName,m_Name,true,::dbtools::EComposeRule::InDataManipulation);
            sSql += sComposedName
                 + " TO ";
            sComposedName = ::dbtools::composeTableName(getMetaData(),sCatalog,sSchema,sTable,true,::dbtools::EComposeRule::InDataManipulation);
            sSql += sComposedName;

            Reference< XStatement > xStmt = m_pImpl->m_xConnection->createStatement(  );
            if ( xStmt.is() )
            {
                xStmt->execute(sSql);
                ::comphelper::disposeComponent(xStmt);
            }
        }

        OTable_TYPEDEF::rename(newName);
    }
    else
        ::dbtools::qualifiedNameComponents(getMetaData(),newName,m_CatalogName,m_SchemaName,m_Name,::dbtools::EComposeRule::InTableDefinitions);
}

Reference< XDatabaseMetaData> OTableHelper::getMetaData() const
{
    return m_pImpl->m_xMetaData;
}

void SAL_CALL OTableHelper::alterColumnByIndex( sal_Int32 index, const Reference< XPropertySet >& descriptor )
{
    ::osl::MutexGuard aGuard(m_aMutex);
    checkDisposed(
#ifdef __GNUC__
        ::connectivity::sdbcx::OTableDescriptor_BASE::rBHelper.bDisposed
#else
        rBHelper.bDisposed
#endif
        );

    Reference< XPropertySet > xOld(
        m_xColumns->getByIndex(index), css::uno::UNO_QUERY);
    if(xOld.is())
        alterColumnByName(getString(xOld->getPropertyValue(OMetaConnection::getPropMap().getNameByIndex(PROPERTY_ID_NAME))),descriptor);
}


OUString SAL_CALL OTableHelper::getName()
{
    OUString sComposedName = ::dbtools::composeTableName(getMetaData(),m_CatalogName,m_SchemaName,m_Name,false,::dbtools::EComposeRule::InDataManipulation);
    return sComposedName;
}

const OUString & OTableHelper::getTableName()
{
    return m_Name;
}

std::shared_ptr<sdbcx::KeyProperties> OTableHelper::getKeyProperties(const OUString& _sName) const
{
    std::shared_ptr<sdbcx::KeyProperties> pKeyProps;
    TKeyMap::const_iterator aFind = m_pImpl->m_aKeys.find(_sName);
    if ( aFind != m_pImpl->m_aKeys.end() )
    {
        pKeyProps = aFind->second;
    }
    else // only a fall back
    {
        OSL_FAIL("No key with the given name found");
        pKeyProps = std::make_shared<sdbcx::KeyProperties>();
    }

    return pKeyProps;
}

void OTableHelper::addKey(const OUString& _sName,const std::shared_ptr<sdbcx::KeyProperties>& _aKeyProperties)
{
    m_pImpl->m_aKeys.emplace(_sName,_aKeyProperties);
}

OUString OTableHelper::getTypeCreatePattern() const
{
    return OUString();
}

Reference< XConnection> const & OTableHelper::getConnection() const
{
    return m_pImpl->m_xConnection;
}

Reference< css::sdb::tools::XTableRename> const & OTableHelper::getRenameService() const
{
    return m_pImpl->m_xRename;
}

Reference< css::sdb::tools::XTableAlteration> const & OTableHelper::getAlterService() const
{
    return m_pImpl->m_xAlter;
}

Reference< css::sdb::tools::XKeyAlteration> const & OTableHelper::getKeyService() const
{
    return m_pImpl->m_xKeyAlter;
}

Reference< css::sdb::tools::XIndexAlteration> const & OTableHelper::getIndexService() const
{
    return m_pImpl->m_xIndexAlter;
}


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