#!/usr/bin/env perl
#
# Copyright (c) 2021 Microsoft Open Technologies, Inc.
#
#    Licensed 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
#
#    THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
#    CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT
#    LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS
#    FOR A PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
#
#    See the Apache Version 2.0 License for specific language governing
#    permissions and limitations under the License.
#
#    Microsoft would like to thank the following companies for their review and
#    assistance with these files: Intel Corporation, Mellanox Technologies Ltd,
#    Dell Products, L.P., Facebook, Inc., Marvell International Ltd.
#
# @file    gensairpc.pl
#
# @brief   This module generates RPC interface of SAI for PTF
#

use strict;
use warnings;
use diagnostics;

use 5.020;

use File::Spec::Functions qw(catdir catfile rel2abs);
use File::Basename qw(dirname);
use English qw(-no_match_vars);
use Getopt::Long::Descriptive;
use File::Path qw(rmtree);
use Term::ANSIColor;
use Cwd qw(getcwd);
use Data::Dumper;
use Const::Fast;
use File::Copy;
use Template;
use Carp;

use lib catdir( dirname( rel2abs(__FILE__) ), 'rpc' );

use Utils::Format;
use Utils;

use SAI::Function::Argument;
use SAI::Struct::Member;
use SAI::Function;
use SAI::Typedef;
use SAI::Struct;
use SAI::Attrs;
use SAI::Stats;
use SAI::Type;

# Avoid warnings related to given/when
no if $] >= 5.018, warnings => 'experimental::smartmatch';

# Consts
# TODO: move some of them to common package (e.g. PREFIX or RETVAL) so that they can be shared
const my $PREFIX         => 'sai_thrift_';
const my $GEN_DIR        => 'generated';
const my $THRIFT_GEN_DIR => 'gen-cpp';
const my $COUNTER        => -1;
const my $RETVAL         => 0;

# Keep the dbg data sorted, to make it comparable
$Data::Dumper::Sortkeys = 1;

my $run_dir     = getcwd;
my $script_dir  = dirname( rel2abs($PROGRAM_NAME) );
my $script_path = rel2abs(__FILE__);
my $gen_dir     = catdir( $script_dir, $GEN_DIR );
my $templates_dir =
  catdir( $ENV{PAR_TEMP} ? "$ENV{PAR_TEMP}/inc" : $script_dir, 'templates' );

# Parameters
my $sai_dir = catdir( dirname($script_dir) );

#<<<
( my $args, my $usage ) = describe_options(
    "$PROGRAM_NAME %o",
    [ 'dbg',             'Print debug information in generated files, implies --dump',                                            { default => 0 }        ],
    [ 'experimental|e',  'Generate also experimental files',                                                                      { default => 0 }        ],
    [ 'dist',            'Create the standalone \'gensairpc\' script (experimental)',                                             { default => 0 }        ],
    [ 'dump|d',          'Dump all data to the file',                                                                             { default => 0 }        ],
    [ 'clean-meta|c',    'Perform clean meta before generation',                                                                  { default => 0 }        ],
    [ 'verbose|v',       'Print more details',                                                                                    { default => 0 }        ],
    [ 'mandatory-attrs', 'Make mandatory attributes obligatory in sai_adapter.py',                                                { default => 0 }        ],
    [ 'dev-utils:s',     'Generate additional development utils within the generated code. Additional options: [=log,zero]',      { default => 0 }        ],
    [ 'adapter_logger',  'Enable the logger in sai_adapter, it will log all the method invocation.',                              { default => 0}         ],
    [ 'attr-header',     'Generate additional header of attributes definitions (including object types)',                         { default => 0 }        ],
    [ 'help|h',          'Print this help',                                                                                       { shortcircuit => 1 }   ],
);
#>>>

if ( $args->help ) {
    print $usage->text;
    exit;
}

if ( $args->dist ) {
    Utils->dist($script_path);
    exit;
}

