Source_Code_Location* current_source_code_location = nullptr;

proc apply_arguments_to_function(Lisp_Object* arguments, Function* function) -> Lisp_Object* {
    Environment* new_env = Memory::create_child_environment(function->parent_environment);

    // positional arguments
    for (int i = 0; i < function->positional_arguments->next_index; ++i) {
        if (arguments->type == Lisp_Object_Type::Pair) {
            // TODO(Felix): here we create new lisp_object_symbols from
            // their identifiers but before we converted them to
            // strings from symbols... Wo maybe just use the symbols?
            define_symbol(
                Memory::get_or_create_lisp_object_symbol(function->positional_arguments->identifiers[i]),
                arguments->value.pair->first, new_env);
        } else {

            create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
            return nullptr;
        }
        arguments = arguments->value.pair->rest;
    }

    String_Array_List* read_in_keywords = create_String_array_list();

    if (arguments->type == Lisp_Object_Type::Nil)
        goto checks;
    // keyword arguments: use all given ones and keep track of the
    // added ones (array list), if end of parameters in encountered or
    // something that is not a keyword is encountered or a keyword
    // that is not recognized is encoutered, jump out of the loop.

    while (arguments->value.pair->first->type == Lisp_Object_Type::Keyword) {
        // check if this one is even an accepted keyword
        bool accepted = false;
        for (int i = 0; i < function->keyword_arguments->next_index; ++i) {
            if (string_equal(
                    arguments->value.pair->first->value.keyword->identifier,
                    function->keyword_arguments->identifiers[i]))
            {
                accepted = true;
                break;
            }
        }
        if (!accepted) {
            // TODO(Felix): if we are actually done with all the
            // necessary keywords then we have to count the rest
            // as :rest here, instead od always creating an error
            // (special case with default variables)
            puts("??");
            create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
            return nullptr;
        }

        // check if it was already read in
        for (int i = 0; i < read_in_keywords->next_index; ++i) {
            if (string_equal(
                    arguments->value.pair->first->value.keyword->identifier,
                    read_in_keywords->data[i]))
            {
                // TODO(Felix): if we are actually done with all the
                // necessary keywords then we have to count the rest
                // as :rest here, instead od always creating an error
                // (special case with default variables)
                create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
                return nullptr;
            }
        }

        // okay so we found a keyword that has to be read in and was
        // not already read in, is there a next element to actually
        // set it to?
        if (arguments->value.pair->rest->type != Lisp_Object_Type::Pair) {
            create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
            return nullptr;
        }

        // if not set it and then add it to the array list
        define_symbol(
            Memory::get_or_create_lisp_object_symbol(arguments->value.pair->first->value.keyword->identifier),
            arguments->value.pair->rest->value.pair->first,
            new_env);

        append_to_array_list(read_in_keywords, arguments->value.pair->first->value.keyword->identifier);

        // overstep both for next one
        arguments = arguments->value.pair->rest->value.pair->rest;

        if (arguments->type == Lisp_Object_Type::Nil) {
            break;
        }
    }

  checks:
    // check if all necessary keywords have been read in
    for (int i = 0; i < function->keyword_arguments->next_index; ++i) {
        String* defined_keyword = function->keyword_arguments->identifiers[i];
        bool was_set = false;
        for (int j = 0; j < read_in_keywords->next_index; ++j) {
            if (string_equal(
                    read_in_keywords->data[j],
                    defined_keyword))
            {
                was_set = true;
                break;
            }
        }
        if (function->keyword_arguments->values->data[i] == nullptr) {
            // if this one does not have a default value
            if (!was_set) {
                create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
                return nullptr;
            }
        } else {
            // this one does have a default value, lets see if we have
            // to use it or if the user supplied his own
            if (!was_set) {
                define_symbol(
                    Memory::get_or_create_lisp_object_symbol(defined_keyword),
                    Memory::copy_lisp_object(function->keyword_arguments->values->data[i]), new_env);
            }
        }
    }


    if (arguments->type == Lisp_Object_Type::Nil) {
        if (function->rest_argument) {
            define_symbol(
                Memory::get_or_create_lisp_object_symbol(function->rest_argument),
                Memory::nil, new_env);
        }
    } else {
        if (function->rest_argument) {
            define_symbol(
                Memory::get_or_create_lisp_object_symbol(function->rest_argument),
                arguments, new_env);
        } else {
            // rest was not declared but additional arguments were found
            create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
            return nullptr;
        }
    }


    Lisp_Object* result;
    try {
        result = eval_expr(function->body, new_env);
    }

    return result;

}

/*
   (prog
     (define type--before type)
     (define type
       (lambda (e)
         (if (and (= (type--before e) :pair) (= (first e) :my-type))
             :my-type
           (type--before e))))
   )
*/

