/*---------------------------------------------------------------------------*\
  =========                 |
  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
   \\    /   O peration     |
    \\  /    A nd           | www.openfoam.com
     \\/     M anipulation  |
-------------------------------------------------------------------------------
    Copyright (C) 2012-2016 OpenFOAM Foundation
    Copyright (C) 2020-2021 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
    This file is part of OpenFOAM.

    OpenFOAM is free software: you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    for more details.

    You should have received a copy of the GNU General Public License
    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.

Application
    foamyHexMeshSurfaceSimplify

Description
    Simplifies surfaces by resampling.

    Uses Thomas Lewiner's topology preserving MarchingCubes.

\*---------------------------------------------------------------------------*/

#include "argList.H"
#include "Time.H"
#include "searchableSurfaces.H"
#include "conformationSurfaces.H"
#include "triSurfaceMesh.H"
#include "labelVector.H"

#include "MarchingCubes.h"


using namespace Foam;

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

// Main program:

int main(int argc, char *argv[])
{
    argList::addNote
    (
        "Re-sample surfaces used in foamyHexMesh operation"
    );
    argList::addArgument("(nx ny nz)", "The resampling interval");
    argList::addArgument("output", "The output triSurface/ file");

    argList::noFunctionObjects();  // Never use function objects

    #include "setRootCase.H"
    #include "createTime.H"

    const auto n = args.get<labelVector>(1);
    const auto exportName = args.get<fileName>(2);

    Info<< "Reading surfaces as specified in the foamyHexMeshDict and"
        << " writing re-sampled " << n << " to " << exportName
        << nl << endl;

    cpuTime timer;

    IOdictionary foamyHexMeshDict
    (
        IOobject
        (
            "foamyHexMeshDict",
            runTime.system(),
            runTime,
            IOobject::MUST_READ_IF_MODIFIED,
            IOobject::NO_WRITE
        )
    );

    // Define/load all geometry
    searchableSurfaces allGeometry
    (
        IOobject
        (
            "cvSearchableSurfaces",
            runTime.constant(),
            "triSurface",
            runTime,
            IOobject::MUST_READ,
            IOobject::NO_WRITE
        ),
        foamyHexMeshDict.subDict("geometry"),
        foamyHexMeshDict.getOrDefault("singleRegionName", true)
    );

    Info<< "Geometry read in = "
        << timer.cpuTimeIncrement() << " s." << nl << endl;


    Random rndGen(64293*Pstream::myProcNo());

    conformationSurfaces geometryToConformTo
    (
        runTime,
        rndGen,
        allGeometry,
        foamyHexMeshDict.subDict("surfaceConformation")
    );

    Info<< "Set up geometry in = "
        << timer.cpuTimeIncrement() << " s." << nl << endl;



    // Extend
    treeBoundBox bb = geometryToConformTo.globalBounds();
    bb.grow(0.1*bb.span());

    Info<< "Meshing to bounding box " << bb << nl << endl;

    const vector span(bb.span());
    const vector d
    (
        span.x()/(n.x()-1),
        span.y()/(n.y()-1),
        span.z()/(n.z()-1)
    );

    MarchingCubes mc(span.x(), span.y(), span.z() ) ;
    mc.set_resolution(n.x(), n.y(), n.z());
    mc.init_all() ;


    // Generate points
    pointField points(mc.size_x()*mc.size_y()*mc.size_z());
    label pointi = 0;

    point pt;
    for( int k = 0 ; k < mc.size_z() ; k++ )
    {
        pt.z() = bb.min().z() + k*d.z();
        for( int j = 0 ; j < mc.size_y() ; j++ )
        {
            pt.y() = bb.min().y() + j*d.y();
            for( int i = 0 ; i < mc.size_x() ; i++ )
            {
                pt.x() = bb.min().x() + i*d.x();
                points[pointi++] = pt;
            }
        }
    }

    Info<< "Generated " << points.size() << " sampling points in = "
        << timer.cpuTimeIncrement() << " s." << nl << endl;


    // Compute data
    const searchableSurfaces& geometry = geometryToConformTo.geometry();
    const labelList& surfaces = geometryToConformTo.surfaces();

    scalarField signedDist;
    labelList nearestSurfaces;
    searchableSurfacesQueries::signedDistance
    (
        geometry,
        surfaces,
        points,
        scalarField(points.size(), sqr(GREAT)),
        searchableSurface::OUTSIDE,     // for non-closed surfaces treat as
                                        // outside
        nearestSurfaces,
        signedDist
    );

    // Fill elements
    pointi = 0;
    for( int k = 0 ; k < mc.size_z() ; k++ )
    {
        for( int j = 0 ; j < mc.size_y() ; j++ )
        {
            for( int i = 0 ; i < mc.size_x() ; i++ )
            {
                mc.set_data(float(signedDist[pointi++]), i, j, k);
            }
        }
    }

    Info<< "Determined signed distance in = "
        << timer.cpuTimeIncrement() << " s." << nl << endl;


    mc.run() ;

    Info<< "Constructed iso surface in = "
        << timer.cpuTimeIncrement() << " s." << nl << endl;


    mc.clean_temps() ;



    // Write output file
    if (mc.ntrigs() > 0)
    {
        Triangle* triangles = mc.triangles();
        List<labelledTri> tris(mc.ntrigs());
        forAll(tris, triI)
        {
            tris[triI] = labelledTri
            (
                triangles[triI].v1,
                triangles[triI].v2,
                triangles[triI].v3,
                0                       // region
            );
        }


        Vertex* vertices = mc.vertices();
        pointField points(mc.nverts());
        forAll(points, pointi)
        {
            Vertex& v = vertices[pointi];
            points[pointi] = point
            (
                bb.min().x() + v.x*span.x()/mc.size_x(),
                bb.min().y() + v.y*span.y()/mc.size_y(),
                bb.min().z() + v.z*span.z()/mc.size_z()
            );
        }


        // Find correspondence to original surfaces
        labelList regionOffsets(surfaces.size());
        label nRegions = 0;
        forAll(surfaces, i)
        {
            const wordList& regions = geometry[surfaces[i]].regions();
            regionOffsets[i] = nRegions;
            nRegions += regions.size();
        }


        geometricSurfacePatchList patches(nRegions);
        nRegions = 0;
        forAll(surfaces, i)
        {
            const wordList& regions = geometry[surfaces[i]].regions();

            forAll(regions, regionI)
            {
                patches[nRegions] = geometricSurfacePatch
                (
                    geometry[surfaces[i]].name() + "_" + regions[regionI],
                    nRegions,
                    "patch"
                );
                nRegions++;
            }
        }

        triSurface s(tris, patches, points, true);

        Info<< "Extracted triSurface in = "
            << timer.cpuTimeIncrement() << " s." << nl << endl;


        // Find out region on local surface of nearest point
        {
            List<pointIndexHit> hitInfo;
            labelList hitSurfaces;
            geometryToConformTo.findSurfaceNearest
            (
                s.faceCentres(),
                scalarField(s.size(), sqr(GREAT)),
                hitInfo,
                hitSurfaces
            );

            // Get region
            DynamicList<pointIndexHit> surfInfo(hitSurfaces.size());
            DynamicList<label> surfIndices(hitSurfaces.size());

            forAll(surfaces, surfI)
            {
                // Extract info on this surface
                surfInfo.clear();
                surfIndices.clear();
                forAll(hitSurfaces, triI)
                {
                    if (hitSurfaces[triI] == surfI)
                    {
                        surfInfo.append(hitInfo[triI]);
                        surfIndices.append(triI);
                    }
                }

                // Calculate sideness of these surface points
                labelList region;
                geometry[surfaces[surfI]].getRegion(surfInfo, region);

                forAll(region, i)
                {
                    label triI = surfIndices[i];
                    s[triI].region() = regionOffsets[surfI]+region[i];
                }
            }
        }

        Info<< "Re-patched surface in = "
            << timer.cpuTimeIncrement() << " s." << nl << endl;

        triSurfaceMesh smesh
        (
            IOobject
            (
                exportName,
                runTime.constant(), // instance
                "triSurface",
                runTime,            // registry
                IOobject::NO_READ,
                IOobject::NO_WRITE,
                IOobject::NO_REGISTER
            ),
            s
        );

        Info<< "writing surfMesh:\n  "
            << smesh.searchableSurface::objectPath() << nl << endl;
        smesh.searchableSurface::write();

        Info<< "Written surface in = "
            << timer.cpuTimeIncrement() << " s." << nl << endl;
    }

    mc.clean_all() ;


    Info<< "End\n" << endl;

    return 0;
}


// ************************************************************************* //
