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

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.UnicodeUtil;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.ReturnFields;


/**
 * A description of the PHP serialization format can be found here:
 * http://www.hurring.com/scott/code/perl/serialize/
 */
public class PHPSerializedResponseWriter implements QueryResponseWriter {
  static String CONTENT_TYPE_PHP_UTF8="text/x-php-serialized;charset=UTF-8";

  private String contentType = CONTENT_TYPE_PHP_UTF8;

  @Override
  public void init(@SuppressWarnings({"rawtypes"})NamedList namedList) {
    String contentType = (String) namedList.get("content-type");
    if (contentType != null) {
      this.contentType = contentType;
    }
  }
  
  @Override
  public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
    PHPSerializedWriter w = new PHPSerializedWriter(writer, req, rsp);
    try {
      w.writeResponse();
    } finally {
      w.close();
    }
  }

  @Override
  public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
    return contentType;
  }
}

class PHPSerializedWriter extends JSONWriter {
  byte[] utf8;

  public PHPSerializedWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) {
    super(writer, req, rsp);
    this.utf8 = BytesRef.EMPTY_BYTES;
    // never indent serialized PHP data
    doIndent = false;
  }

  @Override
  public void writeResponse() throws IOException {
    Boolean omitHeader = req.getParams().getBool(CommonParams.OMIT_HEADER);
    if(omitHeader != null && omitHeader) rsp.removeResponseHeader();
    writeNamedList(null, rsp.getValues());
  }
  
  @Override
  public void writeNamedList(String name, @SuppressWarnings({"rawtypes"})NamedList val) throws IOException {
    writeNamedListAsMapMangled(name,val);
  }
  
  @Deprecated
  @Override
  public void writeStartDocumentList(String name, 
      long start, int size, long numFound, Float maxScore) throws IOException
  {
    throw new UnsupportedOperationException();
  }

  @Override
  public void writeStartDocumentList(String name, 
      long start, int size, long numFound, Float maxScore, Boolean numFoundExact) throws IOException
  {
    writeMapOpener(headerSize(maxScore, numFoundExact));
    writeKey("numFound",false);
    writeLong(null,numFound);
    writeKey("start",false);
    writeLong(null,start);

    if (maxScore!=null) {
      writeKey("maxScore",false);
      writeFloat(null,maxScore);
    }
    if (numFoundExact != null) {
      writeKey("numFoundExact",false);
      writeBool(null, numFoundExact);
    }
    writeKey("docs",false);
    writeArrayOpener(size);
  }

  @Override
  public void writeEndDocumentList() throws IOException
  {
    writeArrayCloser(); // doc list
    writeMapCloser();
  }
  
  @Override
  public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException
  {
    writeKey(idx, false);
    
    LinkedHashMap <String,Object> single = new LinkedHashMap<>();
    LinkedHashMap <String,Object> multi = new LinkedHashMap<>();

    for (String fname : doc.getFieldNames()) {
      if (returnFields != null && !returnFields.wantsField(fname)) {
        continue;
      }

      Object val = doc.getFieldValue(fname);
      if (val instanceof Collection) {
        multi.put(fname, val);
      }else{
        single.put(fname, val);
      }
    }

    writeMapOpener(single.size() + multi.size());
    for(Map.Entry<String, Object> entry : single.entrySet()){
      String fname = entry.getKey();
      Object val = entry.getValue();
      writeKey(fname, true);
      writeVal(fname, val);
    }
    
    for(Map.Entry<String, Object> entry : multi.entrySet()){
      String fname = entry.getKey();
      writeKey(fname, true);

      Object val = entry.getValue();
      if (!(val instanceof Collection)) {
        // should never be reached if multivalued fields are stored as a Collection
        // so I'm assuming a size of 1 just to wrap the single value
        writeArrayOpener(1);
        writeVal(fname, val);
        writeArrayCloser();
      }else{
        writeVal(fname, val);
      }
    }
    
    writeMapCloser();
  }

  
  @Override
  public void writeArray(String name, Object[] val) throws IOException {
    writeMapOpener(val.length);
    for(int i=0; i < val.length; i++) {
      writeKey(i, false);
      writeVal(String.valueOf(i), val[i]);
    }
    writeMapCloser();
  }

  @Override
  @SuppressWarnings({"unchecked"})
  public void writeArray(String name, @SuppressWarnings({"rawtypes"})Iterator val) throws IOException {
    @SuppressWarnings({"rawtypes"})
    ArrayList vals = new ArrayList();
    while( val.hasNext() ) {
      vals.add(val.next());
    }
    writeArray(name, vals.toArray());
  }
  
  @Override
  public void writeMapOpener(int size) throws IOException, IllegalArgumentException {
    // negative size value indicates that something has gone wrong
    if (size < 0) {
      throw new IllegalArgumentException("Map size must not be negative");
    }
    writer.write("a:"+size+":{");
  }
  
  @Override
  public void writeMapSeparator() throws IOException {
    /* NOOP */
  }

  @Override
  public void writeMapCloser() throws IOException {
    writer.write('}');
  }

  @Override
  public void writeArrayOpener(int size) throws IOException, IllegalArgumentException {
    // negative size value indicates that something has gone wrong
    if (size < 0) {
      throw new IllegalArgumentException("Array size must not be negative");
    }
    writer.write("a:"+size+":{");
  }

  @Override  
  public void writeArraySeparator() throws IOException {
    /* NOOP */
  }

  @Override
  public void writeArrayCloser() throws IOException {
    writer.write('}');
  }
  
  @Override
  public void writeNull(String name) throws IOException {
    writer.write("N;");
  }

  @Override
  public void writeKey(String fname, boolean needsEscaping) throws IOException {
    writeStr(null, fname, needsEscaping);
  }
  void writeKey(int val, boolean needsEscaping) throws IOException {
    writeInt(null, String.valueOf(val));
  }

  @Override
  public void writeBool(String name, boolean val) throws IOException {
    writer.write(val ? "b:1;" : "b:0;");
  }

  @Override
  public void writeBool(String name, String val) throws IOException {
    writeBool(name, val.charAt(0) == 't');
  }
  
  @Override
  public void writeInt(String name, String val) throws IOException {
    writer.write("i:"+val+";");
  }
  
  @Override
  public void writeLong(String name, String val) throws IOException {
    writeInt(name,val);
  }

  @Override
  public void writeFloat(String name, String val) throws IOException {
    writeDouble(name,val);
  }

  @Override
  public void writeDouble(String name, String val) throws IOException {
    writer.write("d:"+val+";");
  }

  @Override
  public void writeStr(String name, String val, boolean needsEscaping) throws IOException {
    // serialized PHP strings don't need to be escaped at all, however the 
    // string size reported needs be the number of bytes rather than chars.
    utf8 = ArrayUtil.grow(utf8, val.length() * UnicodeUtil.MAX_UTF8_BYTES_PER_CHAR);
    final int nBytes = UnicodeUtil.UTF16toUTF8(val, 0, val.length(), utf8);

    writer.write("s:");
    writer.write(Integer.toString(nBytes));
    writer.write(":\"");
    writer.write(val);
    writer.write("\";");
  }
}