/* -*- 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 <o3tl/numeric.hxx>

#include <svx/strings.hrc>
#include <svx/dialmgr.hxx>
#include <svx/svdhdl.hxx>
#include <svx/svdmodel.hxx>
#include <svx/svdobjkind.hxx>
#include <svx/scene3d.hxx>
#include <svx/obj3d.hxx>
#include <sdr/properties/e3dproperties.hxx>
#include <sdr/properties/e3dcompoundproperties.hxx>
#include <basegfx/polygon/b3dpolypolygontools.hxx>
#include <basegfx/point/b3dpoint.hxx>
#include <basegfx/matrix/b3dhommatrix.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <svx/helperhittest3d.hxx>
#include <sdr/contact/viewcontactofe3d.hxx>
#include <drawinglayer/geometry/viewinformation3d.hxx>
#include <com/sun/star/uno/Sequence.h>
#include <svx/sdr/contact/viewcontactofe3dscene.hxx>
#include <svx/e3dsceneupdater.hxx>
#include <comphelper/configuration.hxx>

using namespace com::sun::star;

std::unique_ptr<sdr::properties::BaseProperties> E3dObject::CreateObjectSpecificProperties()
{
    return std::make_unique<sdr::properties::E3dProperties>(*this);
}

E3dObject::E3dObject(SdrModel& rSdrModel)
:   SdrAttrObj(rSdrModel),
    mbTfHasChanged(true),
    mbIsSelected(false)
{
    m_bIs3DObj = true;
    m_bClosedObj = true;
}

E3dObject::E3dObject(SdrModel& rSdrModel, E3dObject const & rSource)
:   SdrAttrObj(rSdrModel, rSource),
    mbTfHasChanged(true),
    mbIsSelected(false)
{
    m_bIs3DObj = true;
    m_bClosedObj = true;

    // BoundVol can be copied since also the children are copied
    maLocalBoundVol  = rSource.maLocalBoundVol;
    maTransformation = rSource.maTransformation;

    // Because the parent may have changed, definitely redefine the total
    // transformation next time
    SetTransformChanged();

    // Copy selection status
    mbIsSelected = rSource.mbIsSelected;
}

E3dObject::~E3dObject()
{
}

void E3dObject::SetSelected(bool bNew)
{
    if(mbIsSelected != bNew)
    {
        mbIsSelected = bNew;
    }
}

// Break, default implementations
bool E3dObject::IsBreakObjPossible()
{
    return false;
}

rtl::Reference<SdrAttrObj> E3dObject::GetBreakObj()
{
    return nullptr;
}

SdrInventor E3dObject::GetObjInventor() const
{
    return SdrInventor::E3d;
}

SdrObjKind E3dObject::GetObjIdentifier() const
{
    return SdrObjKind::E3D_Object;
}

// Determine the capabilities of the object
void E3dObject::TakeObjInfo(SdrObjTransformInfoRec& rInfo) const
{
    rInfo.bResizeFreeAllowed    = true;
    rInfo.bResizePropAllowed    = true;
    rInfo.bRotateFreeAllowed    = true;
    rInfo.bRotate90Allowed      = true;
    rInfo.bMirrorFreeAllowed    = false;
    rInfo.bMirror45Allowed      = false;
    rInfo.bMirror90Allowed      = false;
    rInfo.bShearAllowed         = false;
    rInfo.bEdgeRadiusAllowed    = false;
    rInfo.bCanConvToPath        = false;

    // no transparence for 3d objects
    rInfo.bTransparenceAllowed = false;

    // Convert 3D objects in a group of polygons:
    // At first not only possible, because the creation of a group of
    // 2D polygons would be required which need to be sorted by depth,
    // ie at intersections be cut relative to each other. Also the texture
    // coordinates were an unsolved problem.
    rInfo.bCanConvToPoly = false;
    rInfo.bCanConvToContour = false;
    rInfo.bCanConvToPathLineToArea = false;
    rInfo.bCanConvToPolyLineToArea = false;
}

// resize object, used from old 2d interfaces, e.g. in Move/Scale dialog (F4)
void E3dObject::NbcResize(const Point& rRef, const Fraction& xFact, const Fraction& yFact)
{
    // Movement in X, Y in the eye coordinate system
    E3dScene* pScene(getRootE3dSceneFromE3dObject());

    if(nullptr == pScene)
    {
        return;
    }

    // transform pos from 2D world to 3D eye
    const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pScene->GetViewContact());
    const drawinglayer::geometry::ViewInformation3D& aViewInfo3D(rVCScene.getViewInformation3D());
    basegfx::B2DPoint aScaleCenter2D(static_cast<double>(rRef.X()), static_cast<double>(rRef.Y()));
    basegfx::B2DHomMatrix aInverseSceneTransform(rVCScene.getObjectTransformation());

    aInverseSceneTransform.invert();
    aScaleCenter2D = aInverseSceneTransform * aScaleCenter2D;

    basegfx::B3DPoint aScaleCenter3D(aScaleCenter2D.getX(), aScaleCenter2D.getY(), 0.5);
    basegfx::B3DHomMatrix aInverseViewToEye(aViewInfo3D.getDeviceToView() * aViewInfo3D.getProjection());

    aInverseViewToEye.invert();
    aScaleCenter3D = aInverseViewToEye * aScaleCenter3D;

    // Get scale factors
    double fScaleX(xFact);
    double fScaleY(yFact);

    // build transform
    basegfx::B3DHomMatrix aInverseOrientation(aViewInfo3D.getOrientation());
    aInverseOrientation.invert();
    basegfx::B3DHomMatrix aFullTransform(GetFullTransform());
    basegfx::B3DHomMatrix aTrans(aFullTransform);

    aTrans *= aViewInfo3D.getOrientation();
    aTrans.translate(-aScaleCenter3D.getX(), -aScaleCenter3D.getY(), -aScaleCenter3D.getZ());
    aTrans.scale(fScaleX, fScaleY, 1.0);
    aTrans.translate(aScaleCenter3D.getX(), aScaleCenter3D.getY(), aScaleCenter3D.getZ());
    aTrans *= aInverseOrientation;
    aFullTransform.invert();
    aTrans *= aFullTransform;

    // Apply
    basegfx::B3DHomMatrix aObjTrans(GetTransform());
    aObjTrans *= aTrans;

    E3DModifySceneSnapRectUpdater aUpdater(this);
    SetTransform(aObjTrans);
}

// Move object in 2D is needed when using cursor keys
void E3dObject::NbcMove(const Size& rSize)
{
    // Movement in X, Y in the eye coordinate system
    E3dScene* pScene(getRootE3dSceneFromE3dObject());

    if(nullptr == pScene)
    {
        return;
    }

    //Dimensions of the scene in 3D and 2D for comparison
    tools::Rectangle aRect = pScene->GetSnapRect();
    basegfx::B3DHomMatrix aInvDispTransform;
    E3dScene* pParent(getParentE3dSceneFromE3dObject());

    if(nullptr != pParent)
    {
        aInvDispTransform = pParent->GetFullTransform();
        aInvDispTransform.invert();
    }

    // BoundVolume from 3d world to 3d eye
    const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pScene->GetViewContact());
    const drawinglayer::geometry::ViewInformation3D& aViewInfo3D(rVCScene.getViewInformation3D());
    basegfx::B3DRange aEyeVol(pScene->GetBoundVolume());
    aEyeVol.transform(aViewInfo3D.getOrientation());

    if ((aRect.GetWidth() == 0) || (aRect.GetHeight() == 0))
        throw o3tl::divide_by_zero();

    // build relative movement vector in eye coordinates
    basegfx::B3DPoint aMove(
        static_cast<double>(rSize.Width()) * aEyeVol.getWidth() / static_cast<double>(aRect.GetWidth()),
        static_cast<double>(-rSize.Height()) * aEyeVol.getHeight() / static_cast<double>(aRect.GetHeight()),
        0.0);
    basegfx::B3DPoint aPos(0.0, 0.0, 0.0);

    // movement vector to local coordinates of objects' parent
    basegfx::B3DHomMatrix aInverseOrientation(aViewInfo3D.getOrientation());
    aInverseOrientation.invert();
    basegfx::B3DHomMatrix aCompleteTrans(aInvDispTransform * aInverseOrientation);

    aMove = aCompleteTrans * aMove;
    aPos = aCompleteTrans * aPos;

    // build transformation and apply
    basegfx::B3DHomMatrix aTranslate;
    aTranslate.translate(aMove.getX() - aPos.getX(), aMove.getY() - aPos.getY(), aMove.getZ() - aPos.getZ());

    E3DModifySceneSnapRectUpdater aUpdater(pScene);
    SetTransform(aTranslate * GetTransform());
}

void E3dObject::RecalcSnapRect()
{
    maSnapRect = tools::Rectangle();
}

// Inform parent of changes in the structure (eg by transformation), in this
// process the object in which the change has occurred is returned.
void E3dObject::StructureChanged()
{
    E3dScene* pParent(getParentE3dSceneFromE3dObject());

    if(nullptr != pParent)
    {
        pParent->InvalidateBoundVolume();
        pParent->StructureChanged();
    }
}

E3dScene* E3dObject::getParentE3dSceneFromE3dObject() const
{
    return DynCastE3dScene(getParentSdrObjectFromSdrObject());
}

// Determine the top-level scene object
E3dScene* E3dObject::getRootE3dSceneFromE3dObject() const
{
    E3dScene* pParent(getParentE3dSceneFromE3dObject());

    if(nullptr != pParent)
    {
        return pParent->getRootE3dSceneFromE3dObject();
    }

    return nullptr;
}

// Calculate enclosed volume, including all child objects
basegfx::B3DRange E3dObject::RecalcBoundVolume() const
{
    basegfx::B3DRange aRetval;
    if (comphelper::IsFuzzing()) // skip slow path for fuzzing
        return aRetval;

    const sdr::contact::ViewContactOfE3d* pVCOfE3D = dynamic_cast< const sdr::contact::ViewContactOfE3d* >(&GetViewContact());

    if(pVCOfE3D)
    {
        // BoundVolume is without 3D object transformation, use correct sequence
        const drawinglayer::primitive3d::Primitive3DContainer& xLocalSequence(pVCOfE3D->getVIP3DSWithoutObjectTransform());

        if(!xLocalSequence.empty())
        {
            const uno::Sequence< beans::PropertyValue > aEmptyParameters;
            const drawinglayer::geometry::ViewInformation3D aLocalViewInformation3D(aEmptyParameters);

            aRetval = xLocalSequence.getB3DRange(aLocalViewInformation3D);
        }
    }

    return aRetval;
}

// Get enclosed volume and possibly recalculate it
const basegfx::B3DRange& E3dObject::GetBoundVolume() const
{
    if(maLocalBoundVol.isEmpty())
    {
        const_cast< E3dObject* >(this)->maLocalBoundVol = RecalcBoundVolume();
    }

    return maLocalBoundVol;
}

void E3dObject::InvalidateBoundVolume()
{
    maLocalBoundVol.reset();
}

// Pass on the changes in transformation to all child objects
void E3dObject::SetTransformChanged()
{
    InvalidateBoundVolume();
    mbTfHasChanged = true;
}

// Define the hierarchical transformation over all Parents, store in
// maFullTransform and return them
const basegfx::B3DHomMatrix& E3dObject::GetFullTransform() const
{
    if(mbTfHasChanged)
    {
        basegfx::B3DHomMatrix aNewFullTransformation(maTransformation);
        E3dScene* pParent(getParentE3dSceneFromE3dObject());

        if(nullptr != pParent)
        {
            aNewFullTransformation = pParent->GetFullTransform() * aNewFullTransformation;
        }

        const_cast< E3dObject* >(this)->maFullTransform = std::move(aNewFullTransformation);
        const_cast< E3dObject* >(this)->mbTfHasChanged = false;
    }

    return maFullTransform;
}

void E3dObject::NbcSetTransform(const basegfx::B3DHomMatrix& rMatrix)
{
    if(maTransformation != rMatrix)
    {
        maTransformation = rMatrix;
        SetTransformChanged();
        StructureChanged();
    }
}

// Set transformation matrix with repaint broadcast
void E3dObject::SetTransform(const basegfx::B3DHomMatrix& rMatrix)
{
    if(rMatrix != maTransformation)
    {
        NbcSetTransform(rMatrix);
        SetChanged();
        BroadcastObjectChange();
        if (m_pUserCall != nullptr) m_pUserCall->Changed(*this, SdrUserCallType::Resize, tools::Rectangle());
    }
}

basegfx::B3DPolyPolygon E3dObject::CreateWireframe() const
{
    const basegfx::B3DRange aBoundVolume(GetBoundVolume());
    return basegfx::utils::createCubePolyPolygonFromB3DRange(aBoundVolume);
}

// Get the name of the object (singular)
OUString E3dObject::TakeObjNameSingul() const
{
    OUString sName = SvxResId(STR_ObjNameSingulObj3d);

    OUString aName(GetName());
    if (!aName.isEmpty())
    {
        sName += " '" + aName + "'";
    }
    return sName;
}

// Get the name of the object (plural)
OUString E3dObject::TakeObjNamePlural() const
{
    return SvxResId(STR_ObjNamePluralObj3d);
}

rtl::Reference<SdrObject> E3dObject::CloneSdrObject(SdrModel& rTargetModel) const
{
    return new E3dObject(rTargetModel, *this);
}

std::unique_ptr<SdrObjGeoData> E3dObject::NewGeoData() const
{
    return std::make_unique<E3DObjGeoData>();
}

void E3dObject::SaveGeoData(SdrObjGeoData& rGeo) const
{
    SdrAttrObj::SaveGeoData (rGeo);

    static_cast<E3DObjGeoData &>(rGeo).maLocalBoundVol  = maLocalBoundVol;
    static_cast<E3DObjGeoData &>(rGeo).maTransformation = maTransformation;
}

void E3dObject::RestoreGeoData(const SdrObjGeoData& rGeo)
{
    maLocalBoundVol = static_cast<const E3DObjGeoData &>(rGeo).maLocalBoundVol;
    E3DModifySceneSnapRectUpdater aUpdater(this);
    NbcSetTransform(static_cast<const E3DObjGeoData &>(rGeo).maTransformation);
    SdrAttrObj::RestoreGeoData (rGeo);
}

// 2D-rotation of a 3D-body, normally this is done by the scene itself.
// This is however a correct implementation, because everything that has
// happened is a rotation around the axis perpendicular to the screen and that
// is regardless of how the scene has been rotated up until now.
void E3dObject::NbcRotate(const Point& rRef, Degree100 nAngle, double sn, double cs)
{
    // So currently the gluepoints are defined relative to the scene aOutRect.
    // Before turning the gluepoints are defined relative to the page. They
    // take no part in the rotation of the scene. To ensure this, there is the
    // SetGlueReallyAbsolute(sal_True);
    double fAngleInRad = toRadians(nAngle);

    basegfx::B3DHomMatrix aRotateZ;
    aRotateZ.rotate(0.0, 0.0, fAngleInRad);
    NbcSetTransform(aRotateZ * GetTransform());

    SetBoundAndSnapRectsDirty();    // This forces a recalculation of all BoundRects
    NbcRotateGluePoints(rRef,nAngle,sn,cs);  // Rotate the gluepoints (who still
                                            // have coordinates relative to the
                                            // original page)
    SetGlueReallyAbsolute(false);       // from now they are again relative to BoundRect (that is defined as aOutRect)
}

std::unique_ptr<sdr::properties::BaseProperties> E3dCompoundObject::CreateObjectSpecificProperties()
{
    return std::make_unique<sdr::properties::E3dCompoundProperties>(*this);
}

E3dCompoundObject::E3dCompoundObject(SdrModel& rSdrModel)
:   E3dObject(rSdrModel)
{
}

E3dCompoundObject::E3dCompoundObject(SdrModel& rSdrModel, E3dCompoundObject const & rSource)
:   E3dObject(rSdrModel, rSource)
{
}

E3dCompoundObject::~E3dCompoundObject ()
{
}

basegfx::B2DPolyPolygon E3dCompoundObject::TakeXorPoly() const
{
    basegfx::B2DPolyPolygon aRetval;
    const uno::Sequence< beans::PropertyValue > aEmptyParameters;
    drawinglayer::geometry::ViewInformation3D aViewInfo3D(aEmptyParameters);
    E3dScene* pRootScene = fillViewInformation3DForCompoundObject(aViewInfo3D, *this);

    if(pRootScene)
    {
        const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pRootScene->GetViewContact());
        const basegfx::B3DPolyPolygon aCubePolyPolygon(CreateWireframe());
        aRetval = basegfx::utils::createB2DPolyPolygonFromB3DPolyPolygon(aCubePolyPolygon,
            aViewInfo3D.getObjectToView() * GetTransform());
        aRetval.transform(rVCScene.getObjectTransformation());
    }

    return aRetval;
}

sal_uInt32 E3dCompoundObject::GetHdlCount() const
{
    // 8 corners + 1 E3dVolumeMarker (= Wireframe representation)
    return 9;
}

void E3dCompoundObject::AddToHdlList(SdrHdlList& rHdlList) const
{
    const uno::Sequence< beans::PropertyValue > aEmptyParameters;
    drawinglayer::geometry::ViewInformation3D aViewInfo3D(aEmptyParameters);
    E3dScene* pRootScene = fillViewInformation3DForCompoundObject(aViewInfo3D, *this);

    if(pRootScene)
    {
        const basegfx::B3DRange aBoundVolume(GetBoundVolume());

        if(!aBoundVolume.isEmpty())
        {
            const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pRootScene->GetViewContact());

            for(sal_uInt32 a(0); a < 8; a++)
            {
                basegfx::B3DPoint aPos3D;

                switch(a)
                {
                    case 0 : aPos3D.setX(aBoundVolume.getMinX()); aPos3D.setY(aBoundVolume.getMinY()); aPos3D.setZ(aBoundVolume.getMinZ()); break;
                    case 1 : aPos3D.setX(aBoundVolume.getMinX()); aPos3D.setY(aBoundVolume.getMinY()); aPos3D.setZ(aBoundVolume.getMaxZ()); break;
                    case 2 : aPos3D.setX(aBoundVolume.getMinX()); aPos3D.setY(aBoundVolume.getMaxY()); aPos3D.setZ(aBoundVolume.getMinZ()); break;
                    case 3 : aPos3D.setX(aBoundVolume.getMinX()); aPos3D.setY(aBoundVolume.getMaxY()); aPos3D.setZ(aBoundVolume.getMaxZ()); break;
                    case 4 : aPos3D.setX(aBoundVolume.getMaxX()); aPos3D.setY(aBoundVolume.getMinY()); aPos3D.setZ(aBoundVolume.getMinZ()); break;
                    case 5 : aPos3D.setX(aBoundVolume.getMaxX()); aPos3D.setY(aBoundVolume.getMinY()); aPos3D.setZ(aBoundVolume.getMaxZ()); break;
                    case 6 : aPos3D.setX(aBoundVolume.getMaxX()); aPos3D.setY(aBoundVolume.getMaxY()); aPos3D.setZ(aBoundVolume.getMinZ()); break;
                    case 7 : aPos3D.setX(aBoundVolume.getMaxX()); aPos3D.setY(aBoundVolume.getMaxY()); aPos3D.setZ(aBoundVolume.getMaxZ()); break;
                }

                // to 3d view coor
                aPos3D *= aViewInfo3D.getObjectToView() * GetTransform();

                // create 2d relative scene
                basegfx::B2DPoint aPos2D(aPos3D.getX(), aPos3D.getY());

                // to 2d world coor
                aPos2D *= rVCScene.getObjectTransformation();

                rHdlList.AddHdl(std::make_unique<SdrHdl>(Point(basegfx::fround<tools::Long>(aPos2D.getX()), basegfx::fround<tools::Long>(aPos2D.getY())), SdrHdlKind::BezierWeight));
            }
        }
    }

    const basegfx::B2DPolyPolygon aPolyPolygon(TakeXorPoly());

    if(aPolyPolygon.count())
    {
        rHdlList.AddHdl(std::make_unique<E3dVolumeMarker>(aPolyPolygon));
    }
}

SdrObjKind E3dCompoundObject::GetObjIdentifier() const
{
    return SdrObjKind::E3D_CompoundObject;
}

void E3dCompoundObject::RecalcSnapRect()
{
    if (comphelper::IsFuzzing()) // skip slow path for fuzzing
        return;

    const uno::Sequence< beans::PropertyValue > aEmptyParameters;
    drawinglayer::geometry::ViewInformation3D aViewInfo3D(aEmptyParameters);
    E3dScene* pRootScene = fillViewInformation3DForCompoundObject(aViewInfo3D, *this);
    maSnapRect = tools::Rectangle();

    if(!pRootScene)
        return;

    // get VC of 3D candidate
    const sdr::contact::ViewContactOfE3d* pVCOfE3D = dynamic_cast< const sdr::contact::ViewContactOfE3d* >(&GetViewContact());

    if(!pVCOfE3D)
        return;

    // get 3D primitive sequence
    const drawinglayer::primitive3d::Primitive3DContainer xLocalSequence(pVCOfE3D->getViewIndependentPrimitive3DContainer());

    if(xLocalSequence.empty())
        return;

    // get BoundVolume
    basegfx::B3DRange aBoundVolume(xLocalSequence.getB3DRange(aViewInfo3D));

    // transform bound volume to relative scene coordinates
    aBoundVolume.transform(aViewInfo3D.getObjectToView());

    // build 2d relative scene range
    basegfx::B2DRange aSnapRange(
        aBoundVolume.getMinX(), aBoundVolume.getMinY(),
        aBoundVolume.getMaxX(), aBoundVolume.getMaxY());

    // transform to 2D world coordinates
    const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pRootScene->GetViewContact());
    aSnapRange.transform(rVCScene.getObjectTransformation());

    // snap to integer
    maSnapRect = tools::Rectangle(
        sal_Int32(floor(aSnapRange.getMinX())), sal_Int32(floor(aSnapRange.getMinY())),
        sal_Int32(ceil(aSnapRange.getMaxX())), sal_Int32(ceil(aSnapRange.getMaxY())));
}

rtl::Reference<SdrObject> E3dCompoundObject::CloneSdrObject(SdrModel& rTargetModel) const
{
    return new E3dCompoundObject(rTargetModel, *this);
}

// convert given basegfx::B3DPolyPolygon to screen coor
basegfx::B2DPolyPolygon E3dCompoundObject::TransformToScreenCoor(const basegfx::B3DPolyPolygon& rCandidate) const
{
    const uno::Sequence< beans::PropertyValue > aEmptyParameters;
    drawinglayer::geometry::ViewInformation3D aViewInfo3D(aEmptyParameters);
    E3dScene* pRootScene = fillViewInformation3DForCompoundObject(aViewInfo3D, *this);
    basegfx::B2DPolyPolygon aRetval;

    if(pRootScene)
    {
        aRetval = basegfx::utils::createB2DPolyPolygonFromB3DPolyPolygon(rCandidate,
            aViewInfo3D.getObjectToView() * GetTransform());
        const sdr::contact::ViewContactOfE3dScene& rVCScene = static_cast< sdr::contact::ViewContactOfE3dScene& >(pRootScene->GetViewContact());
        aRetval.transform(rVCScene.getObjectTransformation());
    }

    return aRetval;
}

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