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

import java.io.*;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.*;
import java.util.Map.Entry;

import org.apache.jena.JenaRuntime ;
import org.apache.jena.rdf.model.* ;
import org.apache.jena.rdf.model.impl.Util ;
import org.apache.jena.shared.JenaException ;
import org.apache.jena.util.iterator.ClosableIterator ;
import org.apache.jena.util.iterator.WrappedIterator ;
import org.apache.jena.vocabulary.OWL ;
import org.apache.jena.vocabulary.RDF ;
import org.apache.jena.vocabulary.XSD ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Common framework for implementing N3 writers.
 */

public class N3JenaWriterCommon implements RDFWriter
{
    static Logger logger = LoggerFactory.getLogger(N3JenaWriterCommon.class) ;
    
	// N3 writing proceeds in 2 stages.
    // First, it analysis the model to be written to extract information
    // that is going to be specially formatted (RDF lists, one ref anon nodes)
    // Second do the writing walk.
    
    // The simple N3 writer does nothing during preparation.
    
    protected Map<String, Object> writerPropertyMap = null ;

// BaseURI - <#>    
//    final boolean doAbbreviatedBaseURIref = getBooleanValue("abbrevBaseURI", true) ; 
    protected boolean alwaysAllocateBNodeLabel = false ;
    
    // Common variables
    protected RDFErrorHandler errorHandler = null;

    protected Map<String, String> prefixMap 	   	= new HashMap<>() ;	// Prefixes to actually use
    protected Map<String, String> reversePrefixMap  = new HashMap<>() ;   // URI->prefix
	protected Map<Resource, String>	bNodesMap       = null ;		    // BNodes seen.
	protected int bNodeCounter    = 0 ;

    // Specific properties that have a short form.
	// Not Turtle.
    protected static final String NS_W3_log = "http://www.w3.org/2000/10/swap/log#" ;
    
	protected static Map<String, String> wellKnownPropsMapN3 = new HashMap<>() ;
	static {
	    wellKnownPropsMapN3.put(NS_W3_log+"implies",		"=>" ) ;
	    wellKnownPropsMapN3.put(OWL.sameAs.getURI(),	    "="  ) ;
	    wellKnownPropsMapN3.put(RDF.type.getURI(),		"a"  ) ;
	}

    protected static Map<String, String> wellKnownPropsMapTurtle = new HashMap<>() ;
    static {
        //wellKnownPropsMapTurtle.put(OWL.sameAs.getURI(),      "="  ) ;
        wellKnownPropsMapTurtle.put(RDF.type.getURI(),        "a"  ) ;
    }

    protected Map<String, String> wellKnownPropsMap = wellKnownPropsMapN3 ;
    
	// Work variables controlling the output
	protected N3IndentedWriter out = null ;
	//Removed base URI specials - look for  "// BaseURI - <#>" & doAbbreviatedBaseURIref
	//String baseURIref = null ;
    //String baseURIrefHash = null ;

    // Min spacing of items    
	protected int minGap = getIntValue("minGap", 1) ;
	protected String minGapStr = pad(minGap) ;

    // Gap from subject to property
	protected int indentProperty = getIntValue("indentProperty", 6) ;
    
    // Width of property before wrapping.
    // This is not necessarily a control of total width
    // e.g. the pretty writer may be writing properties inside indented one ref bNodes 
	protected int widePropertyLen = getIntValue("widePropertyLen", 20) ;
    
    // Column for property when an object follows a property on the same line
	protected int propertyCol = getIntValue("propertyColumn", 8) ;
    
    // Minimum gap from property to object when object on a new line.
    protected int indentObject = propertyCol ;
    
    // If a subject is shorter than this, the first property may go on same line.
    protected int subjectColumn = getIntValue("subjectColumn", indentProperty) ; 
    // Require shortSubject < subjectCol (strict less than)
    protected int shortSubject = subjectColumn-minGap;
    
    protected boolean useWellKnownPropertySymbols = getBooleanValue("usePropertySymbols", true) ;
    
