/*
 * (C) Copyright 2013 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "atlas/grid/detail/partitioner/MatchingMeshPartitionerBruteForce.h"

#include <vector>

#include "eckit/log/ProgressTimer.h"

#include "atlas/array/ArrayView.h"
#include "atlas/field/Field.h"
#include "atlas/grid/Grid.h"
#include "atlas/mesh/Elements.h"
#include "atlas/mesh/Mesh.h"
#include "atlas/mesh/Nodes.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/runtime/Trace.h"
#include "atlas/util/CoordinateEnums.h"

namespace atlas {
namespace grid {
namespace detail {
namespace partitioner {

namespace {

PartitionerBuilder<MatchingMeshPartitionerBruteForce> __builder( "brute-force" );

double dot_sign( const double& Ax, const double& Ay, const double& Bx, const double& By, const double& Cx,
                 const double& Cy ) {
    return ( Ax - Cx ) * ( By - Cy ) - ( Bx - Cx ) * ( Ay - Cy );
}

/// Point-in-triangle test
/// @note "dot product" test, equivalent to half-plane check
/// @see
/// http://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle
bool point_in_triangle( const PointLonLat& P, const PointLonLat& A, const PointLonLat& B, const PointLonLat& C ) {
    // Compute signs of dot products
    const bool b1 = dot_sign( P.lon(), P.lat(), A.lon(), A.lat(), B.lon(), B.lat() ) < 0,
               b2 = dot_sign( P.lon(), P.lat(), B.lon(), B.lat(), C.lon(), C.lat() ) < 0,
               b3 = dot_sign( P.lon(), P.lat(), C.lon(), C.lat(), A.lon(), A.lat() ) < 0;

    // Check if point is in triangle
    // - check for b1 && b2 && b3 only works for triangles ordered
    // counter-clockwise
    // - check for b1==b2 && b2==b3 works for both, equivalent to "all points on
    // the same side"
    return ( b1 == b2 ) && ( b2 == b3 );
}

/// Point-in-quadrilateral test
/// @note limited to convex quadrilaterals, @see
/// https://en.wikipedia.org/wiki/Convex_polygon
bool point_in_quadrilateral( const PointLonLat& P, const PointLonLat& A, const PointLonLat& B, const PointLonLat& C,
                             const PointLonLat& D ) {
    const bool b1 = dot_sign( P.lon(), P.lat(), A.lon(), A.lat(), B.lon(), B.lat() ) < 0,
               b2 = dot_sign( P.lon(), P.lat(), B.lon(), B.lat(), C.lon(), C.lat() ) < 0,
               b3 = dot_sign( P.lon(), P.lat(), C.lon(), C.lat(), D.lon(), D.lat() ) < 0,
               b4 = dot_sign( P.lon(), P.lat(), D.lon(), D.lat(), A.lon(), A.lat() ) < 0;
    return ( b1 == b2 ) && ( b2 == b3 ) && ( b3 == b4 );
}

}  // namespace

void MatchingMeshPartitionerBruteForce::partition( const Grid& grid, int partitioning[] ) const {
    ATLAS_TRACE( "MatchingMeshPartitionerBruteForce::partition" );


    eckit::mpi::Comm& comm = eckit::mpi::comm();
    const int mpi_rank     = int( comm.rank() );

    // Point coordinates
    // - use a bounding box to quickly discard points,
    // - except when that is above/below bounding box but poles should be included
    //   FIXME: THIS IS A HACK! the coordinates include North/South Pole
    //   (first/last partitions only)

    ATLAS_ASSERT( grid.domain().global() );
    bool includesNorthPole = ( mpi_rank == 0 );
    bool includesSouthPole = ( mpi_rank == ( int( comm.size() ) - 1 ) );

    ATLAS_ASSERT( prePartitionedMesh_.nodes().size() );
    auto lonlat_src = array::make_view<double, 2>( prePartitionedMesh_.nodes().lonlat() );

    std::vector<PointLonLat> coordinates;
    PointLonLat coordinatesMin = PointLonLat( lonlat_src( 0, LON ), lonlat_src( 0, LAT ) );
    PointLonLat coordinatesMax = coordinatesMin;

    for ( idx_t i = 0; i < lonlat_src.size(); ++i ) {
        PointLonLat A( lonlat_src( i, LON ), lonlat_src( i, LAT ) );
        coordinatesMin = PointLonLat::componentsMin( coordinatesMin, A );
        coordinatesMax = PointLonLat::componentsMax( coordinatesMax, A );
        coordinates.push_back( A );
    }

    {
        eckit::ProgressTimer timer( "Partitioning target", grid.size(), "point", double( 10 ), atlas::Log::trace() );
        for ( idx_t i = 0; i < grid.size(); ++i, ++timer ) {
            partitioning[i] = -1;
            const PointLonLat& P( coordinates[i] );

            if ( coordinatesMin[LON] <= P[LON] && P[LON] <= coordinatesMax[LON] && coordinatesMin[LAT] <= P[LAT] &&
                 P[LAT] <= coordinatesMax[LAT] ) {
                const mesh::Cells& elements_src = prePartitionedMesh_.cells();
                const idx_t nb_types            = elements_src.nb_types();
                bool found                      = false;

                for ( idx_t t = 0; t < nb_types && !found; ++t ) {
                    idx_t idx[4];
                    const mesh::Elements& elements      = elements_src.elements( t );
                    const mesh::BlockConnectivity& conn = elements.node_connectivity();

                    const idx_t nb_nodes = elements.nb_nodes();
                    ATLAS_ASSERT( ( nb_nodes == 3 && elements.name() == "Triangle" ) ||
                                  ( nb_nodes == 4 && elements.name() == "Quadrilateral" ) );

                    for ( idx_t j = 0; j < elements.size() && !found; ++j ) {
                        idx[0] = conn( j, 0 );
                        idx[1] = conn( j, 1 );
                        idx[2] = conn( j, 2 );
                        idx[3] = nb_nodes > 3 ? conn( j, 3 ) : 0;

                        if ( ( elements.name() == "Triangle" &&
                               point_in_triangle( P, coordinates[idx[0]], coordinates[idx[1]],
                                                  coordinates[idx[2]] ) ) ||
                             ( elements.name() == "Quadrilateral" &&
                               point_in_quadrilateral( P, coordinates[idx[0]], coordinates[idx[1]], coordinates[idx[2]],
                                                       coordinates[idx[3]] ) ) ) {
                            partitioning[i] = mpi_rank;
                            found           = true;
                        }
                    }
                }
            }
            else if ( ( includesNorthPole && P[LAT] > coordinatesMax[LAT] ) ||
                      ( includesSouthPole && P[LAT] < coordinatesMin[LAT] ) ) {
                partitioning[i] = mpi_rank;
            }
        }
    }

    // Synchronize the partitioning and return a grid partitioner
    comm.allReduceInPlace( partitioning, grid.size(), eckit::mpi::Operation::MAX );
    const int min = *std::min_element( partitioning, partitioning + grid.size() );
    if ( min < 0 ) {
        throw_Exception(
            "Could not find partition for target node (source "
            "mesh does not contain all target grid points)",
            Here() );
    }
}

}  // namespace partitioner
}  // namespace detail
}  // namespace grid
}  // namespace atlas
