#ifndef INCLUDE_INJA_PARSER_HPP_
#define INCLUDE_INJA_PARSER_HPP_

#include <cstddef>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <stack>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "config.hpp"
#include "exceptions.hpp"
#include "function_storage.hpp"
#include "lexer.hpp"
#include "node.hpp"
#include "template.hpp"
#include "throw.hpp"
#include "token.hpp"

namespace inja {

/*!
 * \brief Class for parsing an inja Template.
 */
class Parser {
  using Arguments = std::vector<std::shared_ptr<ExpressionNode>>;
  using OperatorStack = std::stack<std::shared_ptr<FunctionNode>>;

  const ParserConfig& config;

  Lexer lexer;
  TemplateStorage& template_storage;
  const FunctionStorage& function_storage;

  Token tok, peek_tok;
  bool have_peek_tok {false};

  std::string_view literal_start;

  BlockNode* current_block {nullptr};
  ExpressionListNode* current_expression_list {nullptr};

  std::stack<IfStatementNode*> if_statement_stack;
  std::stack<ForStatementNode*> for_statement_stack;
  std::stack<BlockStatementNode*> block_statement_stack;

  void throw_parser_error(const std::string& message) const {
    INJA_THROW(ParserError(message, lexer.current_position()));
  }

  void get_next_token() {
    if (have_peek_tok) {
      tok = peek_tok;
      have_peek_tok = false;
    } else {
      tok = lexer.scan();
    }
  }

  void get_peek_token() {
    if (!have_peek_tok) {
      peek_tok = lexer.scan();
      have_peek_tok = true;
    }
  }

  void add_literal(Arguments &arguments, const char* content_ptr) {
    const std::string_view data_text(literal_start.data(), tok.text.data() - literal_start.data() + tok.text.size());
    arguments.emplace_back(std::make_shared<LiteralNode>(data_text, data_text.data() - content_ptr));
  }

  void add_operator(Arguments &arguments, OperatorStack &operator_stack) {
    auto function = operator_stack.top();
    operator_stack.pop();

    if (static_cast<int>(arguments.size()) < function->number_args) {
      throw_parser_error("too few arguments");
    }

    for (int i = 0; i < function->number_args; ++i) {
      function->arguments.insert(function->arguments.begin(), arguments.back());
      arguments.pop_back();
    }
    arguments.emplace_back(function);
  }

  void add_to_template_storage(const std::filesystem::path& path, std::string& template_name) {
    if (template_storage.find(template_name) != template_storage.end()) {
      return;
    }

    const std::string original_name = template_name;

    if (config.search_included_templates_in_files) {
      // Build the relative path
      template_name = (path / original_name).string();
      if (template_name.compare(0, 2, "./") == 0) {
        template_name.erase(0, 2);
      }

      if (template_storage.find(template_name) == template_storage.end()) {
        // Load file
        std::ifstream file;
        file.open(template_name);
        if (!file.fail()) {
          const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());

          auto include_template = Template(text);
          template_storage.emplace(template_name, include_template);
          parse_into_template(template_storage[template_name], template_name);
          return;
        } else if (!config.include_callback) {
          INJA_THROW(FileError("failed accessing file at '" + template_name + "'"));
        }
      }
    }

    // Try include callback
    if (config.include_callback) {
      auto include_template = config.include_callback(path, original_name);
      template_storage.emplace(template_name, include_template);
    }
  }

  std::string parse_filename() const {
    if (tok.kind != Token::Kind::String) {
      throw_parser_error("expected string, got '" + tok.describe() + "'");
    }

    if (tok.text.length() < 2) {
      throw_parser_error("expected filename, got '" + static_cast<std::string>(tok.text) + "'");
    }

    // Remove first and last character ""
    return std::string {tok.text.substr(1, tok.text.length() - 2)};
  }

  bool parse_expression(Template& tmpl, Token::Kind closing) {
    current_expression_list->root = parse_expression(tmpl);
    return tok.kind == closing;
  }

