#include <libasr/asr.h>
#include <libasr/containers.h>
#include <libasr/exception.h>
#include <libasr/asr_utils.h>
#include <libasr/asr_verify.h>
#include <libasr/pass/transform_optional_argument_functions.h>
#include <libasr/pass/pass_utils.h>
#include <libasr/pass/intrinsic_function_registry.h>

#include <vector>
#include <string>
#include <set>

/*
Need for the pass
==================

Since LLVM IR does not directly support optional arguments, this ASR pass converts optional
arguments of a function/subroutine and function call or subroutine call to two
non-optional arguments, the first argument is same as the original argument and the second
boolean argument to denote the presence of the original optional argument (i.e. `is_var_present_`).

Transformation by the pass
==========================

Consider a function named 'square' with one integer argument 'x' and 'integer(4)' return type,
and with call made to it, it's Fortran code before this pass would look like:

```fortran
integer(4) function square(x)
    integer(4), intent(in), optional :: x   ! one optional argument
    if (present(x)) then
        square = x*x
    else
        square = 1
    end if
end function square

print *, square(4)                          ! function call with present optional argument '4'
```

and after `transform_optional_argument_functions` pass it would look like:

```fortran
integer(4) function square(x, is_x_present_)
    logical(4), intent(in) :: is_x_present_     ! boolean non-optional argument
    integer(4), intent(in) :: x             ! optional argument 'x' is now non-optional argument
    if (is_x_present_) then
        square = x*x
    else
        square = 1
    end if
end function square

print *, square(4, .true.)                  ! function call with second boolean argument set to .true.
```

This same change is done for every optional argument(s) present in the function/subroutine.
*/
namespace LCompilers {

using ASR::down_cast;
using ASR::is_a;

class ReplacePresentCalls: public ASR::BaseExprReplacer<ReplacePresentCalls> {

    private:

    Allocator& al;
    ASR::Function_t* f;

    public:

    ReplacePresentCalls(Allocator& al_, ASR::Function_t* f_) : al{al_}, f{f_}
    {}

    void replace_IntrinsicElementalFunction(ASR::IntrinsicElementalFunction_t* x) {
        if (x->m_intrinsic_id == static_cast<int64_t>(ASRUtils::IntrinsicElementalFunctions::Present)) {
            if (!ASR::is_a<ASR::Var_t>(*x->m_args[0])) {
                return;
            }
            ASR::symbol_t* present_arg = ASR::down_cast<ASR::Var_t>(x->m_args[0])->m_v;
            size_t i;
            for( i = 0; i < f->n_args; i++ ) {
                if( ASR::down_cast<ASR::Var_t>(f->m_args[i])->m_v ) {
                    if( ASR::down_cast<ASR::Var_t>(f->m_args[i])->m_v == present_arg ) {
                        i++;
                        break;
                    }
                }
            }

            if (i < f->n_args) {
                *current_expr = ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc,
                                    ASR::down_cast<ASR::Var_t>(f->m_args[i])->m_v));
                return;
            }
        }
        for (size_t i = 0; i < x->n_args; i++) {
            ASR::expr_t** current_expr_copy_12 = current_expr;
            current_expr = &(x->m_args[i]);
            replace_expr(x->m_args[i]);
            current_expr = current_expr_copy_12;
        }
        replace_ttype(x->m_type);
        ASR::expr_t** current_expr_copy_13 = current_expr;
        current_expr = &(x->m_value);
        replace_expr(x->m_value);
        current_expr = current_expr_copy_13;
    }

    void replace_FunctionCall(ASR::FunctionCall_t* x) {
        ASR::symbol_t* x_sym = x->m_name;
        bool replace_func_call = false;
        if( ASR::is_a<ASR::ExternalSymbol_t>(*x_sym) ) {
            ASR::ExternalSymbol_t* x_ext_sym = ASR::down_cast<ASR::ExternalSymbol_t>(x_sym);
            if( std::string(x_ext_sym->m_module_name) == "lfortran_intrinsic_builtin" &&
                std::string(x_ext_sym->m_name) == "present" ) {
                replace_func_call = true;
            }
        }

        if( !replace_func_call ) {
            return ;
        }

        ASR::symbol_t* present_arg = ASR::down_cast<ASR::Var_t>(x->m_args[0].m_value)->m_v;
        size_t i;
        for( i = 0; i < f->n_args; i++ ) {
            if( ASR::down_cast<ASR::Var_t>(f->m_args[i])->m_v == present_arg ) {
                i++;
                break;
            }
        }

        *current_expr = ASRUtils::EXPR(ASR::make_Var_t(al, x->base.base.loc,
                            ASR::down_cast<ASR::Var_t>(f->m_args[i])->m_v));
    }

};


