/*
 * 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.riot.lang;

import org.apache.jena.atlas.json.io.parser.TokenizerJSON ;
import org.apache.jena.datatypes.TypeMapper ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.riot.Lang ;
import org.apache.jena.riot.RDFLanguages ;
import org.apache.jena.riot.system.ParserProfile ;
import org.apache.jena.riot.system.StreamRDF ;
import org.apache.jena.riot.tokens.Token ;
import org.apache.jena.riot.tokens.TokenType ;
import org.apache.jena.riot.tokens.Tokenizer ;

/**
 * RDF-JSON.
 * <p>
 * This is not JSON-LD.
 *
 * @see <a href="http://www.w3.org/TR/rdf-json/">http://www.w3.org/TR/rdf-json/</a>
 */
public class LangRDFJSON extends LangBase
{
	public LangRDFJSON(Tokenizer tokenizer, ParserProfile profile, StreamRDF dest)
	{
		super(tokenizer, profile, dest) ;
		if (!(tokenizer instanceof TokenizerJSON))
		{
			throw new IllegalArgumentException("Tokenizer for the RDF/JSON parser must be an instance of "+TokenizerJSON.class.getName()) ;
		}
	}

	@Override
	public Lang getLang()   { return RDFLanguages.RDFJSON ; }

	@Override
	protected void runParser()
	{
		this.tryParseGraph() ;
	}

	private void tryParseGraph()
	{
		//Must be a { to start the JSON Object
		if (lookingAt(TokenType.LBRACE))
		{
			//Can discard the { safely
			nextToken() ;

			//Then want to try parsing triples
			tryParseTriples();

			//Ensure that there is a } to end the JSON Object and discard it
			if (!lookingAt(TokenType.RBRACE))
			{
				exception(peekToken(), "Expected a } character to end a JSON Object but got %s", peekToken()) ;
			}
			nextToken() ;

			//Should now be at the end of the file
			if (!lookingAt(TokenType.EOF))
			{
				exception(peekToken(), "Expected the end of the JSON but there is additional content beyond the end of the JSON Object") ;
			}
			nextToken();
		}
		else
		{
			exception(peekToken(), "Expected a { character to start a JSON Object but got %s", peekToken()) ;
		}
	}

	private void tryParseTriples()
	{
		//First we expect to see a Property Name which is the Subject for
		//our Triples
		//Note that subjectExpected starts as false because we can get an empty graph
		//in which case we won't see any subjects
		boolean subjectExpected = false;
		while (moreTokens())
		{
			if (lookingAt(TokenType.RBRACE))
			{
				if (subjectExpected)
				{
					exception(peekToken(), "Expected a Property Name after a comma to represent the Subject of the next block of triples but got %s", peekToken()) ;
				}

				//Otherwise this is the end of the JSON Object representing the Graph so just return
				return;
			}
			else if (isPropertyName())
			{
				subjectExpected = false;
				//Is a Property Name so represents a Subject
				Token t = nextToken() ;
				Node subj;
				if (t.getImage().startsWith("_:"))
				{
					subj = profile.createBlankNode(null, t.getImage().substring(2), t.getLine(), t.getColumn()) ;
				}
				else
				{
					subj = profile.createURI(t.getImage(), t.getLine(), t.getColumn()) ;
				}

				//Should always be a : after a Property Name
				checkColon() ;

				//Now try and parse the Predicate Object List
				tryParsePredicateObjectList(subj) ;

				//After the end of a Predicate Object List may optionally have a comma
				//to denote there are further
				if (lookingAt(TokenType.COMMA))
				{
					nextToken();
					subjectExpected = true;
				}
			}
			else
			{
				if (subjectExpected)
				{
					exception(peekToken(), "Expected a Property Name after a comma to represent the Subject of the next block of triples but got %s", peekToken()) ;
				}
				else
				{
					exception(peekToken(), "Expected either the end of the JSON Object (the } character) or a JSON Property Name (String) to set the Subject for some Triples but got %s", peekToken()) ;
				}
			}
		}
	}

