#ifndef INDEX_H_
#define INDEX_H_

#include <string>
#include <set>
#include <map>
#include <iostream>
#include <memory>
#include <utility>
#include <stack>

constexpr char INVALID_TERM_EXCEPTION[] = "Invalid term <{}>"; // term starts with $
constexpr char OPEN_FILE_EXCEPTION[] = "Unable to open input file <{}>";

constexpr char EMPTY_CONDITION_EXCEPTION[] = "Empty condition";
constexpr char INVALID_CONDITION_EXCEPTION[] = "Invalid condition";
constexpr char MISSING_OPERANDS_EXCEPTION[] = "Missing operands";
constexpr char UNKNOWN_OPERATION_EXCEPTION[] = "Unknown operation <{}>";

constexpr char WORD_SEPARATOR = ' ';
constexpr char SPECIAL_CHAR = '$';

constexpr char NOT_OPERATION_NAME[] = "not";
constexpr char AND_OPERATION_NAME[] = "and";
constexpr char OR_OPERATION_NAME[] = "or";

using Identifier = size_t;

class Index;

enum class OperationType {
    UNARY,
    BINARY
};

class Node {
public:
    virtual ~Node() = default;
    virtual std::set<Identifier> evaluate(const Index& index) const = 0;
};

class LeafNode final : public Node {
private:
    std::string term_;
public:
    LeafNode(std::string term) : term_(std::move(term)) {};
    std::set<Identifier> evaluate(const Index& index) const override;
};

class OperationNode : public Node {
public:
    using Node::Node;
};

class BinaryOperationNode : public OperationNode {
protected:
    std::unique_ptr<Node> left_;
    std::unique_ptr<Node> right_;
public:
    BinaryOperationNode(std::unique_ptr<Node> left, std::unique_ptr<Node> right)
        : left_(std::move(left)), right_(std::move(right)) {};
};

class OrOperationNode final : public BinaryOperationNode {
public:
    using BinaryOperationNode::BinaryOperationNode;
    std::set<Identifier> evaluate(const Index& index) const override;
};

class AndOperationNode final : public BinaryOperationNode {
public:
    using BinaryOperationNode::BinaryOperationNode;
    std::set<Identifier> evaluate(const Index& index) const override;
};

class UnaryOperationNode : public OperationNode {
protected:
    std::unique_ptr<Node> child_;
public:
    UnaryOperationNode(std::unique_ptr<Node> child) 
        : child_(std::move(child)) {};
};

class NotOperationNode final : public UnaryOperationNode {
public:
    using UnaryOperationNode::UnaryOperationNode;
    std::set<Identifier> evaluate(const Index& index) const override;
};


class Query {
private:
    std::unique_ptr<Node> root_;
    std::stack<std::unique_ptr<Node>> parsing_stack_; // not the most elegant

    void process_term(std::string term);
    void process_operation(const std::string& operation_token);
    template<typename NodeType>
    void process_unary_operation();
    template<typename NodeType>
    void process_binary_operation();
    void check_for_missing_operands(OperationType operation_type);
public:
    Query(const std::string& condition);
    std::set<Identifier> evaluate(const Index& index) const;
};

template<typename NodeType>
void Query::process_unary_operation() {
    check_for_missing_operands(OperationType::UNARY);

    auto child(std::move(parsing_stack_.top()));
    parsing_stack_.pop();

    NodeType node{std::move(child)};
    std::unique_ptr<Node> node_ptr = std::make_unique<NodeType>(std::move(node));
    parsing_stack_.push(std::move(node_ptr));
}

template<typename NodeType>
void Query::process_binary_operation() {
    check_for_missing_operands(OperationType::BINARY);

    auto left(std::move(parsing_stack_.top()));
    parsing_stack_.pop();
    auto right(std::move(parsing_stack_.top()));
    parsing_stack_.pop();

    NodeType node{std::move(left), std::move(right)};
    std::unique_ptr<Node> node_ptr = std::make_unique<NodeType>(std::move(node));
    parsing_stack_.push(std::move(node_ptr));
}


class Index {
private:
    // map ensures the terms remain sorted
    // set ensures the identifiers remain sorted
    std::map<std::string, std::set<Identifier>> index_map_;
    Identifier next_identifier_ = 0;
public:
    void process(const std::string& file);
    void dump(std::ostream& stream = std::cout) const;
    std::set<Identifier> search(const std::string& term) const;
friend std::set<Identifier> NotOperationNode::evaluate(const Index& index) const;
};

#endif