#! /usr/bin/perl
##
## Copyright 2019 The Android Open Source Project
##
## 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
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.

use File::Basename;

## mockcify version
##
## 0.7.1 Add tBTA_JV_STATUS return value
##       Remove unused compiler definition HAS_NO_BDROID_BUILDCFG
##
## 0.7.0 Comment out unused mock variables
##
## 0.6.3 Streamline inclusion for headers and source
##
## 0.6.2 Add tBTA_STATUS default value, Cpp type failure log
##
## 0.6.1 Add tBTA_SDP_STATUS default value
##
## 0.6.0 Replace `extern` with `include` for mock_function_count_map
##
## 0.5.0 Add compilation check
##
## 0.4.0 Second re-write
##
## 0.3.2 Remove pragma from source file
##
## 0.3.1 Statically link return value to prevent 'this' pointer in function
##
## 0.3.0 Re-write parser.
##
## 0.2.1 Compilation units only include types and a single related header file
##       Alphabetically sort functions by return value
##       Add non-primative return data values in structure
##
## 0.2.0 First version
##
my $VERSION = "0.7.1";

use diagnostics;
use strict;
use warnings;

use lib "$ENV{ANDROID_BUILD_TOP}/packages/modules/Bluetooth/system/test/tool";
require 'mockcify_util.pl';

my $YEAR = "2023";
my $TOKEN = "MOCKCIFY_TOKEN";
my $MOCKCIFY_BRACKET_GROUP = "MOCKCIFY_BRACKET_GROUP";
my $CLANG_FORMAT = "/usr/bin/clang-format-13";
my $CC = "g++";
my $LIBCHROME = "../../../../external/libchrome/";
my $COMPILE_SCREEN_ENABLED = 0;

my @structs;

my %function_signature;
my %function_return_types;
my @function_names;
my %function_params;
my %function_param_names;
my %function_param_types;

sub clang_format {
    return `$CLANG_FORMAT --style="{ColumnLimit: 10000, PointerAlignment: Left, PointerBindsToType: true, FixNamespaceComments: true }"`;
}

## Create a temp directory for any cruft
my $TMPDIR="/tmp/mockcify";
system("mkdir -p $TMPDIR");
my $OUTDIR = "$TMPDIR/out/";
system("mkdir -p $OUTDIR");
my $INCDIR = "$TMPDIR/include/";
system("mkdir -p $INCDIR");

if (scalar(@ARGV == 0)) {
  printf(STDERR "ERROR Must supply at least one argument\n");
  exit 1;
}

my $arg = shift @ARGV;
## Check only argument for debug vector
if ($arg =~ /--cla[ng]/) {
    exit print clang_format();
} elsif ($arg =~ /--f[ilter]/) {
    exit print read_stdin_and_filter_file();
} elsif ($arg =~ /--l[ines]/) {
    exit print filter_lines(read_stdin_and_filter_file());
} elsif ($arg =~ /--i[nfo]/) {
    my ($incs, $types, $funcs) = parse_info(filter_lines(read_stdin_and_filter_file()));
    exit print @{$incs}, @{$types}, @{$funcs};
} elsif ($arg =~ /--co[mpile]/) {
    exit compilation_screen("mock_" . shift @ARGV);
} elsif ($arg =~ /--cle[an]/) {
    exit system("mv $TMPDIR $TMPDIR.deleted");
} elsif ($arg =~ /--u[nittest]/) {
    print(STDERR "unit testing device");
}

sub help {
    print <<EOF
 Usage:
  Specify a namespace on the command line for the shared structure data.
  Then pipe the C file on stdin and one source and one header file will
  be created based upon the namespace convention

 mockcify.pl stack_l2cap_api < stack/l2cap/l2c_api.cc

 Output files:
  mock_stack_l2cap_api.cc
  mock_stack_l2cap_api.h

  The tool is not capable of parsing C++ and a workaround is to remove
  C++ in the source prior to mock-C-fying the source.

EOF
}

## Only single arg is taken
my $namespace = $arg;