class ReplacePresentCallsVisitor : public ASR::CallReplacerOnExpressionsVisitor<ReplacePresentCallsVisitor>
{
    private:

        ReplacePresentCalls replacer;

    public:

        ReplacePresentCallsVisitor(Allocator& al_,
            ASR::Function_t* f_) : replacer(al_, f_) {}

        void call_replacer() {
            replacer.current_expr = current_expr;
            replacer.replace_expr(*current_expr);
        }

};

class TransformFunctionsWithOptionalArguments: public PassUtils::PassVisitor<TransformFunctionsWithOptionalArguments> {

    public:

        std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx;

        TransformFunctionsWithOptionalArguments(Allocator &al_,
            std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx_) :
        PassVisitor(al_, nullptr), sym2optionalargidx(sym2optionalargidx_)
        {
            pass_result.reserve(al, 1);
        }

        void transform_functions_with_optional_arguments(ASR::Function_t* s) {
            Vec<ASR::expr_t*> new_args;
            Vec<ASR::ttype_t*> new_arg_types;
            new_arg_types.reserve(al, s->n_args);
            new_args.reserve(al, s->n_args);
            ASR::ttype_t* logical_type = ASRUtils::TYPE(ASR::make_Logical_t(al, s->base.base.loc, 4));
            for( size_t i = 0; i < s->n_args; i++ ) {
                ASR::symbol_t* arg_sym = ASR::down_cast<ASR::Var_t>(s->m_args[i])->m_v;
                new_args.push_back(al, s->m_args[i]);
                new_arg_types.push_back(al, ASRUtils::get_FunctionType(*s)->m_arg_types[i]);
                if( is_presence_optional(arg_sym, true) ) {
                    sym2optionalargidx[&(s->base)].push_back(new_args.size() - 1);
                    std::string presence_bit_arg_name = "__libasr_is_present_" + std::string(ASRUtils::symbol_name(arg_sym));
                    presence_bit_arg_name = s->m_symtab->get_unique_name(presence_bit_arg_name, false);
                    ASR::expr_t* presence_bit_arg = PassUtils::create_auxiliary_variable(
                                                        arg_sym->base.loc, presence_bit_arg_name,
                                                        al, s->m_symtab, logical_type, ASR::intentType::In);
                    new_args.push_back(al, presence_bit_arg);
                    new_arg_types.push_back(al, logical_type);
                }
            }

            ASR::FunctionType_t* function_type = ASRUtils::get_FunctionType(*s);
            function_type->m_arg_types = new_arg_types.p;
            function_type->n_arg_types = new_arg_types.size();
            s->m_args = new_args.p;
            s->n_args = new_args.size();
            ReplacePresentCallsVisitor present_replacer(al, s);
            present_replacer.visit_Function(*s);
        }

        bool is_presence_optional(ASR::symbol_t* sym, bool set_presence_to_required=false) {
            if( ASR::is_a<ASR::Variable_t>(*sym) ) {
                ASR::Variable_t* sym_ = ASR::down_cast<ASR::Variable_t>(sym);
                if (sym_->m_presence == ASR::presenceType::Optional) {
                    if( set_presence_to_required ) {
                        sym_->m_presence = ASR::presenceType::Required;
                    }
                    return true;
                }
            }
            return false;
        }

        bool is_optional_argument_present(ASR::Function_t* s) {
            // bind(C) functions use NULL for absent optional arguments
            // per the Fortran standard; no is_present parameter needed.
            if (ASRUtils::get_FunctionType(s)->m_abi == ASR::abiType::BindC) {
                return false;
            }
            for( size_t i = 0; i < s->n_args; i++ ) {
                ASR::symbol_t* arg_sym = ASR::down_cast<ASR::Var_t>(s->m_args[i])->m_v;
                if( is_presence_optional(arg_sym) ) {
                    return true;
                }
            }
            return false;
        }