/**
   This parses the argument specification of funcitons into their
   Function struct. It dois this by allocating new
   positional_arguments, keyword_arguments and rest_argument and
   filling it in
*/
proc parse_argument_list(Lisp_Object* arguments, Function* function) -> void {
    // first init the fields
    function->positional_arguments = create_positional_argument_list(16);
    function->keyword_arguments    = create_keyword_argument_list(16);
    function->rest_argument        = nullptr;

    // okay let's try to read some positional arguments
    while (arguments->type == Lisp_Object_Type::Pair) {
        if (arguments->value.pair->first->type == Lisp_Object_Type::Keyword) {
            if (string_equal(arguments->value.pair->first->value.keyword->identifier, "keys") ||
                string_equal(arguments->value.pair->first->value.keyword->identifier, "rest"))
                break;
            else {
                create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
                return;
            }
        }

        if (arguments->value.pair->first->type != Lisp_Object_Type::Symbol) {
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
            return;
        }

        // okay wow we found an actual symbol
        append_to_positional_argument_list(
            function->positional_arguments,
            arguments->value.pair->first->value.symbol->identifier);

        arguments = arguments->value.pair->rest;
    }

    // okay we are done with positional arguments, lets check for
    // keywords,
    if (arguments->type != Lisp_Object_Type::Pair) {
        if (arguments->type != Lisp_Object_Type::Nil)
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
        return;
    }

    if (arguments->value.pair->first->type == Lisp_Object_Type::Keyword &&
        string_equal(arguments->value.pair->first->value.keyword->identifier, "keys"))
    {
        arguments = arguments->value.pair->rest;
        if (arguments->type != Lisp_Object_Type::Pair ||
            arguments->value.pair->first->type != Lisp_Object_Type::Symbol)
        {
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
            return;
        }

        while (arguments->type == Lisp_Object_Type::Pair) {
            if (arguments->value.pair->first->type == Lisp_Object_Type::Keyword) {
                if (string_equal(arguments->value.pair->first->value.keyword->identifier, "rest"))
                    break;
                else {
                    create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
                    return;
                }
            }

            if (arguments->value.pair->first->type != Lisp_Object_Type::Symbol) {
                create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
                return;
            }

            // we found a symbol (arguments->value.pair->first) for
            // the keyword args! Let's check if the next arguement is
            // :defaults-to
            Lisp_Object* next = arguments->value.pair->rest;
            if (next->type == Lisp_Object_Type::Pair &&
                next->value.pair->first->type == Lisp_Object_Type::Keyword &&
                string_equal(next->value.pair->first->value.keyword->identifier,
                              "defaults-to"))
            {
                // check if there is a next argument too, otherwise it
                // would be an error
                next = next->value.pair->rest;
                if (next->type == Lisp_Object_Type::Pair) {
                    append_to_keyword_argument_list(function->keyword_arguments,
                                                    arguments->value.pair->first->value.symbol->identifier,
                                                    next->value.pair->first);
                    arguments = next->value.pair->rest;
                } else {
                    create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
                    return;
                }
            } else {
                // No :defaults-to, so just add it to the list
                append_to_keyword_argument_list(function->keyword_arguments,
                                                arguments->value.pair->first->value.symbol->identifier,
                                                nullptr);
                arguments = next;
            }
        }
    }


    // Now we are also done with keyword arguments, lets check for
    // if there is a rest argument
    if (arguments->type != Lisp_Object_Type::Pair) {
        if (arguments->type != Lisp_Object_Type::Nil)
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
        return;
    }

    if (arguments->value.pair->first->type == Lisp_Object_Type::Keyword &&
        string_equal(arguments->value.pair->first->value.keyword->identifier, "rest"))
    {
        arguments = arguments->value.pair->rest;
        if (arguments->type != Lisp_Object_Type::Pair ||
            arguments->value.pair->first->type != Lisp_Object_Type::Symbol)
        {
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
            return;
        }
        function->rest_argument = arguments->value.pair->first->value.symbol->identifier;
        if (arguments->value.pair->rest->type != Lisp_Object_Type::Nil) {
            create_error(Error_Type::Ill_Formed_Lambda_List, arguments->sourceCodeLocation);
        }
    } else {
        printf("this should not happen?");
        create_error(Error_Type::Unknown_Error, arguments->sourceCodeLocation);
    }
}


proc list_length(Lisp_Object* node) -> int {
    if (node->type == Lisp_Object_Type::Nil)
        return 0;

    if (node->type != Lisp_Object_Type::Pair) {
        create_error(Error_Type::Type_Missmatch, node->sourceCodeLocation);
        return 0;
    }

    int len = 0;
    while (node->type == Lisp_Object_Type::Pair) {
        ++len;
        node = node->value.pair->rest;
        if (node->type == Lisp_Object_Type::Nil)
            return len;
    }

    create_error(Error_Type::Ill_Formed_List, node->sourceCodeLocation);
    return 0;
}

proc extract_keyword_value(char* keyword, Parsed_Arguments* args) -> Lisp_Object* {
    // NOTE(Felix): This will be a hashmap lookup later
    for (int i = 0; i < args->keyword_keys->next_index; ++i) {
        if (string_equal(args->keyword_keys->data[i]->value.keyword->identifier, keyword))
            return args->keyword_values->data[i];
    }
    return nullptr;
}