if ($namespace =~ /^--/) {
    print(STDERR "ERROR Halting due to ill-formed namespace expression \'$namespace\'\n");
    exit -1;
}

###
### Phase 0: Prepare input and output file streams
###

## Default to stdout
my $FH_SRC;
my $FH_HDR;

my $src_filename;
my $hdr_filename;
## If namepace specified then write to that source and header
if ($namespace eq "TESTING") {
  $FH_SRC = *STDOUT;
  $FH_HDR = *STDOUT;
} else {
  $src_filename="mock_" . $namespace . ".cc";
  $hdr_filename="mock_" . $namespace . ".h";

  open($FH_SRC, ">", $OUTDIR .$src_filename)
    or die $!;

  open($FH_HDR, ">", $OUTDIR .$hdr_filename)
    or die $!;
}

###
### Phase 1: Read input file and apply single line filtering
###
my $text = read_stdin_and_filter_file();

###
### Phase 2: Apply Multiline filters
###
$text = filter_lines($text);

##
## Phase 3: Extract required mock information
##
my ($includes_ref, $typedefs_ref, $functions_ref, $usings_ref,  $namespaces_ref) = parse_info($text);
my @includes = @{$includes_ref};
my @typedefs = @{$typedefs_ref};
my @functions = @{$functions_ref};
my @namespaces = @{$namespaces_ref};
my @usings = @{$usings_ref};

@includes = reject_include_list(@includes);

@functions = grep { parse_function_into_components ($_) } @functions;

##
## Phase 4: Output the mocks source and header
##
print_source($FH_SRC);
print_header($FH_HDR);

close ($FH_SRC);
close ($FH_HDR);

## Format the final source code files
if (defined $src_filename) {
  system("clang-format", "-i", $OUTDIR . $src_filename);
  system("clang-format", "-i", $OUTDIR . $hdr_filename);
}

print(STDERR "Generated files:", $OUTDIR . $src_filename, " ", $OUTDIR . $hdr_filename, "\n");

if ($COMPILE_SCREEN_ENABLED) {
  my $rc = compilation_screen("mock_" . $namespace);
  exit ($rc == 256) ?1 : 0;
}

sub reject_include_list {
    my @incs = ();
    foreach (@_) {
      next if (/init_flags/);
      push(@incs, $_);
    }
    return @incs;
}

sub compile_screen_failed {
    my $src = shift @_;
    print STDERR <<EOF
    MOCK Compilation is EXPERIMENTAL ONLY

    ERROR Failed to compile \'$src\' NOTE: This does not mean
    the mock is unusable as the tool only screens the compilation.

    There could be one of 3 problems:
    1. Undeclared external surface or dependency
    2. C++ code or namespaces mixed in with C code
    3. An issue with proper mock'ing with mockcify.
EOF
}

sub compilation_screen {
    my $base= shift @_;
    my $src=$base . ".cc";
    my $hdr=$base . ".h";

    ## Verious external or generated header not needed for mocks
    foreach((
            "test/mock/mock.h",
            "src/init_flags.rs.h",
            "src/message_loop_thread.rs.h",
            "android/hardware/bluetooth/audio/2.2/IBluetoothAudioProvidersFactory.h",
            "android/hardware/bluetooth/audio/2.2/types.h",
        )) {
        system("mkdir -p $INCDIR". dirname($_));
        system("touch $INCDIR/$_");
    }
    my @incs = (
        $INCDIR,
        $LIBCHROME,
        ".",
        "audio_hal_interface/",
        "include/",
        "stack/include/",
        "btif/include/",
        "internal_include",
        "osi/include/",
        "test/mock/",
        "types/",
    );
    ## Any additional compiler definitions that may be required
    my @compiler_defs = (
    );

    my $link="test/mock/$hdr";
    unlink "$INCDIR/$link";
    symlink "$OUTDIR/$hdr", "$INCDIR/$link";
    system("$CC -c -std=c++20 -o /dev/null -D" . join(" -D", @compiler_defs) . " -I" . join(" -I", @incs) . " $OUTDIR/$src");
    my $rc = $?;
         ($? == 0)
         ? printf(STDERR "SUCCESS Compiled unit \'$src\'\n")
         : compile_screen_failed($src);
    return $rc;
}

