#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#include "NLTemplate.h"

#include <osgEarth/FileUtils>


using namespace std;
using namespace NL::Template;



enum {
    TOKEN_END,
    TOKEN_TEXT,
    TOKEN_BLOCK,
    TOKEN_ENDBLOCK,
    TOKEN_IF,
    TOKEN_ENDIF,
    TOKEN_ELSE,
    TOKEN_INCLUDE,
    TOKEN_SET,
    TOKEN_VAR
};
    


static inline bool alphanum( const char c ) {
    return
    ( c >= 'a' && c <= 'z' ) ||
    ( c >= 'A' && c <= 'Z' ) ||
    ( c >= '0' && c <= '9' ) ||    
    ( c == '_' ) ||
    ( c == '.' ) ||

    // JB:  Added these to enable matching of paths as well
    ( c == ' ' ) ||
    ( c == ':' ) ||
    ( c == '-' ) ||
    ( c == '@' ) ||
    ( c == '/' ) ||
    (c == '!') ||
    ( c == '\\' );
}


static inline long match_var( const char *text, string & result ) {
    if (text[ 0 ] != '{' ||
        text[ 1 ] != '{' ||
        text[ 2 ] != ' ' )
    {
        return -1;
    }
    
    const char *var = text + 3;
    const char *cursor = var;
    
    while ( *cursor ) {
        if (cursor[ 0 ] == ' ' &&
            cursor[ 1 ] == '}' &&
            cursor[ 2 ] == '}' )
        {
            result = string( var, cursor - var );
            return cursor + 3 - text;
        }
        
        if ( !alphanum( *cursor ) ) {
            return -1;
        }
        
        cursor++;
    }
    
    return -1;
}


static long match_tag_with_param( const char *tag, const char *text, string & result ) {
    if (text[ 0 ] != '{' ||
        text[ 1 ] != '%' ||
        text[ 2 ] != ' ')
    {
        return -1;
    }

    long taglen = strlen( tag );
    if ( strncmp( text + 3, tag, taglen ) ) {
        return -1;
    }

    const char *param = text + 3 + taglen;
    
    if ( *param != ' ' ) {
        return -1;
    }
    
    param++;

    const char *cursor = param;

    while ( *cursor ) {
        if (cursor[ 0 ] == ' ' &&
            cursor[ 1 ] == '%' &&
            cursor[ 2 ] == '}' )
        {
            result = string( param, cursor - param );
            return cursor + 3 - text;
        }

        //if ( !alphanum( *cursor )) {
        //    return -1;
        //}
        
        cursor++;
    }
    
    return -1;
}


Tokenizer::Tokenizer( const char *text ) :
text( text ),
len( strlen( text ) ),
pos( 0 ),
peeking( false )
{
}


Tokenizer::~Tokenizer() {
    free( (void*) text );
}


Token Tokenizer::next() {
    static const char * s_endblock = "{% endblock %}";
    static const char * s_block = "block";
    static const char * s_include = "include";
    static const long s_endblock_len = strlen( s_endblock );

    static const char* s_endif = "{% endif %}";
    static const char* s_if = "if";
    static const long s_endif_len = strlen(s_endif);
    static const char* s_else = "{% else %}";
    static const long s_else_len = strlen(s_else);

    static const char* s_set = "set";
    
    if ( peeking ) {
        peeking = false;
        return peek;
    }
    
    Token token;
    token.value.clear();
    peek.value.clear();
    token.type = TOKEN_END;
    peek.type = TOKEN_END;
    
    long textpos = pos;
    long textlen = 0;
    
a:
    if ( pos < len ) {
        long m = match_tag_with_param( s_block, text + pos, peek.value );
        if ( m > 0 ) {
            peek.type = TOKEN_BLOCK;
            pos += m;
        } 
        else if ( !strncmp( s_endblock, text + pos, s_endblock_len ) ) {
            peek.type = TOKEN_ENDBLOCK;
            pos += s_endblock_len;
        }
        else if ((m = match_tag_with_param(s_if, text + pos, peek.value)) > 0) {
            peek.type = TOKEN_IF;
            pos += m;
        }
        else if (!strncmp(s_else, text + pos, s_else_len)) {
            peek.type = TOKEN_ELSE;
            pos += s_else_len;
        }
        else if (!strncmp(s_endif, text + pos, s_endif_len)) {
            peek.type = TOKEN_ENDIF;
            pos += s_endif_len;
        }
        else if ((m = match_tag_with_param( s_include, text + pos, peek.value ) ) > 0 ) {
            peek.type = TOKEN_INCLUDE;
            pos += m;        
        }
        else if ((m = match_tag_with_param(s_set, text + pos, peek.value)) > 0) {
            peek.type = TOKEN_SET;
            pos += m;
        }
        else if ( ( m = match_var( text + pos, peek.value ) ) > 0 ) {
            peek.type = TOKEN_VAR;
            pos += m;
        }
        else {
            textlen ++;
            pos ++;
            peeking = true;
            goto a;
        }
    }

    if ( peeking ) {
        token.type = TOKEN_TEXT;
        token.value = string( text + textpos, textlen );
        return token;
    }

    return peek;
}