my $dbg     = $args->dbg;
my $dump    = $args->dump;
my $verbose = $args->verbose;
$dump    = 1 if $dbg;
$verbose = 1 if $dump;
my $experimental  = $args->experimental;
my $clean         = $args->clean_meta;
my $mandatory_attrs = $args->mandatory_attrs;
my $dev_utils       = ( $args->dev_utils ne q{} ? $args->dev_utils : 1 );
my $adapter_logger   = ( $args->adapter_logger ne q{} ? $args->adapter_logger : 1 );
my $attr_header     = $args->attr_header;

# Configure SAI meta
my $sai_meta_dir = catdir( $sai_dir, 'meta' );
my $sai_parse_path = catfile( $sai_meta_dir, 'parse.pl' );

-d $sai_dir or die "\"$sai_dir\" directory is invalid";

# Declare SAI meta global variables, its libraries need them
our $XMLDIR           = catdir( $sai_meta_dir, 'xml' );
our $INCLUDE_DIR      = catdir( $sai_dir,      'inc' );
our $EXPERIMENTAL_DIR = catdir( $sai_dir,      'experimental' );

# Include SAI meta libs. The intention is that SAI meta path
# can be changed by the parameter, so we cannot use 'use'
our @INC;
unshift @INC, $sai_meta_dir;

require xmlutils;
xmlutils->import;

require utils;
utils->import;

if ($clean) {
    say colored( "Removing \"$GEN_DIR\" directory...", 'bold blue' );
    rmtree $gen_dir;
    say colored( 'Cleaning SAI meta...', 'bold blue' );
    system 'make clean -C ' . $sai_meta_dir;
}

# Generate Doxygen xml files
say colored( 'Building SAI meta XML...', 'bold blue' );
system 'make xml -C ' . $sai_meta_dir;

say colored( 'Parsing...', 'bold blue' );
my $data = get_definitions();
my $vars = {
    apis            => $data->{apis},
    functions       => $data->{functions},
    methods         => $data->{methods},
    structs         => $data->{structs},
    dbg             => $dbg,
    mandatory_attrs => $mandatory_attrs,
    dev_utils       => $dev_utils,
    adapter_logger  => $adapter_logger,
    templates_dir   => $templates_dir
};

mkdir $gen_dir;

if ($dump) {
    say colored( 'Generating sai_dbg.dump...', 'bold blue' );
    open my $dump_file, '>', catfile( $gen_dir, 'sai_dbg.dump' )
      or die 'Could not open the dump file';
    print {$dump_file} Dumper $data;
    close $dump_file or die;
}

my $template = Template->new( ABSOLUTE => 1 );

say colored( 'Generating sai.thrift...', 'bold blue' );
$template->process( catfile( $templates_dir, 'sai.thrift.tt' ),
    $vars, catfile( $run_dir, 'sai.thrift' ) )
  or die $template->error();

say colored( 'Generating Thrift files...', 'bold blue' );
chdir $gen_dir;
# rm gen-cpp is needed since thrift don't override existing files
# even if sai.thrift file changed :(
system("rm -rf gen-cpp");
system('thrift -o . --gen cpp -r ../sai.thrift') == 0 or die colored("Command failed: $!", "red");
chdir $run_dir;

say colored( 'Generating sai_rpc_server.cpp.tt...', 'bold blue' );
open my $skeleton, '<',
  catfile( $gen_dir, $THRIFT_GEN_DIR, 'sai_rpc_server.skeleton.cpp' )
  or die colored( 'Thrift files not generated, probably sai.thrift is invalid',
    'red' );
mkdir catdir( $gen_dir, 'templates' );
open my $server_template, '>',
  catfile( $gen_dir, 'templates', 'sai_rpc_server.cpp.tt' )
  or die;
my $fn_definitions =
  generate_server_template_from_skeleton( $skeleton, $server_template );
close $server_template or die;
close $skeleton        or die;
say "$fn_definitions functions processed";

say colored( 'Generating sai_rpc_server.cpp...', 'bold blue' );
$template->process( catfile( $gen_dir, 'templates', 'sai_rpc_server.cpp.tt' ),
    $vars, catfile( $run_dir, 'sai_rpc_server.cpp' ) )
  or die $template->error();

# say 'Formatting sai_rpc_server.cpp...' if $verbose;
# Utils::Format->cpp( catfile( $gen_dir, 'sai_rpc_server.cpp' ) );