        void visit_TranslationUnit(const ASR::TranslationUnit_t &x) {
            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = down_cast<ASR::Function_t>(item.second);
                    if (is_optional_argument_present(s)) {
                        transform_functions_with_optional_arguments(s);
                    }
                }
            }

            // Now visit everything else
            for (auto &item : x.m_symtab->get_scope()) {
                this->visit_symbol(*item.second);
            }
        }

        void visit_Program(const ASR::Program_t &x) {
            // FIXME: this is a hack, we need to pass in a non-const `x`,
            // which requires to generate a TransformVisitor.
            ASR::Program_t &xx = const_cast<ASR::Program_t&>(x);
            current_scope = xx.m_symtab;

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = down_cast<ASR::Function_t>(item.second);
                    if (is_optional_argument_present(s)) {
                        transform_functions_with_optional_arguments(s);
                    }
                }
            }

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::AssociateBlock_t>(*item.second)) {
                    ASR::AssociateBlock_t *s = ASR::down_cast<ASR::AssociateBlock_t>(item.second);
                    visit_AssociateBlock(*s);
                }
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = ASR::down_cast<ASR::Function_t>(item.second);
                    visit_Function(*s);
                }
            }

            current_scope = xx.m_symtab;
            transform_stmts(xx.m_body, xx.n_body);

        }

        void visit_Module(const ASR::Module_t &x) {
            // FIXME: this is a hack, we need to pass in a non-const `x`,
            // which requires to generate a TransformVisitor.
            ASR::Module_t &xx = const_cast<ASR::Module_t&>(x);
            current_scope = xx.m_symtab;

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = down_cast<ASR::Function_t>(item.second);
                    if (is_optional_argument_present(s)) {
                        transform_functions_with_optional_arguments(s);
                    }
                }
            }

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::AssociateBlock_t>(*item.second)) {
                    ASR::AssociateBlock_t *s = ASR::down_cast<ASR::AssociateBlock_t>(item.second);
                    visit_AssociateBlock(*s);
                }
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = ASR::down_cast<ASR::Function_t>(item.second);
                    visit_Function(*s);
                }
            }

        }

        void visit_Function(const ASR::Function_t &x) {
            // FIXME: this is a hack, we need to pass in a non-const `x`,
            // which requires to generate a TransformVisitor.
            ASR::Function_t &xx = const_cast<ASR::Function_t&>(x);
            current_scope = xx.m_symtab;

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = down_cast<ASR::Function_t>(item.second);
                    if (is_optional_argument_present(s)) {
                        transform_functions_with_optional_arguments(s);
                    }
                }
            }

            for (auto &item : x.m_symtab->get_scope()) {
                if (is_a<ASR::AssociateBlock_t>(*item.second)) {
                    ASR::AssociateBlock_t *s = ASR::down_cast<ASR::AssociateBlock_t>(item.second);
                    visit_AssociateBlock(*s);
                }
                if (is_a<ASR::Function_t>(*item.second)) {
                    ASR::Function_t *s = ASR::down_cast<ASR::Function_t>(item.second);
                    visit_Function(*s);
                }
            }

        }

};