###
### Phase 4.1: Print the source compilation unit and the associated structues
###
sub print_source {
  my $FH = shift @_;
  print_copyright($FH);
  print_generated_note($FH);

  print_mock_header_include($FH);
  print_mock_decl_src($FH);
  print_usings($FH);
  print_internal_structs($FH);
  print_source_namespace_structs($FH);
  print_static_return_values($FH);
  print_mocked_functions($FH);

  print $FH "// END mockcify generation\n";
}

###
### Phase 4.2 Print the header unit to be included with the test
###
sub print_header {
  my $FH = shift @_;
  print_copyright($FH);
  print_pragma($FH);
  print_generated_note($FH);
  print_mock_decl_hdr($FH);

  print_includes($FH);
  print_usings($FH);
  print_defs($FH);
  print_header_test_mock_namespace_structs($FH);
  print $FH "// END mockcify generation";
}

sub get_function_param_names {
    my $name = shift @_;
    my @param_names;
    foreach (0..$#{$function_param_names{$name}}) {
        my $param_name = $function_param_names{$name}[$_];
        my $param_type = $function_param_types{$name}[$_];

        if (!defined($param_type)) {
          printf(STDERR "Unable to find param type def for $name\n");
          next;
        }
        if ($param_type =~ /unique_ptr/) {
            ## Wrap name in a move operation
            push(@param_names, "std::move($param_name)");
        } else {
            push(@param_names, $param_name);
        }
    }
    return join(',', @param_names);
}

##
## Parse a function signature into 4 basic components and insert into
## the global hashes and arrays.
##  1. @function return type
##  2. @function name
##  3. %param types
##  4. %param names
##
sub parse_function_into_components {
  my $function = shift @_;
  ## Ensure this is really a function string
  assert(substr $function, -1 eq ')');

  ## Split on first occurrence of open paren to get return
  ## type and name of function
  my ($return_type_and_name, $params) = split '\(', $function, 2;
  if (!defined($params)) {
      printf(STDERR "WARNING \'params\' is undefined \"$params\" function:\'$function\'\n");
      return 0;
  }
  ## Remove input params closing paren
  $params=~ s/\).*$//;

  ## Parse the return type and function name
  my ($return_type, $name) = $return_type_and_name =~ /(.*)\s(.*)/;

  if (!defined($name)) {
      printf(STDERR "WARNING \'name\' is undefined \"$return_type_and_name\" a [con|des]tructor ?\n");
      return 0;
  }
  if ($name =~ /::/) {
      printf(STDERR "WARNING \'name\' is unhandled class method \'$name\'\n");
      return 0;
  }

  ## Store away complete function signature
  $function_signature{$name} = $function;

  ## Store away the parameter type and names
  chomp($params);
  $function_params{$name} = $params;

  ## Parse the parameter types and names
  my @param_types;
  my @param_names;

  ## Skip when void keyword used for no parameters
  if ($params ne "void") {
    ## TODO Replace all comma types within angle brackets before split
    foreach (split ',', $params) {
      s/^\s+//;
      if (/\(/) {
        ## TODO Parameter is a C style function
        my @vars;
        my @f = split /[\(\)]/;
        push(@vars, substr $f[1], 1);
      } else {
        ## Store the type and name
        my ($type, $name) = /(.*)\s(.*)/;
        push(@param_names, $name);
        push(@param_types, $type);
      }
    }
  }
  push(@function_names, $name);
  $function_return_types{$name} = $return_type;
  $function_param_types{$name} = \@param_types;
  $function_param_names{$name} = \@param_names;
  return 1;
}