const string Dictionary::find( const string & name ) const {
    for ( size_t i=0; i < properties.size(); i++ ) {
        if ( properties[ i ].first == name ) {
            return properties[ i ].second;
        }
    }
    return "";
}


void Dictionary::set( const string & name, const string & value ) {
    for ( size_t i=0; i < properties.size(); i++ ) {
        if ( properties[ i ].first == name ) {
            properties[ i ].second = value;
            return;
        }
    }
    properties.push_back( pair<string, string>( name, value ) );
}


Fragment::~Fragment() {
}


bool Fragment::isBlockNamed( const string & ) const {
    return false;
}



Text::Text( const string & text ) : text( text ) {
}


void Text::render( Output & output, const Dictionary & ) const {
    output.print( text );
}


Fragment *Text::copy() const {
    return new Text( text );
}


Property::Property( const string & name ) : name( name ) {
}


void Property::render( Output & output, const Dictionary & dictionary ) const {
    output.print( dictionary.find( name ) );
}


Fragment *Property::copy() const {
    return new Property( name );
}


Node::~Node() {
    for ( size_t i=0; i < fragments.size(); i++ ) {
        delete fragments[ i ];
    }
}


Fragment *Node::copy() const {
    Node *node = new Node();
    node->properties = properties;
    for ( size_t i=0; i < fragments.size(); i++ ) {
        node->fragments.push_back( fragments[ i ]->copy() );
    }
    return node;
}


void Node::render( Output & output, const Dictionary &dictionary ) const {
    for ( size_t i=0; i < fragments.size(); i++ ) {
        fragments[ i ]->render( output, dictionary);
    }
}



Block & Node::block( const string & name ) const {
    for ( size_t i=0; i < fragments.size(); i++ ) {
        if ( fragments[ i ]->isBlockNamed( name ) ) {
            return *dynamic_cast<Block*>( fragments[ i ] );
        }
    }
    throw 0;
}


Block::Block( const string & name ) : name( name ), enabled( true ), resized( false ) {
}


Fragment *Block::copy() const {
    Block *block = new Block( name );
    block->properties = properties;
    for ( size_t i=0; i < fragments.size(); i++ ) {
        block->fragments.push_back( fragments[ i ]->copy() );
    }
    return block;
}


Block::~Block() {
    for ( size_t i=0; i < nodes.size(); i++ ) {
        delete nodes[ i ];
    }
}


bool Block::isBlockNamed( const string & name ) const {
    return this->name == name;
}


void Block::enable() {
    enabled = true;
}


void Block::disable() {
    enabled = false;
}

void Block::repeat( size_t n ) {
    resized = true;
    for ( size_t i=0; i < nodes.size(); i++ ) {
        delete nodes[ i ];
    }
    nodes.clear();
    for ( size_t i=0; i < n; i++ ) {
        nodes.push_back( static_cast<Node*>( copy() ) );
    }
}


Node & Block::operator[]( size_t index ) {
    return *nodes.at( index );
}


void Block::render( Output & output, const Dictionary & ) const {
    if ( enabled ) {
        if ( resized ) {
            for ( size_t i=0; i < nodes.size(); i++ ) {
                nodes[ i ]->render( output, *nodes[ i ] );
            }
        } else {
            Node::render( output, *this );
        }
    }
}

//
Conditional::Conditional(const string& condition) : condition(condition){
}


Fragment* Conditional::copy() const {
    Conditional* conditional = new Conditional(condition);
    return conditional;
}


Conditional::~Conditional() {
}

void Conditional::render(Output& output, const Dictionary& dictionary) const {
    std::string cond = condition;

    int exclamations = 0;
    size_t index = 0;

    bool negate = false;

    // Handle any number of !'s at the start of the condition.
    while (index < cond.size() && cond[index] == '!') {
        negate = !negate;
        index++;
    }

    cond = cond.substr(index);

    std::string value = dictionary.find(cond);

    if (!negate && (!value.empty() && value != "false") ||
        (negate && (value.empty() || value == "false"))) {
        Node::render(output, dictionary);
    }
}

Output::~Output() {
}


void OutputString::print( const string & text ) {
    buf << text;
}