template <typename T>
bool fill_new_args(Vec<ASR::call_arg_t>& new_args, Allocator& al,
    const T& x, SymbolTable* scope, std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx, Vec<ASR::stmt_t*>& pass_result) {
    ASR::Function_t* owning_function = nullptr;
    {
        SymbolTable* s = scope;
        while( s->asr_owner && ASR::is_a<ASR::symbol_t>(*s->asr_owner) ) {
            ASR::symbol_t* owner_sym = ASR::down_cast<ASR::symbol_t>(s->asr_owner);
            if( ASR::is_a<ASR::Function_t>(*owner_sym) ) {
                owning_function = ASR::down_cast<ASR::Function_t>(owner_sym);
                break;
            } else if( ASR::is_a<ASR::Block_t>(*owner_sym) ) {
                s = s->parent;
            } else {
                break;
            }
        }
    }

    ASR::symbol_t* func_sym = ASRUtils::symbol_get_past_external(x.m_name);
    if (ASR::is_a<ASR::Variable_t>(*func_sym)) {
        // possible it is a `procedure(cb) :: call_back`
        ASR::Variable_t* v = ASR::down_cast<ASR::Variable_t>(func_sym);
        LCOMPILERS_ASSERT(ASR::is_a<ASR::FunctionType_t>(*ASRUtils::extract_type(v->m_type)));
        func_sym = ASRUtils::symbol_get_past_external(v->m_type_declaration);
        ASR::ttype_t* new_type = ASRUtils::duplicate_type(al, ASR::down_cast<ASR::Function_t>(
            ASRUtils::symbol_get_past_external(v->m_type_declaration))->m_function_signature);
        if (ASR::is_a<ASR::Pointer_t>(*v->m_type)) {
            new_type = ASRUtils::TYPE(ASR::make_Pointer_t(al, v->base.base.loc, new_type));
        }
        v->m_type = new_type;
    }
    if (ASR::is_a<ASR::StructMethodDeclaration_t>(*func_sym)) {
        ASR::StructMethodDeclaration_t* class_proc = ASR::down_cast<ASR::StructMethodDeclaration_t>(func_sym);
        func_sym = class_proc->m_proc;
    }

    if (!ASR::is_a<ASR::Function_t>(*func_sym)) {
        return false;
    }

    ASR::Function_t* func = ASR::down_cast<ASR::Function_t>(func_sym);
    bool replace_func_call = false;
    for( size_t i = 0; i < func->n_args; i++ ) {
        if (std::find(sym2optionalargidx[func_sym].begin(),
                      sym2optionalargidx[func_sym].end(), i)
            != sym2optionalargidx[func_sym].end()) {
            replace_func_call = true;
            break ;
        }
    }

    if( !replace_func_call ) {
        return false;
    }

    // Self is now explicitly in call args, so no offset is needed.
    // The first argument (i=0) for method calls is self.

    new_args.reserve(al, func->n_args);
    for( int i = 0, j = 0; j < (int)func->n_args; j++, i++ ) {
        if( std::find(sym2optionalargidx[func_sym].begin(),
                      sym2optionalargidx[func_sym].end(), j)
            != sym2optionalargidx[func_sym].end() ) {
            ASR::Variable_t* func_arg_j = ASRUtils::EXPR2VAR(func->m_args[j]);
            if( i >= (int)x.n_args || x.m_args[i].m_value == nullptr ) {
                std::string m_arg_i_name = scope->get_unique_name("__libasr_created_variable_");
                ASR::ttype_t* arg_type = ASRUtils::duplicate_type(al, func_arg_j->m_type);
                if(ASR::is_a<ASR::String_t>(*ASRUtils::extract_type(arg_type))){// Create String type with dummy len info.
                    arg_type = ASRUtils::duplicate_type(al, arg_type); // New-duplicated node
                    ASR::String_t* str = ASR::down_cast<ASR::String_t>(
                        ASRUtils::extract_type(arg_type));
                    if( str->m_len &&
                        !ASR::is_a<ASR::IntegerConstant_t>(*str->m_len)){
                        str->m_len = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, arg_type->base.loc, 0,
                            ASRUtils::TYPE(ASR::make_Integer_t(al, arg_type->base.loc, 4))));
                    }
                }
                ASR::symbol_t* arg_decl = func_arg_j->m_type_declaration;
                if( ASR::is_a<ASR::Array_t>(*arg_type) ) { // Create dummy array dims
                    ASR::Array_t* array_t = ASR::down_cast<ASR::Array_t>(arg_type);
                    Vec<ASR::dimension_t> dims;
                    dims.reserve(al, array_t->n_dims);
                    for( int i = 0; i < (int)array_t->n_dims; i++ ) {
                        ASR::dimension_t dim;
                        dim.m_length = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, arg_type->base.loc, 1,
                                            ASRUtils::TYPE(ASR::make_Integer_t(al, arg_type->base.loc, 4))));
                        dim.m_start = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, arg_type->base.loc, 1,
                                            ASRUtils::TYPE(ASR::make_Integer_t(al, arg_type->base.loc, 4))));
                        dim.loc = arg_type->base.loc;
                        dims.push_back(al, dim);
                    }
                    arg_type = ASRUtils::TYPE(ASR::make_Array_t(al, arg_type->base.loc,
                                array_t->m_type, dims.p, dims.size(),
                                (ASRUtils::is_character(*array_t->m_type) || ASRUtils::is_class_type(array_t->m_type))
                                    ? ASR::PointerArray : ASR::FixedSizeArray));
                }
                ASR::expr_t* m_arg_i = PassUtils::create_auxiliary_variable(
                    x.m_args[i].loc, m_arg_i_name, al, scope, arg_type, ASR::intentType::Local, arg_decl, func->m_args[j]);
                arg_type = ASRUtils::expr_type(m_arg_i);
                if( ASRUtils::is_array(arg_type) &&
                    ASRUtils::extract_physical_type(arg_type) !=
                    ASRUtils::extract_physical_type(func_arg_j->m_type)) {
                    ASR::ttype_t* m_type = ASRUtils::duplicate_type(al, arg_type, nullptr,
                        ASRUtils::extract_physical_type(func_arg_j->m_type), true);
                    m_arg_i = ASRUtils::EXPR(ASRUtils::make_ArrayPhysicalCast_t_util(
                        al, m_arg_i->base.loc, m_arg_i, ASRUtils::extract_physical_type(arg_type),
                        ASRUtils::extract_physical_type(func_arg_j->m_type), m_type, nullptr));
                }
                ASR::call_arg_t m_call_arg_i;
                m_call_arg_i.loc = x.m_args[i].loc;
                m_call_arg_i.m_value = m_arg_i;
                new_args.push_back(al, m_call_arg_i);
            } else {
                new_args.push_back(al, x.m_args[i]);
            }
            ASR::ttype_t* logical_t = ASRUtils::TYPE(ASR::make_Logical_t(al,
                                        x.m_args[i].loc, 4));
            ASR::expr_t* is_present = nullptr;
            if( i >= (int)x.n_args || x.m_args[i].m_value == nullptr ) {
                is_present = ASRUtils::EXPR(ASR::make_LogicalConstant_t(
                    al, x.m_args[0].loc, false, logical_t));
            } else {
                if( owning_function != nullptr ) {
                    size_t k;
                    bool k_found = false;
                    ASR::expr_t* original_expr = nullptr;
                    if (ASR::is_a<ASR::ArrayPhysicalCast_t>(*x.m_args[i].m_value)) {
                        ASR::ArrayPhysicalCast_t *x_array_cast = ASR::down_cast<ASR::ArrayPhysicalCast_t>(x.m_args[i].m_value);
                        original_expr = x_array_cast->m_arg;
                    }
                    for( k = 0; k < owning_function->n_args; k++ ) {
                        if( original_expr && ASR::is_a<ASR::Var_t>(*original_expr) && ASR::down_cast<ASR::Var_t>(owning_function->m_args[k])->m_v ==
                            ASR::down_cast<ASR::Var_t>(original_expr)->m_v ) {
                            k_found = true;
                            break ;
                        }

                        if( ASR::is_a<ASR::Var_t>(*x.m_args[i].m_value) && ASR::down_cast<ASR::Var_t>(owning_function->m_args[k])->m_v ==
                            ASR::down_cast<ASR::Var_t>(x.m_args[i].m_value)->m_v ) {
                            k_found = true;
                            break ;
                        }
                    }

                    if( k_found && std::find(sym2optionalargidx[&(owning_function->base)].begin(),
                            sym2optionalargidx[&(owning_function->base)].end(), k)
                                != sym2optionalargidx[&(owning_function->base)].end() ) {
                        is_present = owning_function->m_args[k + 1];
                    }
                }

                if( is_present == nullptr ) {
                    ASR::expr_t* arg_i = x.m_args[i].m_value;
                    const Location& loc = arg_i->base.loc;
                    LCOMPILERS_ASSERT(arg_i != nullptr);
                    bool is_dummy_pointer = ASRUtils::is_pointer(func_arg_j->m_type);
                    bool is_data_pointer = ASRUtils::is_pointer(ASRUtils::expr_type(arg_i));
                    bool is_proc_pointer = ASR::is_a<ASR::FunctionType_t>(
                        *ASRUtils::expr_type(arg_i)) &&
                        ASR::is_a<ASR::Var_t>(*arg_i) &&
                        ASR::is_a<ASR::Variable_t>(
                            *ASR::down_cast<ASR::Var_t>(arg_i)->m_v) &&
                        ASRUtils::EXPR2VAR(arg_i)->m_intent == ASR::intentType::Local;
                    if( !is_dummy_pointer && (is_data_pointer || is_proc_pointer) ) {
                        ASR::ttype_t* associated_type_ = ASRUtils::TYPE(
                            ASR::make_Logical_t(al, loc, 4));
                        is_present = ASRUtils::EXPR(ASR::make_PointerAssociated_t(
                            al, loc, arg_i, nullptr, associated_type_, nullptr));
                    } else {
                        is_present = ASRUtils::EXPR(ASR::make_LogicalConstant_t(
                            al, loc, true, logical_t));
                    }
                }
            }
            ASR::call_arg_t present_arg;
            present_arg.loc = x.m_args[i].loc;
            if( i < (int)x.n_args &&
                x.m_args[i].m_value &&
                ASRUtils::is_allocatable(x.m_args[i].m_value) &&
                !ASRUtils::is_allocatable(func_arg_j->m_type) ) {
                ASR::expr_t* arg_expr = x.m_args[i].m_value;
                ASR::ttype_t* arg_expr_type = ASRUtils::expr_type(arg_expr);
                // Create a temporary variable if arg_expr is a FunctionCall
                // This is to avoid calling the function more than once
                if (ASR::is_a<ASR::FunctionCall_t>(*arg_expr)) {
                    std::string dummy_variable_name = scope->get_unique_name("__libasr_created_dummy_variable_functioncall_");
                    ASR::expr_t* dummy_variable = PassUtils::create_auxiliary_variable(
                        x.m_args[i].loc, dummy_variable_name, al, scope,
                        arg_expr_type, ASR::intentType::Local);
                    ASR::stmt_t* assignment = ASRUtils::STMT(
                            ASRUtils::make_Assignment_t_util(al, x.m_args[i].loc, dummy_variable,
                                arg_expr, nullptr, false, ASRUtils::is_array(arg_expr_type)));
                    pass_result.push_back(al, assignment);
                    arg_expr = dummy_variable;
                }
                Vec<ASR::expr_t*> arg_expr_vec;
                arg_expr_vec.reserve(al, 1);
                arg_expr_vec.push_back(al, arg_expr);
                ASR::expr_t* is_allocated = ASRUtils::EXPR(ASR::make_IntrinsicImpureFunction_t(
                    al, x.m_args[i].loc, static_cast<int64_t>(ASRUtils::IntrinsicImpureFunctions::Allocated),
                    arg_expr_vec.p, arg_expr_vec.n, 0, logical_t, nullptr));
                is_present = ASRUtils::EXPR(ASR::make_LogicalBinOp_t(al, x.m_args[i].loc,
                    is_allocated, ASR::logicalbinopType::And, is_present, logical_t, nullptr));

                // If the argument is allocated then pass in the actual argument
                // else pass in a dummy variable allocated on the stack
                // This is to prevent passing in unallocated arguments when non-allocatable arguments are expected by the procedure
                ASR::symbol_t* arg_decl = func_arg_j->m_type_declaration;
                ASR::ttype_t* dummy_variable_type = ASRUtils::duplicate_type(al, func_arg_j->m_type);

                // We make dummy variable allocatable if class type because 
                // local class variable should be llvm pointer
                if (ASRUtils::is_class_type(dummy_variable_type)) {
                    dummy_variable_type = ASRUtils::TYPE(ASRUtils::make_Allocatable_t_util(
                        al, dummy_variable_type->base.loc, dummy_variable_type));
                }
                if (arg_decl && ASRUtils::is_unlimited_polymorphic_type(arg_decl)) {
                    dummy_variable_type = ASRUtils::duplicate_type(al, ASRUtils::type_get_past_allocatable_pointer(arg_expr_type));
                }
                ASR::ttype_t* pointer_variable_type = ASRUtils::duplicate_type(al, ASRUtils::type_get_past_allocatable_pointer(arg_expr_type));
                // Don't declare AssumedLength strings, they are only arguments
                if (ASRUtils::is_string_only(dummy_variable_type) || ASRUtils::is_array_of_strings(dummy_variable_type)) {
                    ASR::ttype_t *int_type = ASRUtils::TYPE(ASR::make_Integer_t(al, dummy_variable_type->base.loc, 4));
                    ASR::expr_t* one = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, dummy_variable_type->base.loc, 1, int_type));
                    ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(dummy_variable_type))->m_len_kind = ASR::ExpressionLength;
                    ASR::down_cast<ASR::String_t>(ASRUtils::extract_type(dummy_variable_type))->m_len = one;
                }
                // Pass in a FixedSizeArray of same rank when non-allocatable DescriptorArray is expected
                if (ASRUtils::is_array(dummy_variable_type) &&
                    !ASRUtils::is_allocatable(dummy_variable_type) &&
                    ASRUtils::extract_physical_type(dummy_variable_type) == ASR::array_physical_typeType::DescriptorArray) {
                    Vec<ASR::dimension_t> dims;
                    dims.reserve(al, 1);
                    size_t n_dims = ASRUtils::extract_n_dims_from_ttype(dummy_variable_type);
                    for (size_t i = 0; i < n_dims; i++) {
                        ASR::dimension_t dim;
                        ASR::ttype_t *int_type = ASRUtils::TYPE(ASR::make_Integer_t(al, dummy_variable_type->base.loc, 4));
                        ASR::expr_t* one = ASRUtils::EXPR(ASR::make_IntegerConstant_t(al, dummy_variable_type->base.loc, 1, int_type));
                        dim.m_start = one;
                        dim.m_length = one;
                        dims.push_back(al, dim);
                    }
                    ASR::array_physical_typeType phy_type = ASR::array_physical_typeType::FixedSizeArray;
                    if (ASRUtils::is_string_only(ASRUtils::extract_type(dummy_variable_type)) ||
                            ASRUtils::is_class_type(ASRUtils::extract_type(dummy_variable_type))) {
                        phy_type = ASR::array_physical_typeType::PointerArray;
                    }
                    dummy_variable_type = ASRUtils::TYPE(
                            ASR::make_Array_t(
                                al, dummy_variable_type->base.loc, ASRUtils::extract_type(dummy_variable_type), dims.p, n_dims, phy_type));
                }
                std::string dummy_variable_name = scope->get_unique_name("__libasr_created_dummy_variable_");
                ASR::expr_t* dummy_variable = PassUtils::create_auxiliary_variable(
                    x.m_args[i].loc, dummy_variable_name, al, scope, dummy_variable_type, ASR::intentType::Local, arg_decl, func->m_args[j]);

                std::string pointer_name = scope->get_unique_name("__libasr_created_variable_pointer_");
                pointer_variable_type = ASRUtils::TYPE(ASR::make_Pointer_t(al, pointer_variable_type->base.loc, pointer_variable_type));
                ASR::expr_t* pointer_variable = PassUtils::create_auxiliary_variable(
                    x.m_args[i].loc, pointer_name, al, scope, pointer_variable_type, ASR::intentType::Local, arg_decl, func->m_args[j]);

                ASRUtils::ASRBuilder builder(al, x.base.base.loc);

                std::vector<ASR::stmt_t*> if_body, else_body;
                if_body.push_back(ASRUtils::STMT(ASR::make_Associate_t(al, dummy_variable_type->base.loc, pointer_variable, arg_expr)));
                else_body.push_back(ASRUtils::STMT(ASR::make_Associate_t(al, dummy_variable_type->base.loc, pointer_variable, dummy_variable)));

                pass_result.push_back(al, builder.If(is_allocated, if_body, else_body));

                ASR::call_arg_t replaced_arg;
                replaced_arg.m_value = pointer_variable;
                new_args.p[new_args.n - 1] = replaced_arg;
            }
            present_arg.m_value = is_present;
            new_args.push_back(al, present_arg);
            j++;
        } else {
            new_args.push_back(al, x.m_args[i]);
        }
    }
    LCOMPILERS_ASSERT(func->n_args == new_args.size());
    return true;
}