    protected boolean allowTripleQuotedStrings = getBooleanValue("useTripleQuotedStrings", true) ;
    protected boolean allowDoubles   = getBooleanValue("useDoubles", true) ;
    protected boolean allowDecimals  = getBooleanValue("useDecimals", true) ;
    
    // ----------------------------------------------------
    // Jena RDFWriter interface

	@Override
    public RDFErrorHandler setErrorHandler(RDFErrorHandler errHandler)
	{
		RDFErrorHandler old = errorHandler;
		errorHandler = errHandler;
		return old;
	}

    @Override
    public Object setProperty(String propName, Object propValue) 
    {
        if ( ! ( propValue instanceof String ) )
        {
            logger.warn("N3.setProperty: Property for '"+propName+"' is not a string") ;
            propValue = propValue.toString() ;
        }
        
        // Store absolute name of property 
        propName = absolutePropName(propName) ;
        if ( writerPropertyMap == null )
            writerPropertyMap = new HashMap<>() ;
        Object oldValue = writerPropertyMap.get(propName);
        writerPropertyMap.put(propName, propValue);
        return oldValue;
   }

    /** Write the model out in N3.  The writer should be one suitable for UTF-8 which
    * excludes a PrintWriter or a FileWriter which use default character set.
    *
    * Examples:
    * <pre>
    * try {
    *      Writer w =  new BufferedWriter(new OutputStreamWriter(output, "UTF-8")) ;
    *      model.write(w, base) ;
    *      try { w.flush() ; } catch (IOException ioEx) {...}
    *  } catch (java.io.UnsupportedEncodingException ex) {} //UTF-8 is required so can't happen
    * </pre>
    * or
    * <pre>
    * try {
    *     OutputStream out = new FileOutputStream(file) ;
    *     Writer w =  new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) ;
    *     model.write(w, base) ;
    * }
    * catch (java.io.UnsupportedEncodingException ex) {}
    * catch (java.io.FileNotFoundException noFileEx) { ... }
    * </pre>
    * @see #write(Model,Writer,String)
    */

    @Override
    public void write(Model baseModel, Writer _out, String base) 
    {
        if (!(_out instanceof BufferedWriter))
            _out = new BufferedWriter(_out);
        out = new N3IndentedWriter(_out);

// BaseURI - <#>        
//        if ( base != null )
//        {
//            baseURIref = base ;
//            if ( !base.endsWith("#") &&! isOpaque(base) )
//                baseURIrefHash = baseURIref+"#" ;
//        }
        
        processModel(baseModel) ;
    }
    
	/** Write the model out in N3, encoded in in UTF-8
	 * @see #write(Model,Writer,String)
	 */

	@Override
    public synchronized void write(Model model, OutputStream output, String base) 
	{
		try {
			Writer w =  new BufferedWriter(new OutputStreamWriter(output, "UTF-8")) ;
			write(model, w, base) ;
			try { w.flush() ; } catch (IOException ioEx) { throw new JenaException(ioEx) ; }
		} catch (java.io.UnsupportedEncodingException ex)
		{
			System.err.println("Failed to create UTF-8 writer") ;
		}
	}

    // ----------------------------------------------------
    // The assumed processing model is:
    // Writing N3 involves ordering the graph into:
    // -- Subjects
    // -- Property lists within subjects
    // -- Object lists with in properties
    
    //  A derived class may choose to intercept and implement at any of these levels. 
     
    // Standard layout is:
    // subject
    //    property1 value1 ;
    //    property2 value2 ;
    //    property3 value3 .
    
    // Normal hook points for subclasses.

    protected void startWriting() {}
    protected void finishWriting() {}
    protected void prepare(Model model) {}
    
    protected void processModel(Model model)
    {
        prefixMap = model.getNsPrefixMap() ;
        bNodesMap = new HashMap<>() ;

        for ( Iterator<Entry<String, String>> iter = prefixMap.entrySet().iterator() ; iter.hasNext() ; )
        {
            Entry<String, String> e = iter.next() ;
            String prefix = e.getKey() ;
            String uri = e.getValue(); 
            
            // XML namespaces name can include '.'
            // Turtle prefixed names can't.
            if ( ! checkPrefixPart(prefix) ) 
                iter.remove() ;
            else
            {
                if ( checkPrefixPart(prefix) )
                    // Build acceptable reverse mapping  
                    reversePrefixMap.put(uri, prefix) ;
            }
        }
        
        startWriting() ;
        prepare(model) ;

        writeHeader(model) ;
        writePrefixes(model) ;
        
        if (prefixMap.size() != 0)
            out.println();

        // Do the output.
        writeModel(model) ;

        // Release intermediate memory - allows reuse of a writer
        finishWriting() ;
        bNodesMap = null ;
    }