##
## Read a file from stdin and does a first pass simple
## filtering that removes single lines.
##
sub read_stdin_and_filter_file {
  my @filtered_lines;
  my @clang_format=clang_format();
  foreach (@clang_format) {
    ## Update header guards with compiler #pragma for proper
    ## decision processing of header or source
    s/^#ifndef [A-Z_0-9]+_H/#pragma once/;

    unless (/^extern/
        or /^#define /
        or / = \{/
        or /^#if /
        or /^constexpr/
        or /^#ifdef/
        or /^#ifndef/
        or /^#else/
        or /^enum/
        or /^static.*;$/
        or /^#endif/) {
        ## Remove any single line C style comments
        s:/\*.*\*/::;
        push(@filtered_lines, $_);
      }
  }
  return join('', @filtered_lines);
}

sub filter_lines {
  $_ = shift @_;
  ## Remove anonymous namespaces
  ## $text =~ s/namespace \{.*\n\} \/\/ namespace/\n/sg;
  s/namespace \{.*\n\} \/\/ namespace?/\n/sg;
  s/namespace \{.?\n\}/\n/g;
  ## Remove C style comments
  s/\s*\/\*(?:(?!\*\/).)*\*\/\n?/\n/sg;
  ## Remove Cpp style comments
  s/\s*\/\/.*//g;
  ## Remove unnecessary bluetooth osi specific modifier
  s/UNUSED_ATTR//g;
  ## Modify internally defined structure typedefs
  s/typedef struct \{.*?\n\} (\w+);/typedef struct $MOCKCIFY_BRACKET_GROUP $1;/sg;
  ## Modify internally defined structure typedefs
  s/typedef struct (\w+) \{.*?\n\} (\w+);/struct $1 $MOCKCIFY_BRACKET_GROUP;/sg;
  ## Modify internally defined structures
  s/struct (\w+) \{.*?\n\};/struct $1 $MOCKCIFY_BRACKET_GROUP;/sg;
  ## Remove lines only with spaces
  s/^\s+$//sg;
  return $_;
}

sub parse_info {
    if (/\n#pragma once\n/) {
        return parse_info_header(shift @_);
    } else {
        return parse_info_source(shift @_);
    }
}

sub parse_info_header {
  my (@includes, @typedefs, @functions, @usings, @namespaces);
  foreach (split('\n')) {
      chomp();
      if (/^ /) {
      } elsif (/^#include /) {
          push(@includes, $_);
      } elsif (/^typedef /) {
          push @typedefs, $_;
      } elsif ($_ =~ /^ *$/) {
          # Skip function body indicated by indentation
      } elsif ($_ =~ /^}/) {
          # Skip function curly bracket closure
      } elsif (/^namespace/) {
          push @namespaces, $_;
      } elsif (/\(/) {
          # Add function signature
          chomp();
          ## Remove all function body after signature
          s/{.*$//;
          ## Remove whitespace on both ends
          s/^\s+|\s+$//g;
          ## Ignore locally linked functions
          next if (/^static/);
          ## Reduce all remaining whitespace to a single space
          s/\s+/ /g;
          ## Remove any semi colons
          s/;//g;
          push(@functions, "$_\n");
      } else {
          # Not a function. skip
      }
  }
  printf(STDERR "Parsed HEADER lines includes:%d typedefs:%d functions:%d\n",
      scalar(@includes), scalar(@typedefs), scalar(@functions));
  return (\@includes, \@typedefs, \@functions, \@usings, \@namespaces);
}

sub parse_info_source{
  my @s = split('\n', $_);
  my (@includes, @typedefs, @functions, @usings, @namespaces);
  foreach (@s) {
      chomp();
      if (/^ /) {
      } elsif (/^#include /) {
          push @includes, $_;
      } elsif (/^typedef /) {
          push @typedefs, $_;
      } elsif (/^using /) {
          push @usings, $_;
      } elsif (/^namespace/) {
          push @namespaces, $_;
      } elsif ($_ =~ /^ *$/) {
          # Skip function body indicated by indentation
      } elsif ($_ =~ /^}/) {
          # Skip function curly bracket closure
        } elsif (/\{/) {
          # Add function signature
          chomp();
          ## Remove all function body after signature
          s/{.*$//;
          ## Remove whitespace on both ends
          s/^\s+|\s+$//g;
          ## Ignore locally linked functions
          next if (/^static/);
          ## Reduce all remaining whitespace to a single space
          s/\s+/ /g;
          push(@functions, "$_\n");
      } else {
          # Not a function. skip
      }
  }
  printf(STDERR "Parsed SOURCE lines includes:%d typedefs:%d functions:%d\n",
      scalar(@includes), scalar(@typedefs), scalar(@functions));
  return (\@includes, \@typedefs, \@functions, \@usings, \@namespaces);
}

## Returns the default type specified by the function return type.
## These are processed in priority order.
sub get_default_return_value_from_type {
  $_ = shift @_;
  assert($_ ne '');
  if (/^bool/) {
    return "false";
  } elsif (/\*$/ or /^std::unique_ptr/ or /^std::shared_ptr/) {  ## Pointer return val
    return "nullptr";
  } elsif (/^void/) {
    return "";
  } elsif (/^std::string/) {
    return "std::string()";
  } elsif (/^std::list\<entry_t\>::iterator/) {
    return "static std::list<entry_t> v";
  } elsif (/^std::list\<section_t\>::iterator/) {
    return "std::list<section_t>";
  } elsif (/reactor_status_t/) {
    return "REACTOR_STATUS_DONE";
  } elsif (/tL2CAP_LE_RESULT_CODE/) {
    return "L2CAP_LE_RESULT_CONN_OK";
  } elsif (/std::vector/) {
    return "retval";
  } elsif (/tBT_TRANSPORT/) {
    return "BT_TRANSPORT_BR_EDR";
  } elsif (/tSDP_STATUS/) {
    return "SDP_SUCCESS";
  } elsif (/tGATT_STATUS/) {
    return "GATT_SUCCESS";
  } elsif (/tHID_STATUS/) {
    return "HID_SUCCESS";
  } elsif (/future_t\*/) {
    return "FUTURE_FAIL";
  } elsif(/bt_status_t/) {
    return "BT_STATUS_SUCCESS";
  } elsif(/.*module_t\*/) {
    return "nullptr";
  } elsif(/btav_a2dp_codec_index_t/) {
    return "BTAV_A2DP_CODEC_INDEX_SOURCE_MIN";
  } elsif(/tBTA_SDP_STATUS/) {
    return "BTA_SDP_SUCCESS";
  } elsif(/tBTA_STATUS/) {
    return "BTA_SUCCESS";
  } elsif(/tBTA_JV_STATUS/) {
    return "tBTA_JV_STATUS::SUCCESS";
  } else {
    ## Decay to int type
    return "0";
  }
}

##
## Various print output boilerplate
###
sub print_copyright {
  my $FH = shift @_;
print $FH <<EOF
/*
 * Copyright $YEAR The Android Open Source Project
 *
 * 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
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
EOF
}

## Print body of each function
sub print_mocked_functions {
  my $FH = shift @_;
  print $FH <<EOF;
// Mocked functions, if any
EOF
  foreach my $name (sort @function_names) {
      my $return_type = $function_return_types{$name};
      assert($return_type ne '');

      my $return_keyword = $return_type eq "void" ? "" : "return";
      my $function_param_names = get_function_param_names($name);

      print $FH <<EOF;
$function_signature{$name} {
    inc_func_call_count(__func__);
    ${return_keyword} test::mock::${namespace}::${name}($function_param_names);
}
EOF
  }
  print $FH <<EOF;
// Mocked functions complete
EOF
}

sub print_static_return_values {
  my $FH = shift @_;
  print $FH <<EOF;
// Mocked function return values, if any
namespace test {
namespace mock {
namespace $namespace {

EOF
  foreach my $name (sort @function_names) {
      $name =~ s/\s+$//;
      my $return_type = $function_return_types{$name};
      assert($return_type ne '');

      next if ($return_type eq "void");
      my $default_return_value = get_default_return_value_from_type($return_type);
      print $FH "${return_type} ${name}::return_value = ${default_return_value};\n";
  }
  print $FH <<EOF;

} // namespace $namespace
} // namespace mock
} // namespace test

EOF
}

##
## Collection of mocked functions
sub print_source_namespace_structs {
  my $FH = shift @_;
  print $FH <<EOF;
namespace test {
namespace mock {
namespace $namespace {

// Function state capture and return values, if needed
EOF
    foreach my $name (sort @function_names) {
      print $FH "struct $name $name;\n";
    }
    print $FH <<EOF;

} // namespace $namespace
} // namespace mock
} // namespace test

EOF
}

##
##  Print the definitions of the various structures for the header files
##
sub print_header_test_mock_namespace_structs {
  my $FH = shift @_;
  print $FH <<EOF;
namespace test {
namespace mock {
namespace $namespace {

// Shared state between mocked functions and tests
EOF
  foreach my $name (sort @function_names) {
      my $input_params = $function_params{$name};
      my $vars_commented_out_input_params = comment_out_input_vars($input_params);
      my $return_type = $function_return_types{$name};
      my @param_names = $function_param_names{$name};
      assert($return_type ne '');

      my $function_param_names = get_function_param_names($name);
      my $return_keyword = $return_type eq "void" ? "" : "return";
      my $return_statement = $return_type eq "void" ? "" : "return return_value;";
      my $return_definition = $return_type eq "void" ? "" : "static $return_type return_value;";

print $FH <<EOF;
// Name: $name
// Params: $input_params
// Return: $return_type
struct $name {
EOF
       if ($return_definition ne "") {
           print $FH "$return_definition\n";
       }
print $FH <<EOF;
    std::function<$return_type($input_params)> body{[]($vars_commented_out_input_params){$return_statement}};
    $return_type operator()($input_params) { ${return_keyword} body($function_param_names);};
};
extern struct $name $name;

EOF
    }
print $FH <<EOF;
} // namespace $namespace
} // namespace mock
} // namespace test

EOF
}

sub print_pragma {
  my $FH = shift @_;
print $FH <<EOF
#pragma once

EOF
}

sub print_generated_note {
  my $FH = shift @_;
  my $gen = scalar(@functions);
print $FH <<EOF;
/*
 * Generated mock file from original source file
 *   Functions generated:$gen
 *
 *  mockcify.pl ver $VERSION
 */

EOF
}

sub print_usings {
  my $FH = shift @_;
print $FH <<EOF;
// Original usings
EOF
  foreach (sort @usings) {
    print $FH $_, "\n";
  }
  print($FH "\n");;
}

sub print_includes {
  my $FH = shift @_;
  print $FH <<EOF;
// Original included files, if any
// NOTE: Since this is a mock file with mock definitions some number of
//       include files may not be required.  The include-what-you-use
//       still applies, but crafting proper inclusion is out of scope
//       for this effort.  This compilation unit may compile as-is, or
//       may need attention to prune from (or add to ) the inclusion set.
EOF
  foreach (sort @includes) {
    print $FH $_, "\n";
  }
  print($FH "\n");;
}

sub print_mock_header_include {
  my $FH = shift @_;
  print $FH <<EOF;
// Mock include file to share data between tests and mock
#include "test/mock/mock_${namespace}.h"

EOF
}

sub print_mock_decl_hdr {
  my $FH = shift @_;
print $FH <<EOF;
#include <cstdint>
#include <functional>

EOF
}

sub print_mock_decl_src {
  my $FH = shift @_;
print $FH <<EOF;
#include <cstdint>

#include "test/common/mock_functions.h"

EOF
}

sub print_defs {
  my $FH = shift @_;
  print $FH <<EOF;
// Mocked compile conditionals, if any

EOF
}

sub print_internal_structs {
  my $FH = shift @_;
  print $FH <<EOF;
// Mocked internal structures, if any
EOF

  foreach (sort @structs) {
    print $FH $_,"\n"};
  print $FH "\n";
}

sub assert {
    my ($condition, $msg) = @_;
    return if $condition;
    if (!$msg) {
        my ($pkg, $file, $line) = caller(0);
        open my $fh, "<", $file;
        my @lines = <$fh>;
        close $fh;
        $msg = "$file:$line: " . $lines[$line - 1];
    }
    die "Assertion failed: $msg";
}