class ReplaceFunctionCallsWithOptionalArguments: public ASR::BaseExprReplacer<ReplaceFunctionCallsWithOptionalArguments> {

    private:

    Allocator& al;
    std::set<ASR::expr_t*> new_func_calls;
    Vec<ASR::stmt_t*>& pass_result;

    public:

    std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx;
    SymbolTable* current_scope;

    ReplaceFunctionCallsWithOptionalArguments(Allocator& al_,
        std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx_, Vec<ASR::stmt_t*>& pass_result_) :
        al(al_), pass_result(pass_result_), sym2optionalargidx(sym2optionalargidx_), current_scope(nullptr)
    {}

    void replace_FunctionCall(ASR::FunctionCall_t* x) {
        Vec<ASR::call_arg_t> new_args;
        if( !fill_new_args(new_args, al, *x, current_scope, sym2optionalargidx, pass_result) ||
            new_func_calls.find(*current_expr) != new_func_calls.end() ) {
            return ;
        }
        *current_expr = ASRUtils::EXPR(ASRUtils::make_FunctionCall_t_util(al,
                            x->base.base.loc, x->m_name, x->m_original_name,
                            new_args.p, new_args.size(), x->m_type, x->m_value,
                            x->m_dt));
        new_func_calls.insert(*current_expr);
    }

};