    protected void writeModel(Model model)
    {
       // Needed only for no prefixes, no blank first line. 
        boolean doingFirst = true;
        ResIterator rIter = listSubjects(model);
        for (; rIter.hasNext();)
        {
            // Subject:
            // First - it is something we will write out as a structure in an object field?
            // That is, a RDF list or the object of exactly one statement.
            Resource subject = rIter.nextResource();
            if ( skipThisSubject(subject) )
            {
                if (N3JenaWriter.DEBUG)
                    out.println("# Skipping: " + formatResource(subject));
                continue;
            }

            // We really are going to print something via writeTriples
            if (doingFirst)
                doingFirst = false;
            else
                out.println();

            writeOneGraphNode(subject) ;
            
            
        }
        rIter.close();
    } 
    
    protected ResIterator listSubjects(Model model) { return model.listSubjects(); }

    protected void writeOneGraphNode(Resource subject)
    {
        // New top level item.
        // Does not take effect until newline.
        out.incIndent(indentProperty) ;
        writeSubject(subject);
        ClosableIterator<Property> iter = preparePropertiesForSubject(subject);
        writePropertiesForSubject(subject, iter) ;
        out.decIndent(indentProperty) ; 
        out.println(" .");
    }

    protected void writePropertiesForSubject(Resource subj, ClosableIterator<Property> iter)
    {
        // For each property.
        for (; iter.hasNext();)
        {
            Property property = iter.next();

            // Object list
            writeObjectList(subj, property);

            if (iter.hasNext())
                out.println(" ;");
        }
        iter.close();
    }

    // Hook called on every resource.
    // Since there is spacing bewteen resource frames, need to know
    // whether an item will cause any output.   
    protected boolean skipThisSubject(Resource r) { return false ; }


    // This is the hook called within writeModel.  
    // NB May not be at the top level (indent = 0)
    
    protected void writeSubject(Resource subject)
    {
        String subjStr = formatResource(subject);
        out.print(subjStr);
        // May be very short : if so, stay on this line.
        
        // Currently at end of subject.
        // NB shortSubject is (subjectColumn-minGap) so there is a gap.

        if (subjStr.length() < shortSubject )
        {
            out.print(pad(subjectColumn - subjStr.length()) );
        }
        else
            // Does not fit this line.
            out.println();
    }
    
    protected void writeHeader(Model model)
    {
// BaseURI - <#>
//        if (baseURIref != null && !baseURIref.equals("") )
//            out.println("# Base: " + baseURIref);
    }
    
    protected N3IndentedWriter getOutput() { return out ; }
    protected Map<String, String> getPrefixes() { return prefixMap ; }
    
    protected void writePrefixes(Model model)
    {
        for ( Entry<String, String> entry : prefixMap.entrySet() )
        {
            String u = entry.getValue();
            String p = entry.getKey();            

// BaseURI - <#>            
//            // Special cases: N3 handling of base names.
//            if (doAbbreviatedBaseURIref && p.equals(""))
//            {
//                if (baseURIrefHash != null && u.equals(baseURIrefHash))
//                    u = "#";
//                if (baseURIref != null && u.equals(baseURIref))
//                    u = "";
//            }

            String tmp = "@prefix " + p + ": ";
            out.print( tmp );
            out.print( pad( 16 - tmp.length() ) );
            // NB Starts with a space to ensure a gap.
            out.println( " <" + u + "> ." );
        }

    }
    