  std::shared_ptr<ExpressionNode> parse_expression(Template& tmpl) {
    size_t current_bracket_level {0};
    size_t current_brace_level {0};
    Arguments arguments;
    OperatorStack operator_stack;

    while (tok.kind != Token::Kind::Eof) {
      // Literals
      switch (tok.kind) {
      case Token::Kind::String: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          literal_start = tok.text;
          add_literal(arguments, tmpl.content.c_str());
        }
      } break;
      case Token::Kind::Number: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          literal_start = tok.text;
          add_literal(arguments, tmpl.content.c_str());
        }
      } break;
      case Token::Kind::LeftBracket: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          literal_start = tok.text;
        }
        current_bracket_level += 1;
      } break;
      case Token::Kind::LeftBrace: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          literal_start = tok.text;
        }
        current_brace_level += 1;
      } break;
      case Token::Kind::RightBracket: {
        if (current_bracket_level == 0) {
          throw_parser_error("unexpected ']'");
        }

        current_bracket_level -= 1;
        if (current_brace_level == 0 && current_bracket_level == 0) {
          add_literal(arguments, tmpl.content.c_str());
        }
      } break;
      case Token::Kind::RightBrace: {
        if (current_brace_level == 0) {
          throw_parser_error("unexpected '}'");
        }

        current_brace_level -= 1;
        if (current_brace_level == 0 && current_bracket_level == 0) {
          add_literal(arguments, tmpl.content.c_str());
        }
      } break;
      case Token::Kind::Id: {
        get_peek_token();

        // Data Literal
        if (tok.text == static_cast<decltype(tok.text)>("true") || tok.text == static_cast<decltype(tok.text)>("false") ||
            tok.text == static_cast<decltype(tok.text)>("null")) {
          if (current_brace_level == 0 && current_bracket_level == 0) {
            literal_start = tok.text;
            add_literal(arguments, tmpl.content.c_str());
          }

          // Operator
        } else if (tok.text == "and" || tok.text == "or" || tok.text == "in" || tok.text == "not") {
          goto parse_operator;

          // Functions
        } else if (peek_tok.kind == Token::Kind::LeftParen) {
          auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
          get_next_token();
          do {
            get_next_token();
            auto expr = parse_expression(tmpl);
            if (!expr) {
              break;
            }
            func->number_args += 1;
            func->arguments.emplace_back(expr);
          } while (tok.kind == Token::Kind::Comma);
          if (tok.kind != Token::Kind::RightParen) {
            throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
          }

          auto function_data = function_storage.find_function(func->name, func->number_args);
          if (function_data.operation == FunctionStorage::Operation::None) {
            throw_parser_error("unknown function " + func->name);
          }
          func->operation = function_data.operation;
          if (function_data.operation == FunctionStorage::Operation::Callback) {
            func->callback = function_data.callback;
          }
          arguments.emplace_back(func);

          // Variables
        } else {
          arguments.emplace_back(std::make_shared<DataNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
        }

        // Operators
      } break;
      case Token::Kind::Equal:
      case Token::Kind::NotEqual:
      case Token::Kind::GreaterThan:
      case Token::Kind::GreaterEqual:
      case Token::Kind::LessThan:
      case Token::Kind::LessEqual:
      case Token::Kind::Plus:
      case Token::Kind::Minus:
      case Token::Kind::Times:
      case Token::Kind::Slash:
      case Token::Kind::Power:
      case Token::Kind::Percent:
      case Token::Kind::Dot: {

      parse_operator:
        FunctionStorage::Operation operation;
        switch (tok.kind) {
        case Token::Kind::Id: {
          if (tok.text == "and") {
            operation = FunctionStorage::Operation::And;
          } else if (tok.text == "or") {
            operation = FunctionStorage::Operation::Or;
          } else if (tok.text == "in") {
            operation = FunctionStorage::Operation::In;
          } else if (tok.text == "not") {
            operation = FunctionStorage::Operation::Not;
          } else {
            throw_parser_error("unknown operator in parser.");
          }
        } break;
        case Token::Kind::Equal: {
          operation = FunctionStorage::Operation::Equal;
        } break;
        case Token::Kind::NotEqual: {
          operation = FunctionStorage::Operation::NotEqual;
        } break;
        case Token::Kind::GreaterThan: {
          operation = FunctionStorage::Operation::Greater;
        } break;
        case Token::Kind::GreaterEqual: {
          operation = FunctionStorage::Operation::GreaterEqual;
        } break;
        case Token::Kind::LessThan: {
          operation = FunctionStorage::Operation::Less;
        } break;
        case Token::Kind::LessEqual: {
          operation = FunctionStorage::Operation::LessEqual;
        } break;
        case Token::Kind::Plus: {
          operation = FunctionStorage::Operation::Add;
        } break;
        case Token::Kind::Minus: {
          operation = FunctionStorage::Operation::Subtract;
        } break;
        case Token::Kind::Times: {
          operation = FunctionStorage::Operation::Multiplication;
        } break;
        case Token::Kind::Slash: {
          operation = FunctionStorage::Operation::Division;
        } break;
        case Token::Kind::Power: {
          operation = FunctionStorage::Operation::Power;
        } break;
        case Token::Kind::Percent: {
          operation = FunctionStorage::Operation::Modulo;
        } break;
        case Token::Kind::Dot: {
          operation = FunctionStorage::Operation::AtId;
        } break;
        default: {
          throw_parser_error("unknown operator in parser.");
        }
        }
        auto function_node = std::make_shared<FunctionNode>(operation, tok.text.data() - tmpl.content.c_str());

        while (!operator_stack.empty() &&
               ((operator_stack.top()->precedence > function_node->precedence) ||
                (operator_stack.top()->precedence == function_node->precedence && function_node->associativity == FunctionNode::Associativity::Left))) {
          add_operator(arguments, operator_stack);
        }

        operator_stack.emplace(function_node);
      } break;
      case Token::Kind::Comma: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          goto break_loop;
        }
      } break;
      case Token::Kind::Colon: {
        if (current_brace_level == 0 && current_bracket_level == 0) {
          throw_parser_error("unexpected ':'");
        }
      } break;
      case Token::Kind::LeftParen: {
        get_next_token();
        auto expr = parse_expression(tmpl);
        if (tok.kind != Token::Kind::RightParen) {
            throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
        }
        if (!expr) {
          throw_parser_error("empty expression in parentheses");
        }
        arguments.emplace_back(expr);
      } break;

      // parse function call pipe syntax
      case Token::Kind::Pipe: {
        // get function name
        get_next_token();
        if (tok.kind != Token::Kind::Id) {
          throw_parser_error("expected function name, got '" + tok.describe() + "'");
        }
        auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
        // add first parameter as last value from arguments
        func->number_args += 1;
        func->arguments.emplace_back(arguments.back());
        arguments.pop_back();
        get_peek_token();
        if (peek_tok.kind == Token::Kind::LeftParen) {
          get_next_token();
          // parse additional parameters
          do {
            get_next_token();
            auto expr = parse_expression(tmpl);
            if (!expr) {
              break;
            }
            func->number_args += 1;
            func->arguments.emplace_back(expr);
          } while (tok.kind == Token::Kind::Comma);
          if (tok.kind != Token::Kind::RightParen) {
            throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
          }
        }
        // search store for defined function with such name and number of args
        auto function_data = function_storage.find_function(func->name, func->number_args);
        if (function_data.operation == FunctionStorage::Operation::None) {
          throw_parser_error("unknown function " + func->name);
        }
        func->operation = function_data.operation;
        if (function_data.operation == FunctionStorage::Operation::Callback) {
          func->callback = function_data.callback;
        }
        arguments.emplace_back(func);
      } break;
      default:
        goto break_loop;
      }

      get_next_token();
    }

  break_loop:
    while (!operator_stack.empty()) {
      add_operator(arguments, operator_stack);
    }

    std::shared_ptr<ExpressionNode> expr;
    if (arguments.size() == 1) {
      expr = arguments[0];
      arguments = {};
    } else if (arguments.size() > 1) {
      throw_parser_error("malformed expression");
    }
    return expr;
  }

  bool parse_statement(Template& tmpl, Token::Kind closing, const std::filesystem::path& path) {
    if (tok.kind != Token::Kind::Id) {
      return false;
    }

    if (tok.text == static_cast<decltype(tok.text)>("if")) {
      get_next_token();

      auto if_statement_node = std::make_shared<IfStatementNode>(current_block, tok.text.data() - tmpl.content.c_str());
      current_block->nodes.emplace_back(if_statement_node);
      if_statement_stack.emplace(if_statement_node.get());
      current_block = &if_statement_node->true_statement;
      current_expression_list = &if_statement_node->condition;

      if (!parse_expression(tmpl, closing)) {
        return false;
      }
    } else if (tok.text == static_cast<decltype(tok.text)>("else")) {
      if (if_statement_stack.empty()) {
        throw_parser_error("else without matching if");
      }
      auto& if_statement_data = if_statement_stack.top();
      get_next_token();

      if_statement_data->has_false_statement = true;
      current_block = &if_statement_data->false_statement;

      // Chained else if
      if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("if")) {
        get_next_token();

        auto if_statement_node = std::make_shared<IfStatementNode>(true, current_block, tok.text.data() - tmpl.content.c_str());
        current_block->nodes.emplace_back(if_statement_node);
        if_statement_stack.emplace(if_statement_node.get());
        current_block = &if_statement_node->true_statement;
        current_expression_list = &if_statement_node->condition;

        if (!parse_expression(tmpl, closing)) {
          return false;
        }
      }
    } else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
      if (if_statement_stack.empty()) {
        throw_parser_error("endif without matching if");
      }

      // Nested if statements
      while (if_statement_stack.top()->is_nested) {
        if_statement_stack.pop();
      }

      auto& if_statement_data = if_statement_stack.top();
      get_next_token();

      current_block = if_statement_data->parent;
      if_statement_stack.pop();
    } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
      get_next_token();

      if (tok.kind != Token::Kind::Id) {
        throw_parser_error("expected block name, got '" + tok.describe() + "'");
      }

      const std::string block_name = static_cast<std::string>(tok.text);

      auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
      current_block->nodes.emplace_back(block_statement_node);
      block_statement_stack.emplace(block_statement_node.get());
      current_block = &block_statement_node->block;
      auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
      if (!success.second) {
        throw_parser_error("block with the name '" + block_name + "' does already exist");
      }

      get_next_token();
    } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
      if (block_statement_stack.empty()) {
        throw_parser_error("endblock without matching block");
      }

      auto& block_statement_data = block_statement_stack.top();
      get_next_token();

      current_block = block_statement_data->parent;
      block_statement_stack.pop();
    } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
      get_next_token();

      // options: for a in arr; for a, b in obj
      if (tok.kind != Token::Kind::Id) {
        throw_parser_error("expected id, got '" + tok.describe() + "'");
      }

      Token value_token = tok;
      get_next_token();

      // Object type
      std::shared_ptr<ForStatementNode> for_statement_node;
      if (tok.kind == Token::Kind::Comma) {
        get_next_token();
        if (tok.kind != Token::Kind::Id) {
          throw_parser_error("expected id, got '" + tok.describe() + "'");
        }

        const Token key_token = value_token;
        value_token = tok;
        get_next_token();

        for_statement_node = std::make_shared<ForObjectStatementNode>(static_cast<std::string>(key_token.text), static_cast<std::string>(value_token.text),
                                                                      current_block, tok.text.data() - tmpl.content.c_str());

        // Array type
      } else {
        for_statement_node =
            std::make_shared<ForArrayStatementNode>(static_cast<std::string>(value_token.text), current_block, tok.text.data() - tmpl.content.c_str());
      }

      current_block->nodes.emplace_back(for_statement_node);
      for_statement_stack.emplace(for_statement_node.get());
      current_block = &for_statement_node->body;
      current_expression_list = &for_statement_node->condition;

      if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("in")) {
        throw_parser_error("expected 'in', got '" + tok.describe() + "'");
      }
      get_next_token();

      if (!parse_expression(tmpl, closing)) {
        return false;
      }
    } else if (tok.text == static_cast<decltype(tok.text)>("endfor")) {
      if (for_statement_stack.empty()) {
        throw_parser_error("endfor without matching for");
      }

      auto& for_statement_data = for_statement_stack.top();
      get_next_token();

      current_block = for_statement_data->parent;
      for_statement_stack.pop();
    } else if (tok.text == static_cast<decltype(tok.text)>("include")) {
      get_next_token();

      std::string template_name = parse_filename();
      add_to_template_storage(path, template_name);

      current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));

      get_next_token();
    } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
      get_next_token();

      std::string template_name = parse_filename();
      add_to_template_storage(path, template_name);

      current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));

      get_next_token();
    } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
      get_next_token();

      if (tok.kind != Token::Kind::Id) {
        throw_parser_error("expected variable name, got '" + tok.describe() + "'");
      }

      const std::string key = static_cast<std::string>(tok.text);
      get_next_token();

      auto set_statement_node = std::make_shared<SetStatementNode>(key, tok.text.data() - tmpl.content.c_str());
      current_block->nodes.emplace_back(set_statement_node);
      current_expression_list = &set_statement_node->expression;

      if (tok.text != static_cast<decltype(tok.text)>("=")) {
        throw_parser_error("expected '=', got '" + tok.describe() + "'");
      }
      get_next_token();

      if (!parse_expression(tmpl, closing)) {
        return false;
      }
    } else {
      return false;
    }
    return true;
  }

  void parse_into(Template& tmpl, const std::filesystem::path& path) {
    lexer.start(tmpl.content);
    current_block = &tmpl.root;

    for (;;) {
      get_next_token();
      switch (tok.kind) {
      case Token::Kind::Eof: {
        if (!if_statement_stack.empty()) {
          throw_parser_error("unmatched if");
        }
        if (!for_statement_stack.empty()) {
          throw_parser_error("unmatched for");
        }
      }
        current_block = nullptr;
        return;
      case Token::Kind::Text: {
        current_block->nodes.emplace_back(std::make_shared<TextNode>(tok.text.data() - tmpl.content.c_str(), tok.text.size()));
      } break;
      case Token::Kind::StatementOpen: {
        get_next_token();
        if (!parse_statement(tmpl, Token::Kind::StatementClose, path)) {
          throw_parser_error("expected statement, got '" + tok.describe() + "'");
        }
        if (tok.kind != Token::Kind::StatementClose) {
          throw_parser_error("expected statement close, got '" + tok.describe() + "'");
        }
      } break;
      case Token::Kind::LineStatementOpen: {
        get_next_token();
        if (!parse_statement(tmpl, Token::Kind::LineStatementClose, path)) {
          throw_parser_error("expected statement, got '" + tok.describe() + "'");
        }
        if (tok.kind != Token::Kind::LineStatementClose && tok.kind != Token::Kind::Eof) {
          throw_parser_error("expected line statement close, got '" + tok.describe() + "'");
        }
      } break;
      case Token::Kind::ExpressionOpen: {
        get_next_token();

        auto expression_list_node = std::make_shared<ExpressionListNode>(tok.text.data() - tmpl.content.c_str());
        current_block->nodes.emplace_back(expression_list_node);
        current_expression_list = expression_list_node.get();

        if (!parse_expression(tmpl, Token::Kind::ExpressionClose)) {
          throw_parser_error("expected expression close, got '" + tok.describe() + "'");
        }
      } break;
      case Token::Kind::CommentOpen: {
        get_next_token();
        if (tok.kind != Token::Kind::CommentClose) {
          throw_parser_error("expected comment close, got '" + tok.describe() + "'");
        }
      } break;
      default: {
        throw_parser_error("unexpected token '" + tok.describe() + "'");
      } break;
      }
    }
    current_block = nullptr;
  }

public:
  explicit Parser(const ParserConfig& parser_config, const LexerConfig& lexer_config, TemplateStorage& template_storage,
                  const FunctionStorage& function_storage)
      : config(parser_config), lexer(lexer_config), template_storage(template_storage), function_storage(function_storage) {}

  Template parse(std::string_view input, const std::filesystem::path& path) {
    auto result = Template(std::string(input));
    parse_into(result, path);
    return result;
  }

  void parse_into_template(Template& tmpl, const std::filesystem::path& filename) {
    auto sub_parser = Parser(config, lexer.get_config(), template_storage, function_storage);
    sub_parser.parse_into(tmpl, filename.parent_path());
  }

  static std::string load_file(const std::filesystem::path& filename) {
    std::ifstream file;
    file.open(filename);
    if (file.fail()) {
      INJA_THROW(FileError("failed accessing file at '" + filename.string() + "'"));
    }
    std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    return text;
  }
};

} // namespace inja

#endif // INCLUDE_INJA_PARSER_HPP_
