/*
 * 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.uima.json.impl;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Arrays;

import javax.naming.OperationNotSupportedException;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter;

/**
 * Utility class that generates JSON output for UIMA descriptors and CASs
 * 
 * This class is built as a wrapper for a popular open-source implementation of JSON capabilities, "Jackson".
 * 
 * Unusually, it implements the ContentHandler interface, so it can be use with the existing code in UIMA which uses
 * content handlers for serialization.
 * 
 * Because of this, it wraps the IOExceptions that the Jackson package throws, into SAXExceptions that ContentHandlers throw.
 * 
 * Use: Create an instance, specifying the output as a Writer or OutputStream or File
 *        (These three forms are supported by the underlying Jackson impl)
 *        Specify also if doing pretty-printing
 *      Call from other serialization class that does walking (either MetaDataObject_impl or XmiCasSerializer),
 *        to this instance
 *        
 * This class is exposes the Jackson "Generator" API for streaming, and 
 * instances of the Jackson Factory instance for configuring.
 *        
 * The caller uses both this class and the Jackson Generator class.
 * 
 * This class lets the underlying Jackson PrettyPrinter classes track the indent level.
 * PrettyPrinting is implemented via customization of the Jackson PrettyPrinting classes
 * 
 */
public class JsonContentHandlerJacksonWrapper implements ContentHandler  {

  private final static char[] BLANKS = new char[80];
  static {
    Arrays.fill(BLANKS,  ' ');
  }
  
  public final static String SYSTEM_LINE_FEED;
  static {
      String lf = System.getProperty("line.separator");
      SYSTEM_LINE_FEED = (lf == null) ? "\n" : lf;
  }
    
  // this value is kept here, rather than in the caller, because some callers (e.g. MetaDataObject)
  //   have many instances (each element could be an instance).
  private final boolean isFormattedOutput;  // set true if pretty printing
  
  public boolean isFormattedOutput() {
    return isFormattedOutput;
  }

  private final JsonGenerator jg;           // the underlying Jackson Generator
  public JsonGenerator getJsonGenerator() {
    return jg;
  }
  
  private static JsonGenerator createGenerator(JsonFactory f, Object o) throws SAXException {
    try {
      if (o instanceof Writer) {
        return f.createGenerator((Writer)o);
      }
      if (o instanceof OutputStream) {
        return f.createGenerator((OutputStream)o);
      }
      if (o instanceof File) {
        return f.createGenerator((File)o, JsonEncoding.UTF8);
      }
      throw new RuntimeException(new OperationNotSupportedException(String.format("Object must be a Writer, OutputStream, or File, but was of class %s",
          o.getClass().getName())));
    } catch (IOException e) {
      throw new SAXException(e);
    }
  }

  private boolean doNl;               // a flag set by users, that does a newline at the next "significant" output 
  private final UimaJsonPrettyPrinter uimaPrettyPrinter;  // set to null or an instance of the prettyprinter

  /*
   * Constructors
   *   Variants:
   *     JsonFactory - a new one created, or one supplied
   *     isFormattedOutput - a pretty print flag, default false
   */
  
  /**
   * Makes a Json content handler that sends its output to the specified destination
   * @param destination - can be a File, an OutputStream, or a Writer
   * @throws SAXException wrapping an IOException
   */
  public JsonContentHandlerJacksonWrapper(Object destination) throws SAXException {
    this(new JsonFactory(), destination);
  }
  
  /**
   * Makes a Json content handler, using a specified JsonFactory instance that can 
   *   be configured according to the Jackson implementation.
   * The resulting content handler will send its output to the specified destination
   * @param jsonFactory -
   * @param o - where the output goes
   * @throws SAXException wrapping an IOException
   */
  public JsonContentHandlerJacksonWrapper(JsonFactory jsonFactory, Object o) throws SAXException {
    this(createGenerator(jsonFactory, o));
  }

  /**
   * Makes a Json content handler, and
   *   specifies a prettyprinting boolean flag (default is no prettyprinting).
   * The resulting content handler will send its output to the specified destination
   * @param o - where the output goes
   * @param isFormattedOutput -
   * @throws SAXException wrapping an IOException
   */
  public JsonContentHandlerJacksonWrapper(Object o, boolean isFormattedOutput) throws SAXException {
    this(new JsonFactory(), o, isFormattedOutput);
  }
  
  /**
   * Makes a Json content handler, using a specified JsonFactory instance that can 
   *   be configured according to the Jackson implementation, and
   *   specifies a prettyprinting boolean flag (default is no prettyprinting).
   * The resulting content handler will send its output to the specified destination
   * @param jsonFactory -
   * @param o where the output goes
   * @param isFormattedOutput - true for pretty printing
   * @throws SAXException wrapping an IOException
   */
  public JsonContentHandlerJacksonWrapper(JsonFactory jsonFactory, Object o, boolean isFormattedOutput) throws SAXException {
    this(
          createGenerator((null == jsonFactory) ? new JsonFactory() : jsonFactory, o), 
          isFormattedOutput);
  }
    
  /*  C o m m o n  */
  /**
   * Makes a Json content handler, using a specified JsonGenerator instance
   * @param jsonGenerator -
   */  
  public  JsonContentHandlerJacksonWrapper(JsonGenerator jsonGenerator) {this(jsonGenerator, false);}

  /**
   * Makes a Json content handler, using a specified JsonGenerator instance
   * @param jsonGenerator -
   * @param isFormattedOutput - set to true for prettyprinting, default is false
   */  
  public  JsonContentHandlerJacksonWrapper(JsonGenerator jsonGenerator, boolean isFormattedOutput) {
    this.jg = jsonGenerator;
    this.isFormattedOutput = isFormattedOutput;
    if (isFormattedOutput) {
      uimaPrettyPrinter = new UimaJsonPrettyPrinter();
      uimaPrettyPrinter.withoutSpacesInObjectEntries();
      jg.setPrettyPrinter(uimaPrettyPrinter);
    } else {
      uimaPrettyPrinter = null;
    }
  }