proc eval_arguments(Lisp_Object* arguments, Environment* env, int *out_arguments_length) -> Lisp_Object* {
    int my_out_arguments_length = 0;
    if (arguments->type == Lisp_Object_Type::Nil) {
        *(out_arguments_length) = 0;
        return arguments;
    }

    Lisp_Object* evaluated_arguments = Memory::create_lisp_object_pair(nullptr, nullptr);
    Lisp_Object* evaluated_arguments_head = evaluated_arguments;
    Lisp_Object* current_head = arguments;
    while (current_head->type == Lisp_Object_Type::Pair) {
        try {
            evaluated_arguments_head->value.pair->first =
                eval_expr(current_head->value.pair->first, env);
        }
        evaluated_arguments_head->value.pair->first->sourceCodeLocation = current_head->value.pair->first->sourceCodeLocation;
        current_head = current_head->value.pair->rest;

        if (current_head->type == Lisp_Object_Type::Pair) {
            evaluated_arguments_head->value.pair->rest = Memory::create_lisp_object_pair(nullptr, nullptr);
            evaluated_arguments_head = evaluated_arguments_head->value.pair->rest;
        } else if (current_head->type == Lisp_Object_Type::Nil) {
            evaluated_arguments_head->value.pair->rest = current_head;
        } else {
            create_error(Error_Type::Ill_Formed_Arguments, arguments->sourceCodeLocation);
            return nullptr;
        }
        ++my_out_arguments_length;
    }
    *(out_arguments_length) = my_out_arguments_length;
    return evaluated_arguments;
}

proc eval_expr(Lisp_Object* node, Environment* env) -> Lisp_Object* {
    switch (node->type) {
    case Lisp_Object_Type::T:
    case Lisp_Object_Type::Nil:
    case Lisp_Object_Type::Number:
    case Lisp_Object_Type::Keyword:
    case Lisp_Object_Type::String:
        return node;
    case Lisp_Object_Type::Symbol: {
        Lisp_Object* symbol;
        try {
            symbol = lookup_symbol(node, env);
        }
        return symbol;
    }
    case Lisp_Object_Type::Pair: {
        current_source_code_location = node->sourceCodeLocation;

        Lisp_Object* lispOperator;
        if (node->value.pair->first->type != Lisp_Object_Type::CFunction &&
            node->value.pair->first->type != Lisp_Object_Type::Function)
        {
            try {
                lispOperator = eval_expr(node->value.pair->first, env);
            }
        } else {
                lispOperator = node->value.pair->first;
        }

        Lisp_Object* arguments = node->value.pair->rest;
        int arguments_length;

        // check for c function
        if (lispOperator->type == Lisp_Object_Type::CFunction) {
            Lisp_Object* result = lispOperator->value.cFunction->function(arguments, env);
            return result;
        }

        // check for lisp function
        if (lispOperator->type == Lisp_Object_Type::Function) {
            // only for lambdas we evaluate the arguments before
            // apllying
            if (lispOperator->value.function->type == Function_Type::Lambda) {
                try {
                    arguments = eval_arguments(arguments, env, &arguments_length);
                }
            }

            Lisp_Object* result;
            try {
                result = apply_arguments_to_function(arguments, lispOperator->value.function);
            }
            return result;
        }
    }
    default: {
        create_error(Error_Type::Not_A_Function, node->sourceCodeLocation);
        return nullptr;
    }
    }
}

proc is_truthy (Lisp_Object* expression, Environment* env) -> bool {
    Lisp_Object* result;
    try {
        result = eval_expr(expression, env);
    }
    if (result->type == Lisp_Object_Type::Nil)
        return false;
    return true;
}

proc interprete_file (char* file_name) -> Lisp_Object* {
    Memory::init();
    Environment* env = Memory::create_empty_environment();
    Parser::init(env);

    char* file_content = read_entire_file(file_name);
    if (!file_content) {
        create_error(Error_Type::Unknown_Error, nullptr);
    }

    load_built_ins_into_environment(env);

    try {
        built_in_load(Memory::create_string("pre.slime"), env);
    }

    Lisp_Object_Array_List* program;
    try {
        program = Parser::parse_program(
            Memory::create_string(file_name), file_content);
    }

    Lisp_Object* result = Memory::nil;
    for (int i = 0; i < program->next_index; ++i) {
        try {
            result = eval_expr(program->data[i], env);
        }
    }

    return result;
}

proc interprete_stdin() -> void {
    Memory::init();
    Environment* env = Memory::create_built_ins_environment();
    Parser::init(env);

    printf("Welcome to the lispy interpreter.\n");

    char* line;

    built_in_load(Memory::create_string("pre.slime"), env);

    if (error) {
        log_error();
        delete_error();
    }

    Lisp_Object* parsed, * evaluated;
    while (true) {
        printf(">");
        line = read_expression();
        parsed = Parser::parse_single_expression(line);
        free(line);
        if (error) {
            log_error();
            delete_error();
            continue;
        }
        evaluated = eval_expr(parsed, env);
        if (error) {
            log_error();
            delete_error();
            continue;
        }
        print(evaluated);
        printf("\n");
    }
}
