/*
  Copyright (c) 2000 Caldera Systems

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "parser.h"

#include <assert.h>

#include <qiodevice.h>
#include <qshared.h>

#include <kdebug.h>


NodeValue::NodeValue( const QString &val, bool quoted, const QString &spacing ) :
    m_spacing( spacing ),
    m_quoted( quoted )
{
    m_value = val;
}

NodeValue::NodeValue( int val, bool quoted, const QString &spacing ) :
    m_spacing( spacing ),
    m_quoted( quoted )
{
    m_value = QString::number( val );
}

NodeValue::NodeValue( float val, bool quoted, const QString &spacing ) :
    m_spacing( spacing ),
    m_quoted( quoted )
{
    m_value = QString::number( val );
}

NodeValue::NodeValue( RangeValue val, bool quoted, const QString &spacing ) :
    m_spacing( spacing ),
    m_quoted( quoted )
{
    m_value = QString::number( val.m_from ) + "-" + QString::number( val.m_to );
}


QString NodeValue::stringValue() const
{
    return m_value;
}

int NodeValue::intValue() const
{
    bool ok;
    int value = m_value.toInt( &ok );
    if ( !ok )
        kdDebug() << "Value '" << m_value << "' is not an integer." << endl;
    
    return value;
}

float NodeValue::floatValue() const
{
    bool ok;
    float value = m_value.toFloat( &ok );
    if ( !ok )
        kdDebug() << "Value '" << m_value << "' is not a float." << endl;
    
    return value;
}

/*
RangeValue NodeValue::rangeValue() const
{
    bool ok;
    int value = m_value.toInt( &ok );
    if ( !ok )
        kdDebug() << "Value '" << m_value << "' is not a range." << endl;
    
    return value;
}
*/

Node::Ptr Node::List::namedItem( const QString &name, bool caseSensitive )
{
    ConstIterator it = begin();
    for (; it != end(); ++it )
        if ( ( caseSensitive && name == (*it)->name() ) ||
             ( !caseSensitive && name == (*it)->name().lower() ) )
            return *it;

    return 0;
}

Node::Node()
{
    m_parent = 0;
    m_midSpacing = " "; // default value
}

Node::~Node()
{
    List::Iterator it = m_children.begin();
    List::Iterator end = m_children.end();
    for (; it != end; ++it )
        (*it)->m_parent = 0;
}

void Node::appendChild( const Ptr &child )
{
    Ptr ch = child;

    if ( !ch )
    {
        kdDebug() << "tried appending null child!!!" << endl;
        return;
    }

    if ( ch->m_parent )
        ch->m_parent->removeChild( ch );

    ch->m_parent = this;

    m_children.append( ch );
}

void Node::removeChild( const Ptr &child )
{
    Ptr ch = child;

    List::Iterator it = m_children.find( ch );

    if ( it != m_children.end() )
    {
        ch->m_parent = 0;
        m_children.remove( it );
    }
}

void Node::save( QTextStream &stream ) const
{
    if ( m_parent )
    {
        stream << m_lead << m_name << m_midSpacing;

        NodeValue::List::ConstIterator it = m_values.begin();
        NodeValue::List::ConstIterator end = m_values.end();
        for (; it != end; ++it )
        {
            if ( (*it).quoted() )
                stream << '\"';

            stream << (*it).stringValue();

            if ( (*it).quoted() )
                stream << '\"';

            stream << (*it).spacing();
        }

        stream << m_comment << endl;
    }

    List::ConstIterator it = m_children.begin();
    List::ConstIterator end = m_children.end();
    for (; it != end; ++it )
        (*it)->save( stream );
}

Parser::Parser()
{
    m_dev = 0;
    m_stream = 0;
}

Parser::~Parser()
{
}

Node::Ptr Parser::parse( QIODevice *source, const QStringList &flatNodes )
{
    m_flatNodes = flatNodes;
    m_dev = source;
    m_stream = new QTextStream( m_dev );
    m_root = Node::Ptr( new Node );
    m_current = m_root;

    while ( !m_stream->atEnd() )
    {
        Node::Ptr n = parseLine( m_stream->readLine() );
        if ( n )
            m_current->appendChild( n );
    }

    Node::Ptr res = m_root;

    // free all references
    m_current = 0;
    m_root = 0;
    delete m_stream;
    m_stream = 0;
    m_dev = 0;
    return res;
}

