#pragma once

#include <string>
#include <vector>
#include <memory>
#include <type_traits>
#include "Lib/Utils.hh"
#include "Lib/Script/FrontEnd/Token.hh"
#include "Lib/Script/FrontEnd/Tokens.hh"
#include "Lib/Script/FrontEnd/Location.hh"

namespace Vivy::Script
{

/* Structural And Declarative Objects */
class IrRoot;      // A root, with all the needed modules, etc
class IrModule;    // A module (single vvs file)
class IrAttribute; // Attribute placed on the module, function, etc
class IrImport;    // An import statement
class IrOption;    // An option to pass to jobs (no global vars)

/* Types */
class IrType;
class IrTAss;       // Ass types, SYLLABE, LINE, etc
class IrTOption;    // An option type, need to have the same shape
class IrTPrimitive; // Primitive types

/* Jobs */
class IrJob;
class IrImportedJob; // An imported job

/* Functions */
class IrFunction;
class IrImportedFunction; // A utility to hide the imported functions

/* Variables */
class IrVariable;
class IrVPrimitive; // A simple base variable
class IrVTable;     // A variable with fields (a struct)

/* Instructions */
class IrInstruction;
class IrILoop;        // Any loop
class IrIAffectation; // Variable affectation
class IrIReturn;      // Return statement
class IrIDeclaration; // Variable declaration
class IrIConditional; // If/Else/Elif statements

/* Expression */
class IrExpression;
class IrEConstExpr;    // A constant expression, known at compile time
class IrECall;         // A function call
class IrECompOp;       // A comparaison (!=, <=, ...)
class IrEArithmeticOp; // Simple operations (+, *, ...)
class IrELogicOp;      // A logic operation (and, or, ...)
class IrEVariableRef;  // Reference to a variable, local or argument

/* A signature for a callable element */
class IrCallableSignature;

/* Base class forward declaration needed for concepts */
class IrElement;

/*
** Base concept for an IR element class.
*/

template <typename T>
concept IrElementObject = std::is_base_of<IrElement, T>::value;

template <typename... T>
concept IrElementObjects = std::conjunction<std::is_base_of<IrElement, T>...>::value;

/*
** IrElementConstructible object. Used to create an element from
** the passed arguments (`typename... Args`).
*/

template <typename T, typename... Args>
concept IrElementConstructible = IrElementObject<T> && requires(T irElem, Args &&...args)
{
    T(std::forward<Args>(args)...);
};

/*
** IrElementDefaultConstructible object. Used to create an element
** from nothing.
*/

template <typename T>
concept IrElementDefaultConstructible =
    std::is_base_of<IrElement, T>::value && has_any_default_constructor<T>::value;

/*
** IrElementParsable object. Used for the create with the pointer
** to the tokens passed. Also removes the
** `virtual void parse(std::vector<Token>*)` abstract method.
*/

template <typename T>
concept IrElementParsable = std::is_base_of<IrElement, T>::value && requires(T irElem)
{
    irElem.parse(new std::vector<Token>);
};

/* Base class definition: IrElement */
class IrElement {
    IrElement *parentElement       = nullptr;
    IrAttribute *elementAttributes = nullptr;
    std::vector<IrElement *> childElements;

protected:
    IrElement() noexcept = default;
    IrElement(IrElement *p) noexcept;

    void detachElementFromParent() noexcept;
    void addChild(IrElement *) noexcept;

    void throwUnexpectedToken(const Token &tok) const;

public:
    virtual ~IrElement() noexcept;

    virtual const IrElement *parent() const noexcept;
    virtual IrElement *parent() noexcept;

    void setParent(IrElement *p) noexcept;
    void setAttribute(IrAttribute *a) noexcept;

    virtual std::string toString() const noexcept = 0;

    /*
    ** Get attributes for an element!
    */

    std::vector<StrV> attribute(const std::string &name) const noexcept;
    std::vector<StrV> attribute(StrV name) const noexcept;

    /*
    ** Create an IrElement by parsing a token stream representing the file.
    ** May throw an exception because parse is not noexcept. Always return a
    ** non null pointer, will throw an exception on failure.
    **
    ** TODO: Use concepts IrElementParsable and IrElementConstructible.
    */

    template <typename T>
    requires IrElementParsable<T> && IrElementDefaultConstructible<T>
    static T *create(IrElement *p, std::vector<Token> *tokens)
    {
        using DeleteCall = decltype(&IrElement::destroy<T>);
        std::unique_ptr<T, DeleteCall> ret(new T, IrElement::destroy<T>);
        ret->setParent(p != nullptr ? p : ret.get());
        ret->parse(tokens);
        return ret.release();
    }

    template <typename T, typename... Args>
    requires IrElementConstructible<T, Args...> && IrElementParsable<T>
    static T *create(IrElement *p, std::vector<Token> *tokens, Args &&...args)
    {
        using DeleteCall = decltype(&IrElement::destroy<T>);
        std::unique_ptr<T, DeleteCall> ret(new T(std::forward<Args>(args)...),
                                           IrElement::destroy<T>);
        ret->setParent(p != nullptr ? p : ret.get());
        ret->parse(tokens);
        return ret.release();
    }

    template <typename T>
    requires IrElementParsable<T>
    static T *create(IrElement *p)
    {
        using DeleteCall = decltype(&IrElement::destroy<T>);
        std::unique_ptr<T, DeleteCall> ret(new T, IrElement::destroy<T>);
        ret->setParent(p != nullptr ? p : ret.get());
        return ret.release();
    }

    template <typename T, typename... Args>
    requires IrElementConstructible<T, Args...>
    static T *create(IrElement *p, Args &&...args)
    {
        using DeleteCall = decltype(&IrElement::destroy<T>);
        std::unique_ptr<T, DeleteCall> ret(new T(std::forward<Args>(args)...),
                                           IrElement::destroy<T>);
        ret->setParent(p != nullptr ? p : ret.get());
        return ret.release();
    }

    /* A safer call to delete, to also remove the children from its parent */
    template <typename T>
    requires IrElementObject<T>
    static void destroy(T *const e) noexcept
    {
        if (e != nullptr) {
            e->detachElementFromParent();
            delete e;
        }
    }
};

/*
** Define an IrElement sub-class. Sets the correct visibility for the
** constructors and add IrElement as a friend for the create static
** method.
*/

#define VIVY_IR_ELEMENT(clazz)                                \
protected:                                                    \
    friend class IrElement;                                   \
    template <typename... T> friend struct HasAnyConstructor; \
                                                              \
private:

}