  /**
   * Call this to indicate that the prettyprinter should write a new line just before the next significant output.
   *   It won't do this before a "comma", and some other punctuation.
   * Has no effect if no prettyprinting is being done.
   */
  public void writeNlJustBeforeNext() {
    doNl = true;
  }
  
  /* prettyPrinting */
    
  private class UimaJsonPrettyPrinter extends DefaultPrettyPrinter implements PrettyPrinter {

    private static final long serialVersionUID = 1L;
    
    /*
     * add a new line after separators, and before array or object entries
     * (non-Javadoc)
     * @see com.fasterxml.jackson.core.util.DefaultPrettyPrinter#writeObjectEntrySeparator(com.fasterxml.jackson.core.JsonGenerator)
     */
    @Override
    public void writeObjectEntrySeparator(JsonGenerator jg) throws IOException, JsonGenerationException {
      super.writeObjectEntrySeparator(jg);
      maybeOutputNlOrBlank(jg);     
    }

    @Override
    public void writeArrayValueSeparator(JsonGenerator jg) throws IOException, JsonGenerationException {
      super.writeArrayValueSeparator(jg);
      maybeOutputNlOrBlank(jg);
    }

    @Override
    public void beforeArrayValues(JsonGenerator jg) throws IOException, JsonGenerationException {
      maybeOutputNl(jg);
    }

    @Override
    public void beforeObjectEntries(JsonGenerator jg) throws IOException, JsonGenerationException {
      maybeOutputNl(jg);
    }
    
    private void maybeOutputNl(JsonGenerator jg) throws IOException {
      if (doNl) {
        maybeOutputNlIndent();
        doNl = false;
      }      
    }
    
    private void maybeOutputNlOrBlank(JsonGenerator jg) throws IOException {
      if (doNl) {
        maybeOutputNl(jg);  // resets doNl
      } else {
        jg.writeRaw(' ');
      }
    }
    
    private void maybeOutputNlIndent () throws IOException {
      if (isFormattedOutput) {
        jg.writeRaw(SYSTEM_LINE_FEED);
        jg.writeRaw(BLANKS, 0, Math.min(BLANKS.length, _nesting << 1)); // nesting * 2 for spaces per level
      }
    } 
  }
  
  /**
   * Assumes a Json object has been started, and adds property fields to it
   * This method allows reusing common code in the caller.
   * The attr values can be 
   *   arrays (expressed as a valid Json String including the separators, used by XmiCasSerializer only 
   *   strings (which will be scanned for needed escaping and surrounded by necessary quotes
   *   other (which are assumed to not need surrounding quotes
   * @param atts
   */
  private void outputAttrsAsProperties(Attributes atts) {
    if (null != atts) {
      try {        
        for (int i = 0; i < atts.getLength(); i++) {
          String val = atts.getValue(i);
          if (val != null && (!val.equals(""))) {
            final String prefix = atts.getQName(i);
            final String attType = atts.getType(i);
            if ("array".equals(atts.getType(i))) {
              jg.writeArrayFieldStart(prefix);
              jg.writeRawValue(val);  // assumes the caller has formatted the array values properly
                                      // caller 
              jg.writeEndArray();
              continue;
            }
            
            if ("string".equals(attType)) {
              jg.writeStringField(prefix, val);
              continue;
            } 
            
            if ("boolean".equals(attType)) {
              jg.writeBooleanField(prefix, val.equals("true"));
              continue;
            } 
                        
            jg.writeFieldName(prefix);
            jg.writeRawValue(val);     
          }
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }        
  }

  
  /* *****************************
   * mostly unused methods to make this 
   * a ContentHandler
   *******************************/
  private void unsupported() { throw new UnsupportedOperationException();} 
  public void characters(char[] ch, int start, int length) throws SAXException {unsupported();}
  public void endDocument() throws SAXException {}
  public void endPrefixMapping(String prefix) throws SAXException {}
  public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {unsupported();}
  public void processingInstruction(String target, String data) throws SAXException {unsupported();}
  public void setDocumentLocator(Locator locator) {unsupported();}
  public void skippedEntity(String name) throws SAXException {unsupported();}
  public void startDocument() throws SAXException {}
  public void startPrefixMapping(String prefix, String uri) throws SAXException {}
  public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
    try {
      if (qName != null) {
        jg.writeStartObject();
        jg.writeFieldName(qName);
        jg.writeStartObject();
      }
      outputAttrsAsProperties(atts);
    } catch (IOException e) {
      throw new SAXException(e);
    }
  }
  
  public void endElement(String uri, String localName, String qName) throws SAXException {
    try {
    if (null != qName) {
      jg.writeEndObject();
    } 
    jg.writeEndObject();
    } catch (IOException e) {
      throw new SAXException(e);
    }
  }

  
  public void withoutNl() {
    if (isFormattedOutput) {
      uimaPrettyPrinter.indentObjectsWith(FSIWN);
      uimaPrettyPrinter.indentArraysWith(FSIWN);
    }
  }
  
  public void withNl() {
    if (isFormattedOutput) {
      uimaPrettyPrinter.indentObjectsWith(DefaultPrettyPrinter.Lf2SpacesIndenter.instance);
    }
  }
  
  private static class FixedSpaceIndenterWithNesting extends FixedSpaceIndenter {
    private static final long serialVersionUID = 1L;

    @Override
    public boolean isInline() { return false; }  
  }
  
  private static final FixedSpaceIndenterWithNesting FSIWN = new FixedSpaceIndenterWithNesting();

}