if ($attr_header) {
    say 'Generating gensaiattrs.c...';
    $template->process( catfile( $templates_dir, 'gensaiattrs.c.tt' ),
        $vars, catfile( $gen_dir, 'gensaiattrs.c' ) )
      or die $template->error();

    say 'Builiding gensaiattrs...';
    chdir $gen_dir;
    system "gcc -O2 -Wall -I $INCLUDE_DIR gensaiattrs.c -o gensaiattrs";

    say colored( 'Generating sai_gen_attributes.h...', 'bold blue' );
    system './gensaiattrs > sai_gen_attributes.h';
    chdir $run_dir;
}

say colored( 'Generating sai_adapter.py...', 'bold blue' );
$template->process( catfile( $templates_dir, 'sai_adapter.py.tt' ),
    $vars, catfile( $run_dir, 'sai_adapter.py' ) )
  or die $template->error();

# say 'Formatting sai_adapter.py...' if $verbose;
# Utils::Format->python( catfile( $gen_dir, 'sai_adapter.py' ) );

# Thrift tools can generate the skeleton of RPC server file.
# Replace it with the Template Toolkit template, so that we can
# easily generate the functions content.
sub generate_server_template_from_skeleton {
    my $skeleton        = shift;
    my $server_template = shift;

    my $definitions = 0;

    say {$server_template}
      '/* AUTOGENERATED FILE! DO NOT EDIT */';
    say {$server_template}
      '[% PROCESS "$templates_dir/sai_rpc_server_functions.tt" -%]';
    say {$server_template}
      '[% PROCESS "$templates_dir/sai_rpc_server_helper_functions.tt" -%]';
    while ( my $line = <$skeleton> ) {
        given ($line) {
            when ( /int main/ .. /}/ ) {

                # Ignore the main function
            }
            when (/printf[(]["](?:\w+)\\n["][)]/) {
                ++$definitions;

                # Replace "printf" with function body template
                say {$server_template} '[% PROCESS sai_rpc_function_body -%]';
            }
            when (/\s(\w+)\s$PREFIX(\w+)[(]/) {

                # Get the return type and the function name and
                # set the template variable
                say {$server_template}
"[% function_name = 'sai_$2'; ret_type = '$1'; function = functions.\$function_name -%]";
                $line =~ s/_return/[% function.rpc_return.name %]_out/g;
                print {$server_template} $line;
            }
            when (/class /) {

                # Include additional files
                say {$server_template} '#ifdef __cplusplus';
                say {$server_template} 'extern "C" {';
                say {$server_template} '#endif';
                say {$server_template} '#include <sai.h>';
                say {$server_template} '#include <saiextensions.h>'
                  if $experimental;
                say {$server_template} '#ifdef __cplusplus';
                say {$server_template} '}';
                say {$server_template} '#include <iostream>';
                say {$server_template} '#endif';

                # Define global variable before "class"
                print {$server_template}
"\nextern sai_object_id_t switch_id;\nsai_object_id_t switch_id;\nextern sai_object_id_t gSwitchId;\n\n\n";

                # Define helper functions
                print {$server_template} "[% PROCESS helper_functions %]\n\n\n";

                print {$server_template} $line;
            }
            default {
               # Just print the line (if it not a "Your implementation" comment)
                print {$server_template} $line unless $line =~ /\/\/ Your/;
            }
        }
    }

    return $definitions;
}

# Find the api name within the header related to the currenct XML file.
sub get_api_name
{
    # it may happen that metadata is already generated, and SAI directory is
    # mounted in docker, then the location will be invalid inside docker, so
    # let's use only filename and inc directory

    my $location = shift;

    $location =~ s!.+/!!; # leave only filename

    # The hash should be static - don't open and traverse the header
    # if we already found the API name inside.
    state %api_names = (
            "saimetadatalogger.h" => "common",
            "saimetadatatypes.h" => "common",
            "sai.h" => "common",
            "saiobject.h" => "object", # for sai_object_key_entry_t
            "saitypes.h" => "common" );

    return $api_names{$location} if exists $api_names{$location};

    my $file = "$sai_dir/inc/$location";

    $file = "$sai_dir/experimental/$location" if $location =~ /experimental|extension/;

    open(H, '<', $file) or die "Failed to open: $file: $!";

    my @lines = <H>;

    close H or croak;

    for my $line (@lines)
    {
        if ($line =~ /^typedef struct _sai_(\w+)_api_t/)
        {
            $api_names{$location} = $1;

            return $1;
        }
    }

    if ($location =~ /^sai\w*extensions.h$/)
    {
        $api_names{$location} = "common";

        return "common";
    }

    die "File $file doesn't contain api struct!";
}