	private void tryParsePredicateObjectList(Node subj)
	{
		//RDF/JSON defines a Predicate Object list to be a JSON Object
		//where each Predicate is a Property Name and the Object List
		//is a JSON Array

		//First must see the { to start the object which we can then discard
		if (!lookingAt(TokenType.LBRACE))
		{
			exception(peekToken(), "Expected a { character to start the JSON Object for a Predicate Object List but got a %s", peekToken()) ;
		}
		nextToken() ;

		//Then we must see a Property Name or the end of the predicate object list
		boolean first = true;
		boolean propertyNameExpected = true;
		while (true)
		{
			if (isPropertyName())
			{
				first = false;
				propertyNameExpected = false;
				Token t = nextToken();
				Node pred = profile.createURI(t.getImage(), t.getLine(), t.getColumn()) ;

				//Must be a : after Property Name
				checkColon() ;

				//Then we can try and parse the Object List
				tryParseObjectList(subj, pred) ;

				//After this we may optionally see a , which
				//means that there are more property names present
				//i.e. further predicates for this subject
				if (lookingAt(TokenType.COMMA))
				{
					nextToken() ;
					propertyNameExpected = true;
				}
			}
			else if (!first && lookingAt(TokenType.RBRACE))
			{
				//This is the end of the Predicate Object List Object
				if (propertyNameExpected)
				{
					exception(peekToken(), "Expected a further Property Name to represent a Predicate after a comma in a Predicate Object List but got %s", peekToken()) ;
				}
				nextToken();
				return;
			}
			else
			{
				if (propertyNameExpected)
				{
					exception(peekToken(), "Expected a Property Name to represent a Predicate as part of a Predicate Object List but got %s", peekToken()) ;
				}
				else
				{
					exception(peekToken(), "Expected a Property Name or the end of the Predicate Object List but got %s", peekToken()) ;
				}
			}
		}
	}

	private void tryParseObjectList(Node subj, Node pred)
	{
		//RDF/JSON defines an Object List to be a JSON Array where
		//the array may consist of JSON Objects which represent
		//the objects of Triples

		//First must see the [ to start the array
		if (lookingAt(TokenType.LBRACKET))
		{
			nextToken();

			boolean first = true;
			boolean objectExpected = true;
			while (true)
			{
				if (lookingAt(TokenType.LBRACE))
				{
					if (!objectExpected)
					{
						exception(peekToken(), "Expected the end of the JSON Array for the Object List as no comma was seen after the preceding } but got %s", peekToken());
					}

					first = false;
					objectExpected = false;

					//Parse the Object and Emit the Triple
					Node obj = tryParseObject();
					Triple t = profile.createTriple(subj, pred, obj, currLine, currCol) ;
					dest.triple(t) ;

					//After this may optionally see a comma to indicate
					//that there are further objects in the object list
					if (lookingAt(TokenType.COMMA))
					{
						nextToken();
						objectExpected = true;
					}
				}
				else if (!first && lookingAt(TokenType.RBRACKET))
				{
					//This is the end of the Object List Object
					if (objectExpected)
					{
						exception(peekToken(), "Expected a further JSON Object to represent an Object after a comma in a Object List but got %s", peekToken()) ;
					}
					nextToken();
					return;
				}
				else
				{
					if (objectExpected)
					{
						exception(peekToken(), "Expected a JSON Object to represent an Object as part of a Object List but got %s", peekToken()) ;
					}
					else
					{
						exception(peekToken(), "Expected a JSON Object or the end of the Object List but got %s", peekToken()) ;
					}
				}
			}
		}
		else
		{
			exception(peekToken(), "Expected a [ character to start a JSON Array for the Object List but got %s", peekToken()) ;
		}
	}