    protected void writeObjectList(Resource subject, Property property)
    {
        String propStr = formatProperty(property) ;

//        if (wellKnownPropsMap.containsKey(property.getURI()))
//            propStr = (String) wellKnownPropsMap.get(property.getURI());
//        else
//            propStr = formatResource(property);

        // Write with object lists as clusters of statements with the same property
        // Looks more like a machine did it but fewer bad cases.

        StmtIterator sIter = subject.listProperties(property);
        for (; sIter.hasNext();)
        {
            Statement stmt = sIter.nextStatement() ;
            String objStr = formatNode(stmt.getObject()) ;
            
            out.print(propStr);
            out.incIndent(indentObject);

            if ( (propStr.length()+minGap) <= widePropertyLen )
            {
                // Property col allows for min gap but widePropertyLen > propertyCol 
                // (which looses alignment - this is intentional.
                // Ensure there is at least min gap.
                
                int padding = calcPropertyPadding(propStr) ;
                out.print(pad(padding)) ;
                
//                if ( propStr.length() < propertyWidth ) 
//                    out.print( pad(propertyCol-minGap-propStr.length()) ) ;
//                out.print(minGapStr) ;
            }
            else
                // Does not fit this line.
                out.println();

            // Write one object - simple writing.
            
            out.print(objStr) ;
            out.decIndent(indentObject);

            if ( sIter.hasNext() )
            {
                out.println(" ;") ;
            }
        }
        sIter.close() ;

    }

    protected String formatNode(RDFNode node)
    {
        if (node instanceof Literal)
            return formatLiteral((Literal) node);
        else
            return formatResource((Resource)node) ;
    }

    protected void writeObject(RDFNode node)
    {
        if (node instanceof Literal)
        {
            writeLiteral((Literal) node);
            return;
        }

        Resource rObj = (Resource) node;

        out.print(formatResource(rObj));
    }
    
    protected void writeLiteral(Literal literal) 
    {
        out.print(formatLiteral(literal)) ;
    }
    
    protected ClosableIterator<Property> preparePropertiesForSubject(Resource r)
    {
        // Properties to do.
        Set<Property> properties = new HashSet<>() ;

        StmtIterator sIter = r.listProperties();
        for ( ; sIter.hasNext() ; )
            properties.add(sIter.nextStatement().getPredicate()) ;
        sIter.close() ;
        return WrappedIterator.create(properties.iterator()) ;
    }
    
    
    // Utility operations
    protected String formatResource(Resource r)
	{
		if ( r.isAnon() )
		{
            if ( ! alwaysAllocateBNodeLabel )
            {
                // Does anything point to it?
                StmtIterator sIter = r.getModel().listStatements(null, null, r) ;
    
                if ( ! sIter.hasNext() )
                {
                    sIter.close() ;
                    // This bNode is not referenced so don't need the bNode Id.
                    // Must be a subject - indent better be zero!
                    // This only happens for subjects because object bNodes
                    // referred to once (the other case for [] syntax)
                    // are handled elsewhere (by oneRef set)
    
                    // Later: use [ prop value ] for this.
                    return "[]" ;
                }
                sIter.close() ;
            }
			if ( ! bNodesMap.containsKey(r) )
				bNodesMap.put(r, "_:b"+(++bNodeCounter)) ;
			return bNodesMap.get(r) ;

		}

		// It has a URI.
        
		if ( r.equals(RDF.nil) )
			return "()" ;

        return formatURI(r.getURI()) ;
    }