class ReplaceFunctionCallsWithOptionalArgumentsVisitor : public ASR::CallReplacerOnExpressionsVisitor<ReplaceFunctionCallsWithOptionalArgumentsVisitor>
{
    private:

        Allocator& al;
        ReplaceFunctionCallsWithOptionalArguments replacer;
        Vec<ASR::stmt_t*> pass_result;
        Vec<ASR::stmt_t*>* parent_body;

    public:

        ReplaceFunctionCallsWithOptionalArgumentsVisitor(Allocator& al_,
            std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx_) :
        al(al_), replacer(al_, sym2optionalargidx_, pass_result), parent_body(nullptr) {
            pass_result.n = 0;
            pass_result.reserve(al_, 0);
        }

        void transform_stmts(ASR::stmt_t **&m_body, size_t &n_body) {
            Vec<ASR::stmt_t*> body;
            body.reserve(al, n_body);
            for (size_t i = 0; i < n_body; i++) {
                body.push_back(al, m_body[i]);
            }
            if( parent_body ) {
                for (size_t j=0; j < pass_result.size(); j++) {
                    parent_body->push_back(al, pass_result[j]);
                }
            }
            // pass_result can contain FunctionCalls which need to be replaced
            // Run the pass till no more statements are added to body
            bool converge = false;
            const int NUM_TRIES = 10;
            for (size_t k = 0; k < NUM_TRIES; k++) {
                Vec<ASR::stmt_t*> new_body;
                new_body.reserve(al, body.n);
                for (size_t i = 0; i < body.n; i++) {
                    pass_result.n = 0;
                    pass_result.reserve(al, 1);
                    Vec<ASR::stmt_t*>* parent_body_copy = parent_body;
                    parent_body = &new_body;
                    visit_stmt(*body[i]);
                    parent_body = parent_body_copy;
                    if( pass_result.size() > 0 ) {
                        for (size_t j=0; j < pass_result.size(); j++) {
                            new_body.push_back(al, pass_result[j]);
                        }
                    }
                    new_body.push_back(al, body[i]);
                }
                if (body.n == new_body.n) {
                    converge = true;
                    break;
                }
                body.n = new_body.n;
                body.p = new_body.p;
            }
            if (!converge) {
                throw LCompilersException("FunctionCall nesting is too high, didn't converge in " + std::to_string(NUM_TRIES) + " tries.");
            }
            m_body = body.p;
            n_body = body.size();
            pass_result.n = 0;
        }