void OutputStdout::print( const std::string &text ) {
    cout << text;
}


Loader::~Loader() {
}


const std::string& Loader::getReferrer() const
{
    return _referrer;
}

void Loader::setReferrer(const std::string& referrer)
{
    _referrer = referrer; 
}


const char * LoaderFile::load( const char *name ) {

    std::string fullPath = osgEarth::Util::getFullPath(_referrer, std::string(name));

    FILE *f = fopen( fullPath.c_str(), "rb" );
    if (!f) {
        std::cout << "NLTemplate error:  Failed to open " << fullPath << std::endl;
        return 0;
    }
    fseek( f, 0, SEEK_END );
    long len = ftell( f );
    if (len > 0)
    {
        fseek( f, 0, SEEK_SET );
        char *buffer = (char*) malloc( len + 1 );
        fread( (void*) buffer, (size_t)len, 1, f );
        fclose( f );
        buffer[ len ] = 0;
        return buffer;
    }
    else
    {
        fclose(f);
        return 0L;
    }
}


Template::Template( Loader & loader ) : Block( "main" ), loader( loader ) {
}

void Template::set_variable(const char* expr, Node* node)
{
    std::string sexpr(expr);
    auto eqPos = sexpr.find('=');
    if (eqPos != std::string::npos && eqPos > 0 && eqPos < (int)sexpr.length()-1)
    {
        auto varName = sexpr.substr(0, eqPos);
        if (!varName.empty())
        {
            auto varValue = sexpr.substr(eqPos + 1);
            if (!varValue.empty())
            {
                node->set(varName, varValue);
                return;
            }
        }
    }

    OE_WARN << "NLTemplate error: set expression must be in form VAR=VALUE; you said: \"" << sexpr << "\"" << std::endl;
    return;
}

void Template::load_recursive( const char *name, vector<Tokenizer*> & files, vector<Node*> & nodes ) {
        
    const char* loaded = loader.load( name );
    if (!loaded)
    {
        return;
    }

    Tokenizer *tokenizer = new Tokenizer( loaded );
    files.push_back( tokenizer );

    std::string referrer = osgEarth::Util::getFullPath(loader.getReferrer(), std::string(name));
    _referrerStack.push_back( referrer );
    loader.setReferrer( referrer );      

    bool done = false;

    while( !done ) {
        Token token = files.back()->next();
        switch ( token.type ) {
            case TOKEN_END:
                done = true;
                break;
            case TOKEN_BLOCK: {
                Block *block = new Block( token.value );
                nodes.back()->fragments.push_back( block );
                nodes.push_back( block );
            }
                break;
            case TOKEN_ENDBLOCK:
                nodes.pop_back();
                break;
            case TOKEN_IF: {
                Conditional* conditional = new Conditional(token.value);
                nodes.back()->fragments.push_back(conditional);
                nodes.push_back(conditional);
            }
                break;
            case TOKEN_ELSE: {
                Conditional* conditional = dynamic_cast<Conditional*>(nodes.back());
                if (!conditional) {
                    std::cout << "Found {% else %} without matching {% if %}" << std::endl;
                    return;
                }
                // Pop the if
                nodes.pop_back();

                Conditional* elseNode = new Conditional("!" + conditional->condition);
                nodes.back()->fragments.push_back(elseNode);
                nodes.push_back(elseNode);
                }
                break;
            case TOKEN_ENDIF:
                nodes.pop_back();
                break;
            case TOKEN_VAR:
                nodes.back()->fragments.push_back( new Property( token.value ) );
                break;
            case TOKEN_TEXT:
                nodes.back()->fragments.push_back( new Text( token.value ) );
                break;
            case TOKEN_INCLUDE:
                load_recursive( token.value.c_str(), files, nodes );
                break;
            case TOKEN_SET:
                set_variable(token.value.c_str(), nodes.back());
                break;
        }
    }

    _referrerStack.pop_back();
    if (!_referrerStack.empty())
    {
        loader.setReferrer( _referrerStack.back() );
    }
    else
    {
        loader.setReferrer("");
    }
    
    delete files.back();
    files.pop_back();
}


void Template::clear() {
    for ( size_t i=0; i < fragments.size(); i++ ) {
        delete fragments[ i ];
    }
    for ( size_t i=0; i < nodes.size(); i++ ) {
        delete nodes[ i ];
    }
    nodes.clear();
    fragments.clear();
    properties.clear();
}


void Template::load( const char *name ) {
    clear();
    
    vector<Node*> stack;
    stack.push_back( this );
    
    vector<Tokenizer*> file_stack;
   
    load_recursive( name, file_stack, stack );

}


void Template::render( Output & output ) const {
    Node::render( output, *this );
}