    protected String formatLiteral(Literal literal)
    {
        String datatype = literal.getDatatypeURI() ;
        String lang = literal.getLanguage() ;
    	String s = literal.getLexicalForm() ;
    
        if ( datatype != null )
        {
            // Special form we know how to handle?
            // Assume valid text
            if ( datatype.equals(XSD.integer.getURI()) )
            {
                try {
                    new java.math.BigInteger(s) ;
                    return s ;
                } catch (NumberFormatException nfe) {}
                // No luck.  Continue.
                // Continuing is always safe.
            }
                
            if ( datatype.equals(XSD.decimal.getURI()) )
            {
                // Must have ., can't have e or E
                if ( s.indexOf('.') >= 0 &&
                     s.indexOf('e') == -1 && s.indexOf('E') == -1 )
                {
                    // Turtle - N3 does not allow .3 +.3 or -.3
                    // See if parsable.
                    try {
                        new BigDecimal(s);
                        return s ;
                    } catch (NumberFormatException nfe) {}
                }
            }
            
            if ( this.allowDoubles && datatype.equals(XSD.xdouble.getURI()) )
            {
                // Must have 'e' or 'E' (N3 and Turtle now read 2.3 as a decimal).
                if ( s.indexOf('e') >= 0 ||
                     s.indexOf('E') >= 0 )
                {
                    try {
                        // Validate it.
                        Double.parseDouble(s) ;
                        return s ;
                    } catch (NumberFormatException nfe) {}
                    // No luck.  Continue.
                }
            }
        }
        // Format the text - with escaping.
        StringBuffer sbuff = new StringBuffer() ;
        boolean singleQuoteLiteral = true ;
        
        String quoteMarks = "\"" ;
        
        // Things that force the use of """ strings
        if ( this.allowTripleQuotedStrings &&
             ( s.indexOf("\n") != -1 ||
               s.indexOf("\r") != -1 ||
               s.indexOf("\f") != -1 ) )
        {
            quoteMarks = "\"\"\"" ;
            singleQuoteLiteral = false ;
        }
        
        sbuff.append(quoteMarks);
        string(sbuff, s, singleQuoteLiteral) ;
        sbuff.append(quoteMarks);
    
        if ( Util.isLangString(literal) ) {
            sbuff.append("@") ;
            sbuff.append(lang) ;
        } else if ( ! Util.isSimpleString(literal) ) {
            sbuff.append("^^") ;
            sbuff.append(formatURI(datatype)) ;
        }
        return sbuff.toString() ;
    }
    
    protected String formatProperty(Property p)
    {
        String prop = p.getURI() ;
        if ( this.useWellKnownPropertySymbols && wellKnownPropsMap.containsKey(prop) )
            return wellKnownPropsMap.get(prop);

        return formatURI(prop) ;
    }
    
    protected String formatURI(String uriStr)
    {
		

// BaseURI - <#>		
//        if ( doAbbreviatedBaseURIref && uriStr.equals(baseURIref) )
//            return "<>" ;

		// Try for a prefix and write as prefixed name.
		// 1/ Try splitting as a prefixed name
		// 2/ Search for possibilities
		
		// Stage 1.
		int idx = splitIdx(uriStr) ;
		// Depends on legal URIs.
		if ( idx >= 0 )
		{
		    // Include the # itself.
		    String x = uriStr.substring(0,idx+1) ;
		    String prefix = reversePrefixMap.get(x) ;
		    if ( prefix != null )
		    {
		        String localPart = uriStr.substring(idx+1) ;
		        if ( checkNamePart(localPart) )
		            return prefix+':'+localPart ;
		    }
		}
		
		// Unsplit. Could just return here. 
//		// Find the longest if several.
//        // Possible optimization: split URI and have URI=> ns: map.
//        // Ordering prefixes by length, then first hit is better.
//        // 
//        // Also: could just assume that the split is on / or #
//        // Means we need to find a prefix just once. 
//		for ( Iterator<String> pIter = prefixMap.keySet().iterator() ; pIter.hasNext() ; )
//		{
//			String p = pIter.next() ;
//			String u = prefixMap.get(p) ;
//			if ( uriStr.startsWith(u) )
//				if ( matchURI.length() < u.length() )
//				{
//					matchPrefix = p ;
//					matchURI = u ;
//				}
//		}
//		if ( matchPrefix != null )
//		{
//			String localname = uriStr.substring(matchURI.length()) ;
//            
//            if ( checkPrefixedName(matchPrefix, localname) )
//                return matchPrefix+":"+localname ;
//
//            // Continue and return quoted URIref
//		}

		// Not as a prefixed name - write as a quoted URIref
        // It should be right - the writer should be UTF-8 on output.
		return "<"+uriStr+">" ;
	}

    protected static int splitIdx(String uriStr)
    {
        int idx = uriStr.lastIndexOf('#') ;
        if ( idx >= 0 )
            return idx ;
        // No # - try for /
        idx = uriStr.lastIndexOf('/') ;
        return idx ;
    }
    