        void call_replacer() {
            replacer.current_expr = current_expr;
            replacer.current_scope = current_scope;
            replacer.replace_expr(*current_expr);
        }

};

class ReplaceSubroutineCallsWithOptionalArgumentsVisitor : public PassUtils::PassVisitor<ReplaceSubroutineCallsWithOptionalArgumentsVisitor>
{

    public:

        std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx;

        ReplaceSubroutineCallsWithOptionalArgumentsVisitor(Allocator& al_,
            std::map<ASR::symbol_t*, std::vector<int32_t>>& sym2optionalargidx_):
        PassVisitor(al_, nullptr), sym2optionalargidx(sym2optionalargidx_)
        {
            pass_result.reserve(al, 1);
        }

        void visit_SubroutineCall(const ASR::SubroutineCall_t& x) {
            Vec<ASR::call_arg_t> new_args;
            if( !fill_new_args(new_args, al, x, current_scope, sym2optionalargidx, pass_result) ) {
                return ;
            }
            pass_result.push_back(al, ASRUtils::STMT(ASRUtils::make_SubroutineCall_t_util(al,
                                    x.base.base.loc, x.m_name, x.m_original_name,
                                    new_args.p, new_args.size(), x.m_dt,
                                    nullptr, false)));
        }
};

void pass_transform_optional_argument_functions(
    Allocator &al, ASR::TranslationUnit_t &unit,
    const LCompilers::PassOptions& /*pass_options*/) {
    std::map<ASR::symbol_t*, std::vector<int32_t>> sym2optionalargidx;
    TransformFunctionsWithOptionalArguments v(al, sym2optionalargidx);
    v.visit_TranslationUnit(unit);
    ReplaceFunctionCallsWithOptionalArgumentsVisitor w(al, sym2optionalargidx);
    w.visit_TranslationUnit(unit);
    ReplaceSubroutineCallsWithOptionalArgumentsVisitor y(al, sym2optionalargidx);
    y.visit_TranslationUnit(unit);
    PassUtils::UpdateDependenciesVisitor x(al);
    x.visit_TranslationUnit(unit);
}


} // namespace LFortran