# The main function that parses all XML files and creates all
# types definitions.
sub get_definitions {
    my %methods_table;
    my %all_functions;
    my %all_structs;
    my %all_attrs;
    my @all_enums;
    my %apis;
    my $i = 0;

    # Iterate over files
    for ( GetSaiXmlFiles($XMLDIR) ) {
        my $xml = ReadXml($_);

        # Iterate over definitions
        for ( @{ $xml->{compounddef}[0]->{sectiondef} } ) {
            if ( $_->{kind} eq 'typedef' or $_->{kind} eq 'enum' ) {
                for ( @{ $_->{memberdef} } ) {
                    my $function;
                    my $typedef;
                    my $object;
                    my $struct;
                    my $attrs;
                    my $stats;
                    my $api;

                    # Get API name
                    my $location = $_->{location}[0]->{file};
                    unless ($experimental) {
                        next if $location =~ /(experimental|extension)/;
                    }
                    $api = get_api_name($location);
                    $apis{$api} = {} unless ref $apis{$api} eq 'HASH';

                    # Populate attribute list per object
                    next if get_object_types( $api, $apis{$api}, $_ );
                    next if get_attributes( $apis{$api}, $_ );
                    next if get_stats( $apis{$api}, $_ );

                    # Populate type definitions and function declarations

                    next if get_typedef( $apis{$api}, $_, $api eq 'common' );

                    next
                      if get_struct( $apis{$api}, \%all_structs,
                        \%methods_table, $_ );

                    next
                      if $api ne 'common'
                      and
                      get_function( $apis{$api}, \%all_functions, $_, $api );

                    # Add enum name to the list
                    if ( $_->{kind} eq 'enum' ) {
                        my $enum_name = $_->{name}[0];
                        $enum_name =~ s/^_//;
                        push @all_enums, $enum_name;
                    }

                }
            }
        }
    }

    my $api_list = assign_attr_types( \%apis, \@all_enums );

    return {
        apis      => $api_list,
        attrs     => \%all_attrs,
        structs   => \%all_structs,
        functions => \%all_functions,
        methods   => \%methods_table
    };
}

# To set or get attribute, the proper value struct field need to be used.
# Obtain the name of this field and assign it to the attribute.
# If the attribute is of enum type, then it is s32, otherwise the correct type
# should be found within sai_attribute_value_t fields.
sub get_attr_type {
    my $attr       = shift;
    my $attr_types = shift;
    my $all_enums  = shift;

    my $type = $attr->type->thrift_name;

    # First, check if we have enum
    return 's32' if ( $attr->type->name ~~ $all_enums );

    # Try to compare types of attribute and attr value otherwise
    for ( @{ $attr_types->members } ) {
        return $_->thrift_name if ( $type eq $_->type->thrift_name );
    }

    carp colored( "Unknown type $type of attribute " . $attr->name, 'red' );
    return;
}

# To set or get attribute, the proper value struct field need to be used.
# Obtain the name of this field and assign it to all attributes.
sub assign_attr_types {
    my $apis      = shift;
    my $all_enums = shift;

    my $attr_types;
    for ( @{ $apis->{common}->{structs} } ) {
        $attr_types = $_ if $_->name eq 'sai_attribute_value_t';
    }

    croak unless $attr_types;

    for my $api ( values %{$apis} ) {
        for my $object ( values %{ $api->{objects} } ) {
            for my $attr ( $object->{attrs}->all ) {
                $attr->typename(
                    get_attr_type( $attr, $attr_types, $all_enums ) );
            }
        }
    }

    return $apis;
}