    // Checks of prefixed names
    // These tests must agree, or be more restrictive, than the parser. 
    protected static boolean checkPrefixedName(String ns, String local)
    {
        return checkPrefixPart(ns) && checkNamePart(local) ;
    }
    
    /* http://www.w3.org/TeamSubmission/turtle/#sec-grammar-grammar
     * [27]    qname           ::=     prefixName? ':' name?
     * [30]    nameStartChar   ::=     [A-Z] | "_" | [a-z] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] | [#x0370-#x037D] | [#x037F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
     * [31]    nameChar        ::=     nameStartChar | '-' | [0-9] | #x00B7 | [#x0300-#x036F] | [#x203F-#x2040]
     * [32]    name            ::=     nameStartChar nameChar*
     * [33]    prefixName      ::=     ( nameStartChar - '_' ) nameChar*
     */
    
    protected static boolean checkPrefixPart(String s)
    {
        if ( s.length() == 0 )
            return true;
        CharacterIterator cIter = new StringCharacterIterator(s) ;
        char ch = cIter.first() ;
        if ( ! checkNameStartChar(ch) )
            return false ;
        if ( ch == '_' )    // Can't start with _ (bnodes labels handled separately) 
            return false ;
        return checkNameTail(cIter) ;
    }
    
    protected static boolean checkNamePart(String s)
    {
        if ( s.length() == 0 )
            return true; 
        CharacterIterator cIter = new StringCharacterIterator(s) ;
        char ch = cIter.first() ;
        if ( ! checkNameStartChar(ch) )
            return false ;
        return checkNameTail(cIter) ;
    }
    
    private static boolean checkNameTail(CharacterIterator cIter)
    {
        // Assumes cIter.first already called but nothing else.
        // Skip first char.
        char ch = cIter.next() ;
        for ( ; ch != java.text.CharacterIterator.DONE ; ch = cIter.next() )
        {
            if ( ! checkNameChar(ch) )
                return false ;
        } 
        return true ;
    }

    protected static boolean checkNameStartChar(char ch)
    {
        if ( Character.isLetter(ch) )
            return true ;
        if ( ch == '_' )
            return true ;
        return false ;
    }

    protected static boolean checkNameChar(char ch)
    {
        if ( Character.isLetterOrDigit(ch) )
            return true ;
        if ( ch == '_' )
            return true ;
        if ( ch == '-' )
            return true ;
        return false ;
    }

    
    protected final static String WS = "\n\r\t" ;

	protected static void string(StringBuffer sbuff, String s, boolean singleQuoteLiteral)
    {
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);

            // Escape escapes and quotes
            if (c == '\\' || c == '"' )
            {
                sbuff.append('\\') ;
                sbuff.append(c) ;
                continue ;
            }
            
            // Characters to literally output.
            // This would generate 7-bit safe files 
//            if (c >= 32 && c < 127)
//            {
//                sbuff.append(c) ;
//                continue;
//            }    

            // Whitespace
            if ( singleQuoteLiteral && ( c == '\n' || c == '\r' || c == '\f' ) )
            {
                if (c == '\n') sbuff.append("\\n");
                if (c == '\t') sbuff.append("\\t");
                if (c == '\r') sbuff.append("\\r");
                if (c == '\f') sbuff.append("\\f");
                continue ;
            }
            