Node::Ptr Parser::parseLine( QString line )
{
    int commentPos = -1;
    int dataStart = -1;
    bool inQuotes = false;
    QString lead;

    unsigned int i = 0;
    for (; i < line.length(); ++i )
    {
        QChar c = line[ i ];

        if ( c == '\"' )
            inQuotes = !inQuotes;

        if ( ( c != ' ' && c != '\t' && c != '\n' ) && dataStart == -1 )
            dataStart = i;

        if ( dataStart == -1 )
            lead += c;

        if ( c == '#' && commentPos == -1 && !inQuotes )
            commentPos = i;

    }

    if ( inQuotes && commentPos == -1 ) // broken line?
    {
        kdDebug() << "broken line. appending \"" << endl;
        line.append( '\"' );
    }

    Node::Ptr n = Node::Ptr( new Node );

    n->setLead( lead );
    n->setComment( line.mid( commentPos ) );
    n->setMidSpacing( QString::null );

    if ( dataStart != -1 &&
         ( ( dataStart < commentPos ) || ( commentPos == -1 ) ) )
    {
        int endPos = commentPos;

        if ( endPos == -1 )
            endPos = line.length();

        QString valueStr = line.mid( dataStart, endPos - dataStart );

        // try to separate keyword from value

        QString key;
        QString midSpacing;
        QString val; // ### find better variable name

        parseValueString( valueStr, key, midSpacing, val );

        n->setName( key );
        n->setMidSpacing( midSpacing );

        key = key.lower();

        NodeValue::List parsedNodeValues = parseValues( val );

        bool isFlatNode = false;

        if ( parsedNodeValues.count() == 1 &&
             m_flatNodes.contains( parsedNodeValues[ 0 ].stringValue().lower() ) )
            isFlatNode = true;

        if ( ( key == "section" ||
               key == "subsection" ||
               key == "mode" ) &&
            !isFlatNode )
        {
            n->setValues( parsedNodeValues );

            Node::Ptr oldCurrent = m_current;
            m_current = n;

            Node::Ptr endSectionElement;

            while ( endSectionElement == 0 &&
                    !m_stream->atEnd() )
            {
                Node::Ptr node = parseLine( m_stream->readLine() );

                if ( node &&
                     ( node->name().lower() == "endsection" ||
                       node->name().lower() == "endsubsection" ||
                       node->name().lower() == "endmode" ) )
                {
                    endSectionElement = node;
                }
                else if ( node )
                    m_current->appendChild( node );
            }

            // append new node (n) to m_current and make the last
            // node the current new node (so that it gets appended
            // to m_current when leaving this method

            m_current = oldCurrent;

            m_current->appendChild( n );

            if ( endSectionElement )
                m_current->appendChild( endSectionElement );

            n = 0;
        }
        else if ( key == "endsection" ||
                  key == "endsubsection" ||
                  key == "endmode" )
        {}
        else
            n->setValues( parsedNodeValues );
    }

    return n;
}

void Parser::parseValueString( QString str, QString &key, QString &midSpacing, QString &val )
{
    unsigned int i = 0;
    int midSpacingStart = -1;
    int valueStart = -1;
    bool inQuotes = false;

    for (; i < str.length(); ++i )
    {
        QChar c = str[ i ];

        if ( c == '\"' )
            inQuotes = !inQuotes;

        if ( ( c == ' ' || c == '\t' ) && !inQuotes )
        {
            if ( midSpacingStart == -1 )
                midSpacingStart = i;
        }
        else if ( midSpacingStart != -1 && valueStart == -1 )
            valueStart = i;
    }

    if ( inQuotes )
        str.append( '"' );

    if ( midSpacingStart == -1 )
        midSpacingStart = str.length();

    key = str.left( midSpacingStart );

    if ( valueStart != -1 && valueStart > midSpacingStart )
    {
        midSpacing = str.mid( midSpacingStart, valueStart - midSpacingStart );
        val = str.mid( valueStart );
    }
}

NodeValue::List Parser::parseValues( QString str )
{
    NodeValue::List res;

    enum ParseState { STATE_PARSING_QUOTED_STRING, STATE_PARSING_SPACING, STATE_PARSING_STRING };

    unsigned int i = 0;
    ParseState state = STATE_PARSING_STRING;
    QString currentSpacing;
    QString currentValue;
    bool quoted = false;

    if ( str[ 0 ] == '\"' )
    {
        state = STATE_PARSING_QUOTED_STRING;
        ++i;
        quoted = true;
    }

    while ( i < str.length() )
    {
        QChar c = str[ i ];

        switch ( state )
        {
            case STATE_PARSING_QUOTED_STRING:
            {
                if ( c == '\"' )
                {
                    state = STATE_PARSING_SPACING;
                    currentSpacing = QString::null;
                }
                else
                    currentValue += c;
            };
            break;
            case STATE_PARSING_STRING:
            {
                if ( c != ' ' && c != '\t' )
                    currentValue += c;
                else
                {
                    state = STATE_PARSING_SPACING;
                    currentSpacing = c;
                }
            };
            break;
            case STATE_PARSING_SPACING:
            {
                if ( c == ' ' || c == '\t' )
                    currentSpacing += c;
                else
                {
                    if ( quoted )
                        res << NodeValue( currentValue, true, currentSpacing );
                    else
                        res << NodeValue( currentValue, false, currentSpacing );

                    currentSpacing = QString::null;
                    currentValue = QString::null;

                    if ( c == '\"' )
                    {
                        state = STATE_PARSING_QUOTED_STRING;
                        quoted = true;
                    }
                    else
                    {
                        state = STATE_PARSING_STRING;
                        quoted = false;
                        currentValue = c;
                    }
                }
            };
            break;
            default: assert( false );
        };

        ++i;
    }

    if ( state == STATE_PARSING_STRING )
    {
        res << NodeValue( currentValue, false, QString::null );
    }
    else if ( state == STATE_PARSING_QUOTED_STRING ) // broken config line, forgot '"'
    {
        res << NodeValue( currentValue, true, QString::null );
    }
    else if ( state == STATE_PARSING_SPACING )
    {
        if ( quoted )
            res << NodeValue( currentValue, true, currentSpacing ); // ### currentSpacing added to fix the case Blah "foo"    # comment here
        else
            res << NodeValue( currentValue, false, currentSpacing );
    }

    // compress "123" "-" "456" to RangeValue
    if ( res.count() == 3 &&
         res[ 1 ].stringValue() == "-" )
    {
        // ### we loose the spacings here :(

        RangeValue r;
        r.m_from = res[ 0 ].floatValue();
        r.m_to = res[ 2 ].floatValue();

        res.clear();
        res << NodeValue( r, false, QString::null );
    }


    return res;
}
