/*
 * (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/mesh/detail/AccumulateFacets.h"
#include "atlas/mesh/Elements.h"
#include "atlas/mesh/HybridElements.h"
#include "atlas/mesh/Nodes.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Trace.h"

namespace atlas {
namespace mesh {
namespace detail {

void accumulate_facets( const mesh::HybridElements& cells, const mesh::Nodes& nodes,
                        std::vector<idx_t>& facet_nodes_data,  // shape(nb_facets,nb_nodes_per_facet)
                        std::vector<idx_t>& connectivity_facet_to_elem, idx_t& nb_facets, idx_t& nb_inner_facets,
                        idx_t& missing_value ) {
    ATLAS_TRACE();
    missing_value = -1;
    std::vector<std::vector<idx_t>> node_to_facet( nodes.size() );
    for ( auto& facet : node_to_facet ) {
        facet.reserve( 6 );
    }
    nb_facets       = 0;
    nb_inner_facets = 0;
    if ( connectivity_facet_to_elem.size() == 0 ) {
        connectivity_facet_to_elem.reserve( 6 * cells.size() );
    }
    if ( facet_nodes_data.size() == 0 ) {
        facet_nodes_data.reserve( 6 * cells.size() );
    }
    for ( idx_t t = 0; t < cells.nb_types(); ++t ) {
        const mesh::Elements& elements            = cells.elements( t );
        const mesh::BlockConnectivity& elem_nodes = elements.node_connectivity();
        auto elem_flags                           = elements.view<int, 1>( elements.flags() );

        auto patch = [&elem_flags]( idx_t e ) {
            using Topology = atlas::mesh::Nodes::Topology;
            return Topology::check( elem_flags( e ), Topology::PATCH );
        };

        idx_t nb_elems          = elements.size();
        idx_t nb_nodes_in_facet = 2;

        std::vector<std::vector<int>> facet_node_numbering;
        idx_t nb_facets_in_elem;
        if ( elements.name() == "Quadrilateral" ) {
            nb_facets_in_elem = 4;
            facet_node_numbering.resize( nb_facets_in_elem, std::vector<int>( nb_nodes_in_facet ) );
            facet_node_numbering[0][0] = 0;
            facet_node_numbering[0][1] = 1;
            facet_node_numbering[1][0] = 1;
            facet_node_numbering[1][1] = 2;
            facet_node_numbering[2][0] = 2;
            facet_node_numbering[2][1] = 3;
            facet_node_numbering[3][0] = 3;
            facet_node_numbering[3][1] = 0;
        }
        else if ( elements.name() == "Triangle" ) {
            nb_facets_in_elem = 3;
            facet_node_numbering.resize( nb_facets_in_elem, std::vector<int>( nb_nodes_in_facet ) );
            facet_node_numbering[0][0] = 0;
            facet_node_numbering[0][1] = 1;
            facet_node_numbering[1][0] = 1;
            facet_node_numbering[1][1] = 2;
            facet_node_numbering[2][0] = 2;
            facet_node_numbering[2][1] = 0;
        }
        else {
            throw_Exception( elements.name() + " is not \"Quadrilateral\" or \"Triangle\"", Here() );
        }

        std::vector<idx_t> facet_nodes( nb_nodes_in_facet );

        for ( idx_t e = 0; e < nb_elems; ++e ) {
            if ( patch( e ) ) {
                continue;
            }
            for ( idx_t f = 0; f < nb_facets_in_elem; ++f ) {
                bool found_face = false;

                for ( idx_t jnode = 0; jnode < nb_nodes_in_facet; ++jnode ) {
                    facet_nodes[jnode] = elem_nodes( e, facet_node_numbering[f][jnode] );
                }

                idx_t node = facet_nodes[0];
                for ( const idx_t face : node_to_facet[node] ) {
                    idx_t nb_matched_nodes = 0;
                    if ( nb_nodes_in_facet > 1 )  // 2D or 3D
                    {
                        for ( idx_t jnode = 0; jnode < nb_nodes_in_facet; ++jnode ) {
                            idx_t other_node = facet_nodes[jnode];
                            for ( const idx_t other_face : node_to_facet[other_node] ) {
                                if ( other_face == face ) {
                                    ++nb_matched_nodes;
                                    break;
                                }
                            }
                        }
                        if ( nb_matched_nodes == nb_nodes_in_facet ) {
                            connectivity_facet_to_elem[2 * face + 1] = e + elements.begin();
                            ++nb_inner_facets;
                            found_face = true;
                            break;
                        }
                    }
                }

                if ( found_face == false ) {
                    connectivity_facet_to_elem.emplace_back( elements.begin() + e );
                    // if 2nd element stays missing_value, it is a bdry face
                    connectivity_facet_to_elem.emplace_back( missing_value );
                    for ( idx_t n = 0; n < nb_nodes_in_facet; ++n ) {
                        node_to_facet[facet_nodes[n]].emplace_back( nb_facets );
                        facet_nodes_data.emplace_back( facet_nodes[n] );
                    }
                    ++nb_facets;
                }
            }
        }
    }
}

void accumulate_facets_in_range( std::vector<array::Range>& range, const mesh::HybridElements& cells,
                                 const mesh::Nodes& /*nodes*/,
                                 std::vector<idx_t>& facet_nodes_data,  // shape(nb_facets,nb_nodes_per_facet)
                                 std::vector<idx_t>& connectivity_facet_to_elem, idx_t& nb_facets,
                                 idx_t& nb_inner_facets, idx_t& missing_value,
                                 std::vector<std::vector<idx_t>>& node_to_facet ) {
    ATLAS_TRACE();
    if ( connectivity_facet_to_elem.size() == 0 ) {
        connectivity_facet_to_elem.reserve( 6 * cells.size() );
    }
    if ( facet_nodes_data.size() == 0 ) {
        facet_nodes_data.reserve( 6 * cells.size() );
    }
    for ( idx_t t = 0; t < cells.nb_types(); ++t ) {
        const mesh::Elements& elements            = cells.elements( t );
        const mesh::BlockConnectivity& elem_nodes = elements.node_connectivity();
        auto elem_flags                           = elements.view<int, 1>( elements.flags() );

        auto patch = [&elem_flags]( idx_t e ) {
            using Topology = atlas::mesh::Nodes::Topology;
            return Topology::check( elem_flags( e ), Topology::PATCH );
        };

        idx_t nb_nodes_in_facet = 2;

        std::vector<std::vector<int>> facet_node_numbering;
        idx_t nb_facets_in_elem;
        if ( elements.name() == "Quadrilateral" ) {
            nb_facets_in_elem = 4;
            facet_node_numbering.resize( nb_facets_in_elem, std::vector<int>( nb_nodes_in_facet ) );
            facet_node_numbering[0][0] = 0;
            facet_node_numbering[0][1] = 1;
            facet_node_numbering[1][0] = 1;
            facet_node_numbering[1][1] = 2;
            facet_node_numbering[2][0] = 2;
            facet_node_numbering[2][1] = 3;
            facet_node_numbering[3][0] = 3;
            facet_node_numbering[3][1] = 0;
        }
        else if ( elements.name() == "Triangle" ) {
            nb_facets_in_elem = 3;
            facet_node_numbering.resize( nb_facets_in_elem, std::vector<int>( nb_nodes_in_facet ) );
            facet_node_numbering[0][0] = 0;
            facet_node_numbering[0][1] = 1;
            facet_node_numbering[1][0] = 1;
            facet_node_numbering[1][1] = 2;
            facet_node_numbering[2][0] = 2;
            facet_node_numbering[2][1] = 0;
        }
        else {
            throw_Exception( elements.name() + " is not \"Quadrilateral\" or \"Triangle\"", Here() );
        }

        std::vector<idx_t> facet_nodes( nb_nodes_in_facet );

        const idx_t e_start = range[t].start();
        const idx_t e_end   = range[t].end();

        for ( idx_t e = e_start; e < e_end; ++e ) {
            if ( patch( e ) ) {
                continue;
            }

            for ( idx_t f = 0; f < nb_facets_in_elem; ++f ) {
                bool found_face = false;

                for ( idx_t jnode = 0; jnode < nb_nodes_in_facet; ++jnode ) {
                    facet_nodes[jnode] = elem_nodes( e, facet_node_numbering[f][jnode] );
                }

                int node = facet_nodes[0];
                for ( idx_t face : node_to_facet[node] ) {
                    idx_t nb_matched_nodes = 0;
                    if ( nb_nodes_in_facet > 1 )  // 2D or 3D
                    {
                        for ( idx_t jnode = 0; jnode < nb_nodes_in_facet; ++jnode ) {
                            idx_t other_node = facet_nodes[jnode];
                            for ( idx_t other_face : node_to_facet[other_node] ) {
                                if ( other_face == face ) {
                                    ++nb_matched_nodes;
                                    break;
                                }
                            }
                        }
                        if ( nb_matched_nodes == nb_nodes_in_facet ) {
                            connectivity_facet_to_elem[2 * face + 1] = e + elements.begin();
                            ++nb_inner_facets;
                            found_face = true;
                            break;
                        }
                    }
                }

                if ( found_face == false ) {
                    connectivity_facet_to_elem.emplace_back( elements.begin() + e );
                    // if 2nd element stays missing_value, it is a bdry face
                    connectivity_facet_to_elem.emplace_back( missing_value );
                    for ( idx_t n = 0; n < nb_nodes_in_facet; ++n ) {
                        node_to_facet[facet_nodes[n]].emplace_back( nb_facets );
                        facet_nodes_data.emplace_back( facet_nodes[n] );
                    }
                    ++nb_facets;
                }
            }
        }
    }
}