# Create and store the object type Enum
sub get_object_types {
    my $api_name = shift;
    my $api      = shift;
    my $enum     = shift;

    return 0
      unless SAI::Enum->validate_xml_typedef($enum)
      and $enum->{name}[0] =~ /object_type_t/;
    my $object_types = SAI::Enum->new( xml_typedef => $enum );

    if ($object_types) {
        say "\"object_types\" added to \"$api_name\" hash" if $verbose;
        $api->{object_types} = $object_types;
    }

    return 1;
}

# Create and store the Attrs object
sub get_attributes {
    my $api  = shift;
    my $enum = shift;

    return 0 unless SAI::Attrs->validate_xml_typedef($enum);
    my $attrs = SAI::Attrs->new( xml_typedef => $enum );

    if ( $attrs and $attrs->object ) {
        $api->{objects}->{ $attrs->object }->{attrs} = $attrs;
    }

    return 1;
}

# Create and store the Stats object
sub get_stats {
    my $api  = shift;
    my $enum = shift;

    return 0 unless SAI::Stats->validate_xml_typedef($enum);
    my $stats = SAI::Stats->new( xml_typedef => $enum );

    if ( $stats and $stats->object ) {
        $api->{objects}->{ $stats->object }->{stats} = $stats;
    }

    return 1;
}

# Create and store the Typedef object
sub get_typedef {
    my $api         = shift;
    my $xml_typedef = shift;
    my $raw         = shift;

    # If $raw, the we can use raw thrift types only
    $raw //= 0;

    return 0 unless SAI::Typedef->validate_xml_typedef($xml_typedef);
    my $typedef = SAI::Typedef->new(
        raw         => $raw,
        xml_typedef => $xml_typedef,
    );

    push @{ $api->{typedefs} }, $typedef;

    return 1;
}

# In SAI RPC server we don't call SAI functions directly - pointers
# to them are stored in some structures, so we need to know their
# names.
sub get_method_names {
    my $struct = shift;

    my $function;
    my %methods;

    for my $method ( GetStructKeysInOrder($struct) ) {
        my $type =
          { SAI::Struct::Member->parse_xml_typedef( $struct->{$method} ) }
          ->{type};
        if ( $type =~ /^(\w+)_fn$/ ) { $function = $1 }
        else                         { next }

        $methods{$function} = $method;
    }

    return \%methods;
}

# Create and store the Struct object.
# The struct of API function pointers is an exception - just the its name.
sub get_struct {
    my $api           = shift;
    my $all_structs   = shift;
    my $methods_table = shift;
    my $xml_typedef   = shift;

    my @members;
    my $name;

    # Make sure we have a struct
    return 0 unless SAI::Struct->validate_xml_typedef($xml_typedef);

    $name = { SAI::Struct->parse_xml_typedef($xml_typedef) }->{name};
    my %struct_def = ExtractStructInfo( $name, 'struct_' );

    # If we have method table, then we don't create a structure -
    # just take the method names
    if ( $name =~ /_api_t$/ ) {
        my $method_names = get_method_names( \%struct_def );
        %{$methods_table} = ( %{$methods_table}, %{$method_names} );
        return 1;
    }

    push @members, SAI::Struct::Member->new( xml_typedef => $struct_def{$_} )
      for ( GetStructKeysInOrder( \%struct_def ) );

    my $struct = SAI::Struct->new(
        members     => \@members,
        xml_typedef => $xml_typedef,
    );

    push @{ $api->{structs} }, $struct;
    $all_structs->{ $struct->thrift_name } = $struct;

    return 1;
}

# Create and store the Function object.
sub get_function {
    my $api           = shift;
    my $all_functions = shift;
    my $definition    = shift;
    my $api_name      = shift;

    return 0 unless SAI::Function->validate_xml_typedef($definition);
    my $function = SAI::Function->new( xml_typedef => $definition );

    $function->api($api_name);

    push @{ $api->{functions} }, $function;
    $all_functions->{ $function->name } = $function;

    return 1;
}

__END__
