/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.atlas.json.io.parser;

import static org.apache.jena.riot.tokens.TokenType.COLON ;
import static org.apache.jena.riot.tokens.TokenType.COMMA ;
import static org.apache.jena.riot.tokens.TokenType.KEYWORD ;
import static org.apache.jena.riot.tokens.TokenType.LBRACE ;
import static org.apache.jena.riot.tokens.TokenType.RBRACE ;
import static org.apache.jena.riot.tokens.TokenType.RBRACKET ;
import org.apache.jena.atlas.json.io.JSONHandler ;

/** Json (extended) parser
 * Extensions:
 *   Use of ', """ and ''' for strings
 *   Bare words for strings in maps (JSON objects)
 *   Hex numbers
 * Objects with duplicate keys are a warning (keeps last)
 */
public class JSONP extends JSONParserBase
{
    private JSONHandler handler ;

    public JSONP(TokenizerJSON tokens, JSONHandler handler)
    {
        super(tokens) ;
        this.handler = handler ;
    }
    
    public void parse()
    {
        if ( ! peekToken().hasType(LBRACE) )
            exception("Not a JSON object START: "+peekToken()) ;
        parseObject() ;
    }
    
    private void parseObject()
    {
        // JSON Object
        nextToken() ;
        handler.startObject(currLine, currCol) ;
        if ( lookingAt(RBRACE) )
        {
            nextToken() ;
            handler.finishObject(currLine, currCol) ;
            return ;
        }
        // ** Read pairs until the cows come home.  Or a } occurs.
        for(;;)
        {
            handler.startPair(currLine, currCol) ;
            if ( ! lookingAt(KEYWORD) && ! lookingAtKeyString() )
                exception("Not a key for a JSON object: "+peekToken()) ;
            String key = peekToken().getImage() ;
            nextToken() ;
            handler.valueString(key, currLine, currCol) ;
            if ( ! lookingAt(COLON) )
                exception("Not a colon: "+peekToken()) ;
            nextToken() ;
            handler.keyPair(currLine, currCol) ;
            
            // One parse.
            parseAny() ;
            handler.finishPair(currLine, currCol) ;
            if ( ! lookingAt(COMMA) )
                break ;
            nextToken() ;
        }
        
        if ( ! lookingAt(RBRACE) )
            exception("Illegal: "+peekToken()) ;
        nextToken() ;
        handler.finishObject(currLine, currCol) ;
    }

    /** Parse one element into the JSONhandler (includes nesting) */
    public void parseAny()
    {
        switch(peekToken().getType())
        {
            case LBRACE:    { parseObject() ; return ; }
            case LBRACKET:  { parseArray() ; return ; }
            
            // Number
            case INTEGER:   { handler.valueInteger(peekToken().getImage(), currLine, currCol) ; nextToken() ; return ; }
            case DECIMAL:   { handler.valueDecimal(peekToken().getImage(), currLine, currCol) ; nextToken() ; return ; }
            case DOUBLE:    { handler.valueDouble(peekToken().getImage(), currLine, currCol) ; nextToken() ; return ; }

            // String - liberal - includes single quotes and triple quoted forms if the tokenizer supports them. 
            case STRING:
            {
                handler.valueString(peekToken().getImage(), currLine, currCol) ;
                nextToken() ; 
                return ;
            }
                
            case KEYWORD:
            { 
                String image = peekToken().getImage() ;
                if ( image.equalsIgnoreCase("true") )    { handler.valueBoolean(true, currLine, currCol) ; nextToken() ; return ; }
                if ( image.equalsIgnoreCase("false") )   { handler.valueBoolean(false, currLine, currCol) ; nextToken() ; return ; }
                if ( image.equalsIgnoreCase("null") )    { handler.valueNull(currLine, currCol) ; nextToken() ; return ; }
                //exception("Unrecognized keyword: "+token()) ;
                // Very liberal
                handler.valueString(image, currLine, currCol) ;
                break ;
            }

            default:
                exception("Unrecognized token: "+peekToken()) ;
        }
    }

    private void parseArray()
    {
        handler.startArray(currLine, currCol) ;
        nextToken() ;
        if ( lookingAt(RBRACKET) )
        {
            nextToken() ;
            handler.finishArray(currLine, currCol) ;
            return ;
        }

        for(;;)
        {
            parseAny() ;
            handler.element(currLine, currCol) ;
            if ( ! lookingAt(COMMA) )
                break ;
            nextToken() ;
        }
        if ( ! lookingAt(RBRACKET) )
            exception("Illegal: "+peekToken()) ;
        nextToken() ;
        handler.finishArray(currLine, currCol) ;
    }
}