void accumulate_facets_ordered_by_halo( const mesh::HybridElements& cells, const mesh::Nodes& nodes,
                                        std::vector<idx_t>& facet_nodes_data,  // shape(nb_facets,nb_nodes_per_facet)
                                        std::vector<idx_t>& connectivity_facet_to_elem, idx_t& nb_facets,
                                        idx_t& nb_inner_facets, idx_t& missing_value,
                                        std::vector<idx_t>& halo_offsets ) {
    ATLAS_TRACE();

    static int MAXHALO = 50;
    std::vector<std::vector<array::Range>> ranges( MAXHALO, std::vector<array::Range>( cells.nb_types() ) );

    int maxhalo{0};
    for ( idx_t t = 0; t < cells.nb_types(); ++t ) {
        const mesh::Elements& elements = cells.elements( t );
        auto elem_halo                 = elements.view<int, 1>( elements.halo() );
        idx_t nb_elems                 = elements.size();

        int halo{0};
        int begin{0};
        int end{0};
        for ( idx_t e = 0; e < nb_elems; ++e ) {
            ATLAS_ASSERT( elem_halo( e ) >= halo );
            if ( elem_halo( e ) > halo ) {
                end             = e;
                ranges[halo][t] = array::Range{begin, end};
                begin           = end;
                ++halo;
            }
        }
        end             = nb_elems;
        ranges[halo][t] = array::Range{begin, end};
        maxhalo         = std::max( halo, maxhalo );
    }


    missing_value = -1;
    std::vector<std::vector<idx_t>> node_to_facet( nodes.size() );
    for ( auto& facets : node_to_facet ) {
        facets.reserve( 6 );
    }
    nb_facets       = 0;
    nb_inner_facets = 0;


    halo_offsets = std::vector<idx_t>{0};
    for ( int h = 0; h <= maxhalo; ++h ) {
        accumulate_facets_in_range( ranges[h], cells, nodes, facet_nodes_data, connectivity_facet_to_elem, nb_facets,
                                    nb_inner_facets, missing_value, node_to_facet );
        halo_offsets.emplace_back( nb_facets );
    }
}


}  // namespace detail
}  // namespace mesh
}  // namespace atlas