            // Output as is (subject to UTF-8 encoding on output that is)
            sbuff.append(c) ;
            
//            // Unicode escapes
//            // c < 32, c >= 127, not whitespace or other specials
//            String hexstr = Integer.toHexString(c).toUpperCase();
//            int pad = 4 - hexstr.length();
//            sbuff.append("\\u");
//            for (; pad > 0; pad--)
//                sbuff.append("0");
//            sbuff.append(hexstr);
        }
    }
    
    protected int calcPropertyPadding(String propStr)
    {
        int padding = propertyCol - propStr.length();
        if (padding < minGap)
            padding = minGap;
        return padding ;
    }
     
	protected static String pad(int cols)
	{
		StringBuffer sb = new StringBuffer() ;
		for ( int i = 0 ; i < cols ; i++ )
			sb.append(' ') ;
		return sb.toString() ;
	}

	// Utilities

    protected int countProperties(Resource r) 
	{
		int numProp = 0 ;
		StmtIterator sIter = r.listProperties() ;
		for ( ; sIter.hasNext() ; )
		{
			sIter.nextStatement() ;
			numProp++ ;
		}
		sIter.close() ;
		return numProp ;
	}

    protected int countProperties(Resource r, Property p) 
	{
		int numProp = 0 ;
		StmtIterator sIter = r.listProperties(p) ;
		for ( ; sIter.hasNext() ; )
		{
			sIter.nextStatement() ;
			numProp++ ;
		}
		sIter.close() ;
		return numProp ;
	}


    protected int countArcsTo(Resource resource) 
	{
		return countArcsTo(null, resource) ;
	}

    protected int countArcsTo(Property prop, Resource resource) 
	{
		int numArcs = 0 ;
		StmtIterator sIter = resource.getModel().listStatements(null, prop, resource) ;
		for ( ; sIter.hasNext() ; )
		{
			sIter.nextStatement() ;
			numArcs++ ;
		}
		sIter.close() ;
		return numArcs ;
	}


    protected Iterator<RDFNode> rdfListIterator(Resource r)
	{
		List<RDFNode> list = new ArrayList<>() ;

		for ( ; ! r.equals(RDF.nil); )
		{
			StmtIterator sIter = r.getModel().listStatements(r, RDF.first, (RDFNode)null) ;
			list.add(sIter.nextStatement().getObject()) ;
			if ( sIter.hasNext() )
				// @@ need to cope with this (unusual) case
				throw new JenaException("N3: Multi valued list item") ;
			sIter = r.getModel().listStatements(r, RDF.rest, (RDFNode)null) ;
			r = (Resource)sIter.nextStatement().getObject() ;
			if ( sIter.hasNext() )
				throw new JenaException("N3: List has two tails") ;
		}
		return list.iterator() ;
	}
    
    // Convenience operations for accessing system properties.
    
    protected String getStringValue(String prop, String defaultValue)
    {
        String p = getPropValue(prop) ;
        
        if ( p == null )
            return defaultValue ;
        return p ;
    }
     
    protected boolean getBooleanValue(String prop, boolean defaultValue)
    {
        String p = getPropValue(prop) ;
        
        if ( p == null )
            return defaultValue ;
            
        if ( p.equalsIgnoreCase("true") )
            return true ;
        
        if ( p.equals("1") )
            return true ;
            
        return false ;
    }        

    protected int getIntValue(String prop, int defaultValue)
    {
        String p = getPropValue(prop) ;
        if ( p == null )
            return defaultValue ;
        try {
            return Integer.parseInt(p) ;
        } catch (NumberFormatException ex)
        {
            logger.warn("Format error for property: "+prop) ;
            return defaultValue ;
        }
    }
    
    // May be the absolute or local form of the property name
    
    protected String getPropValue(String prop)
    {
        prop = absolutePropName(prop) ;
        if ( writerPropertyMap != null && writerPropertyMap.containsKey(prop) )
        {
            Object obj = writerPropertyMap.get(prop) ;
            if ( ! ( obj instanceof String ) )
                logger.warn("getPropValue: N3 Property for '"+prop+"' is not a string") ;
            return (String)obj ; 
        }
        String s = JenaRuntime.getSystemProperty(prop) ;
        if ( s == null )
            s = JenaRuntime.getSystemProperty(localPropName(prop)) ;
        return s ;
    }
    
    protected String absolutePropName(String propName)
    {
        if ( propName.indexOf(':') == -1 )
            return N3JenaWriter.propBase + propName ;
        return propName ;
    }
    
    protected String localPropName(String propName)
    {
        if ( propName.startsWith(N3JenaWriter.propBase) )
            propName = propName.substring(N3JenaWriter.propBase.length()) ;
        return propName ;
    }
    
    private boolean isOpaque(String uri)
    {
        try {
            return new URI(uri).isOpaque() ;
        } catch (URISyntaxException ex) { return true ; }
    }
}
