#include "builtins/builtins.h"
#include "precedence.h"
#include "builtins/helpers.h"

using namespace empathy;
using namespace empathy::ir;
using namespace empathy::parse;
using namespace empathy::builtins;

// Dollar is a low level operator: it doesn't expect a value rhs operand,
// but just an identifier ir instruction.
// So we can't use the convenient operator wrapper rule.
//
// Also, it works both as a prefix operator to construct a TVar,
// and as an infix operator (with a decl or TEXpr as the left value) to
// construct a Decl of the form "<type> $FOO" or a TDecl of the form
// "<TExpr> $FOO" (for instance $T $FOO)
namespace
{
    optional< Value > BuildOrRetrieveTVar( const Context& c, const StringId& name )
    {
        // Template name bindings are stored with a "$$" prefix so they don't
        // collide with regular names.
        auto captureIdentity = AppendToVectorTerm( c.identity(),
            TERM( "$$"_sid ), TERM( name ) );

        // Look up the TVar name. If it exists, then we're inside of a template function
        // body, refering to a bound TVar, so we just return the stored value.
        // Otherwise, we're in a template declaration and have to construct a new TVar.
        Term result;

        switch( c.env()->retrieveValue( captureIdentity, c.identity(), result ) )
        {
            case sema::Env::Status::Success:
                return ValueFromIRExpr( result );

            case sema::Env::Status::AmbiguousMatch:
                cout << "unexpected ambiguous match when resolving '$" << name << "'.\n";
                return nullopt;
        }

        // No bound value was found: construct a TVar.
        return ToValue( TVar( name ) );
    }

    bool ParsePrefixDollarOperator( Parser& p, const Term& t, uint32_t prec )
    {
        auto nameTerm = p.resolver()->consumeUnresolved();
        if( !nameTerm )
        {
            cout << "expected an identifier after '$'.\n";
            return false;
        }

        const auto* name = get_if< StringId >( &nameTerm->first );
        if( !name )
        {
            cout << "expected an identifier after '$'.\n";
            return false;
        }

        const auto& c = p.resolver()->context();
        if( auto val = BuildOrRetrieveTVar( c, *name ) )
        {
            p.pushValue( move( *val ) );
            return true;
        }

        return false;
    }

    optional< uint32_t > InfixDollarPrecedence( const Parser& p, const Term& t )
    {
        const auto& leftVal = p.peekLastValue();
        if( !leftVal )
            return nullopt;

        auto nameTerm = p.resolver()->lookAheadUnresolved();
        if( !nameTerm )
            return nullopt;

        if( !holds_alternative< StringId >( nameTerm->first ) )
            return nullopt;

        if( !leftVal->isType() && !builtins::IsTExpr( *leftVal ) )
            return nullopt;

        // The infix dollar operator is a syntactic shortcut meant to behave like we are applying
        // a TExpr to a type or another TExpr (just like a regular decl), so it have the application precedence.
        return precedence::Application;
    }

    bool ParseInfixDollarOperator( Parser& p, const Term& t, uint32_t prec )
    {
        const auto& leftVal = p.peekLastValue();
        if( !leftVal )
            return false;

        if( !leftVal->isType() && !IsTExpr( *leftVal )  )
            return false;

        auto nameTerm = p.resolver()->consumeUnresolved();
        const auto* name = get_if< StringId >( &nameTerm->first );
        const auto& c = p.resolver()->context();

        auto val = BuildOrRetrieveTVar( c, *name );
        if( !val )
            return false;

        // If the TVar was already bound, we may end up with something in val
        // that isn't a TVar, which is an error.
        if( !IsTVar( *val ) )
        {
            cout << "expected an unbound identifier after '$'.\n";
            return false;
        }

        auto typeOrTExpr = ValueToIRExpr( *p.popValue() );
        auto tdecl = builtins::BuildTDecl( c, move( typeOrTExpr ), *name );
        assert( tdecl );
        p.pushValue( ToValue( move( *tdecl ) ) );
        return true;
    }
}

namespace empathy::builtins
{
    void SetupDollarOp( Env& e )
    {
        Rule r( ParsePrefixDollarOperator, InfixDollarPrecedence, ParseInfixDollarOperator );
        RegisterRule( e, "$"_sid, move( r ) );
    }
}