	private Node tryParseObject()
	{
		//RDF/JSON defines the Object of a Triple to be encoded as a
		//JSON Object
		//It has mandatory properties 'value' and 'type' plus optional
		//properties 'lang', 'xml:lang' and 'datatype'

		Node obj = null;
		Token value = null, type = null, lang = null, datatype = null;

		//First we expect to see the { character to start the JSON Object
		if (lookingAt(TokenType.LBRACE))
		{
			//Discard the {
			nextToken();

			//Then see a stream of tokens which are property value pairs
			//representing the properties of the object
			boolean first = true;
			boolean propertyNameExpected = true;
			while (true)
			{
				if (isPropertyName())
				{
					first = false;
					propertyNameExpected = false;

					Token t = nextToken();
					String name = t.getImage();

					//Must always be a : after a property name
					checkColon();

					//Is this one of our valid properties
                    switch ( name )
                    {
                        case "value":
                            if ( value == null )
                            {
                                value = checkValidForObjectProperty();
                            }
                            else
                            {
                                exception( t,
                                           "Encountered the value property on an Object when the value property has already been specified" );
                            }
                            break;
                        case "type":
                            if ( type == null )
                            {
                                type = checkValidForObjectProperty();
                            }
                            else
                            {
                                exception( t,
                                           "Encountered the type property on an Object when the type property has already been specified" );
                            }
                            break;
                        case "lang":
                        case "xml:lang":
                            if ( lang == null && datatype == null )
                            {
                                lang = checkValidForObjectProperty();
                            }
                            else
                            {
                                exception( t,
                                           "Encountered the %s property on an Object when lang/datatype has already been specified",
                                           name );
                            }
                            break;
                        case "datatype":
                            if ( lang == null && datatype == null )
                            {
                                datatype = checkValidForObjectProperty();
                            }
                            else
                            {
                                exception( t,
                                           "Encountered the %s property on an Object when lang/datatype has already been specified",
                                           name );
                            }
                            break;
                        default:
                            exception( t,
                                       "Unexpected Property Name %s encountered, expected one of value, type, lang or datatype",
                                       t.getImage() );
                            break;
                    }

					//After each Property Value pair we may optionally
					//see a comma to indicate further pairs are present
					if (lookingAt(TokenType.COMMA))
					{
						nextToken();
						propertyNameExpected = true;
					}
				}
				else if (!first && lookingAt(TokenType.RBRACE))
				{
					if (propertyNameExpected)
					{
						exception(peekToken(), "Expected a further Property Name to represent a property of the Object of a Triple after a comma but got %s", peekToken()) ;
					}
					break;
				}
				else
				{
					exception(peekToken(), "Expected a Property Name to define a property relating to the Object of a Triple but got %s", peekToken()) ;
				}
			}
			//Discard the } which terminated the Object
			nextToken();

			//Next up validate the tokens we've got
			if (type == null) exception(peekToken(), "Unable to parse the Object for a Triple from a JSON Object as the required 'type' property is not present") ;
			if (value == null) exception(peekToken(), "Unable to parse the Object for a Triple from a JSON Object as the required 'value' property is not present") ;

			//Use these to create the Object
			String typeStr = type.getImage();
            switch ( typeStr )
            {
                case "uri":
                    obj = profile.createURI( value.getImage(), value.getLine(), value.getColumn() );
                    break;
                case "bnode":
                    obj = profile.createBlankNode( null, value.getImage().substring( 2 ), value.getLine(),
                                                   value.getColumn() );
                    break;
                case "literal":
                    if ( lang != null )
                    {
                        obj = profile.createLangLiteral( value.getImage(), lang.getImage(), value.getLine(),
                                                         value.getColumn() );
                    }
                    else if ( datatype != null )
                    {
                        obj = profile.createTypedLiteral( value.getImage(), TypeMapper.getInstance().getSafeTypeByName(
                            datatype.getImage() ), value.getLine(), value.getColumn() );
                    }
                    else
                    {
                        obj = profile.createStringLiteral( value.getImage(), value.getLine(), value.getColumn() );
                    }
                    break;
                default:
                    exception( type,
                               "Unable to parse the Object for a Triple from a JSON Object as the value %s given for the 'type' property is not one of uri, bnode or literal",
                               typeStr );
                    break;
            }
		}
		else
		{
			exception(peekToken(), "Expected a { character to start a JSON Object to represent the Object of a Triple but got %s", peekToken()) ;
		}
		return obj;
	}

    private boolean isPropertyName()
	{
		return lookingAt(TokenType.STRING);
	}

    private Token checkValidForObjectProperty()
    {
    	Token t = null;
    	if (lookingAt(TokenType.STRING) )
    		t = nextToken();
    	else
    		exception(peekToken(), "JSON Values given for properties for an Object must be Strings") ;
    	return t;
    }

	private void checkColon()
	{
		if (!lookingAt(TokenType.COLON))
			exception(peekToken(), "Expected a : character after a JSON Property Name but got %s", peekToken()) ;
		nextToken() ;
	}

}
