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

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.FastStreamingDocsCallback;
import org.apache.solr.client.solrj.impl.BinaryRequestWriter;
import org.apache.solr.client.solrj.impl.StreamingBinaryResponseParser;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.util.FastJavaBinDecoder.Tag;

import static org.apache.solr.common.util.Utils.NEW_ARRAYLIST_FUN;
import static org.apache.solr.common.util.Utils.NEW_LINKED_HASHMAP_FUN;

public class TestFastJavabinDecoder extends SolrTestCaseJ4 {


  public void testTagRead() throws Exception {
    BinaryRequestWriter.BAOS baos = new BinaryRequestWriter.BAOS();
    FastOutputStream faos = FastOutputStream.wrap(baos);

    JavaBinCodec codec = new JavaBinCodec(faos, null);
    codec.writeVal(10);
    codec.writeVal(100);
    codec.writeVal("Hello!");

    faos.flushBuffer();
    faos.close();


    FastInputStream fis = new FastInputStream(null, baos.getbuf(), 0, baos.size());
    FastJavaBinDecoder.StreamCodec scodec = new FastJavaBinDecoder.StreamCodec(fis);
    scodec.start();
    Tag tag = scodec.getTag();
    assertEquals(Tag._SINT, tag);
    assertEquals(10, scodec.readSmallInt(scodec.dis));
    tag = scodec.getTag();
    assertEquals(Tag._SINT, tag);
    assertEquals(100, scodec.readSmallInt(scodec.dis));
    tag = scodec.getTag();
    assertEquals(Tag._STR, tag);
    assertEquals("Hello!", scodec.readStr(fis));
  }

  public void testSimple() throws IOException {
    String sampleObj = "{k : v , " +
        "mapk : {k1: v1, k2 : [v2_1 , v2_2 ]}," +
        "listk : [ 1, 2, 3 ]," +
        "maps : [ {id: kov1}, {id : kov2} ,{id:kov3 , longv : 234} ]," +
        "}";


    Map m = (Map) Utils.fromJSONString(sampleObj);
    BinaryRequestWriter.BAOS baos = new BinaryRequestWriter.BAOS();
    new JavaBinCodec().marshal(m, baos);

    Map m2 = (Map) new JavaBinCodec().unmarshal(new FastInputStream(null, baos.getbuf(), 0, baos.size()));

    LinkedHashMap fastMap = (LinkedHashMap) new FastJavaBinDecoder()
        .withInputStream(new FastInputStream(null, baos.getbuf(), 0, baos.size()))
        .decode(FastJavaBinDecoder.getEntryListener());
    assertEquals(Utils.writeJson(m2, new StringWriter(), true).toString(),
        Utils.writeJson(fastMap, new StringWriter(), true).toString());

    Object newMap = new FastJavaBinDecoder()
        .withInputStream(new FastInputStream(null, baos.getbuf(), 0, baos.size()))
        .decode(e -> {
          e.listenContainer(new LinkedHashMap<>(), e_ -> {
            Map rootMap = (Map) e_.ctx();
            if (e_.type() == DataEntry.Type.ENTRY_ITER) {
              e_.listenContainer(rootMap.computeIfAbsent(e_.name(), NEW_ARRAYLIST_FUN),
                  FastJavaBinDecoder.getEntryListener());
            } else if (e_.type() == DataEntry.Type.KEYVAL_ITER) {
              e_.listenContainer(rootMap.computeIfAbsent(e_.name(), NEW_LINKED_HASHMAP_FUN), e1 -> {
                Map m1 = (Map) e1.ctx();
                if ("k1".equals(e1.name())) {
                  m1.put(e1.name(), e1.val().toString());
                }
                //eat up k2
              });
            } else if (e_.type() == DataEntry.Type.STR) {
              rootMap.put(e_.name(), e_.val().toString());
            }

          });
        });
    ((Map) m2.get("mapk")).remove("k2");
    assertEquals(Utils.writeJson(m2, new StringWriter(), true).toString(),
        Utils.writeJson(newMap, new StringWriter(), true).toString());

  }

  public void testFastJavabinStreamingDecoder() throws IOException {
    BinaryRequestWriter.BAOS baos = new BinaryRequestWriter.BAOS();
    try (InputStream is = getClass().getResourceAsStream("/solrj/javabin_sample.bin")) {
      IOUtils.copy(is, baos);
    }
    SimpleOrderedMap o = (SimpleOrderedMap) new JavaBinCodec().unmarshal(baos.toByteArray());
    SolrDocumentList list = (SolrDocumentList) o.get("response");
    System.out.println(" " + list.getNumFound() + " , " + list.getStart() + " , " + list.getMaxScore());
    class Pojo {
      long _idx;
      CharSequence id;
      boolean inStock;
      float price;
      List<NamedList> children;
    }
    StreamingBinaryResponseParser parser = new StreamingBinaryResponseParser(new FastStreamingDocsCallback() {

      @Override
      public Object initDocList(Long numFound, Long start, Float maxScore) {
        assertEquals((Long) list.getNumFound(), numFound);
        assertEquals((Long) list.getStart(), start);
        assertEquals(list.getMaxScore(), maxScore);
        return new int[1];
      }

      @Override
      public Object startDoc(Object docListObj) {
        Pojo pojo = new Pojo();
        pojo._idx = ((int[]) docListObj)[0]++;
        return pojo;
      }

      @Override
      public void field(DataEntry field, Object docObj) {
        Pojo pojo = (Pojo) docObj;
        if ("id".equals(field.name())) {
          pojo.id = ((Utf8CharSequence) field.val()).clone();
        } else if (field.type() == DataEntry.Type.BOOL && "inStock".equals(field.name())) {
          pojo.inStock = field.boolVal();
        } else if (field.type() == DataEntry.Type.FLOAT && "price".equals(field.name())) {
          pojo.price = field.floatVal();
        }

      }

      @Override
      public void endDoc(Object docObj) {
        Pojo pojo = (Pojo) docObj;
        SolrDocument doc = list.get((int) pojo._idx);
        assertEquals(doc.get("id"), pojo.id.toString());
        if (doc.get("inStock") != null)
          assertEquals(doc.get("inStock"), pojo.inStock);
        if (doc.get("price") != null)
          assertEquals((Float) doc.get("price"), pojo.price, 0.001);
      }
    });

    parser.processResponse(new FastInputStream(null, baos.getbuf(), 0, baos.size()), null);


  }

  public void testParsingWithChildDocs() throws IOException {
    SolrDocument d1 = TestJavaBinCodec.generateSolrDocumentWithChildDocs();
    d1.setField("id", "101");
    SolrDocument d2 = TestJavaBinCodec.generateSolrDocumentWithChildDocs();
    d2.setField("id", "102");
    d2.setField("longs", Arrays.asList(100l, 200l));

    SolrDocumentList sdocs = new SolrDocumentList();
    sdocs.setStart(0);
    sdocs.setNumFound(2);
    sdocs.add(d1);
    sdocs.add(d2);

    SimpleOrderedMap orderedMap = new SimpleOrderedMap();
    orderedMap.add("response", sdocs);

    BinaryRequestWriter.BAOS baos = new BinaryRequestWriter.BAOS();
    new JavaBinCodec().marshal(orderedMap, baos);
    boolean[] useListener = new boolean[1];
    useListener[0] = true;

    class Pojo {
      CharSequence id;
      CharSequence subject;
      CharSequence cat;
      long[] longs;
      final List<Pojo> children = new ArrayList<>();

      public void compare(SolrDocument d) {
        assertEquals(id, d.getFieldValue("id"));
        assertEquals(subject, d.getFieldValue("subject"));
        assertEquals(cat, d.getFieldValue("cat"));
        assertEquals(d.getChildDocumentCount(), children.size());
        List<Long> l = (List<Long>) d.getFieldValue("longs");
        if(l != null){
          assertNotNull(longs);
          for (int i = 0; i < l.size(); i++) {
            Long v = l.get(i);
            assertEquals(v.longValue(), longs[i]);
          }
        }
        List<SolrDocument> childDocuments = d.getChildDocuments();
        if (childDocuments == null) return;
        for (int i = 0; i < childDocuments.size(); i++) {
          children.get(i).compare(childDocuments.get(i));
        }

      }

    }
    List<Pojo> l = new ArrayList<>();
    StreamingBinaryResponseParser binaryResponseParser = new StreamingBinaryResponseParser(new FastStreamingDocsCallback() {

      @Override
      public Object initDocList(Long numFound, Long start, Float maxScore) {
        return l;
      }

      @Override
      public Object startDoc(Object docListObj) {
        Pojo pojo = new Pojo();
        ((List) docListObj).add(pojo);
        return pojo;
      }

      @Override
      public void field(DataEntry field, Object docObj) {
        Pojo pojo = (Pojo) docObj;
        if (field.name().equals("id")) {
          pojo.id = field.strValue();
        } else if (field.name().equals("subject")) {
          pojo.subject = field.strValue();
        } else if (field.name().equals("cat")) {
          pojo.cat = field.strValue();
        } else if (field.type() == DataEntry.Type.ENTRY_ITER && "longs".equals(field.name())) {
          if(useListener[0]){
            field.listenContainer(pojo.longs = new long[field.length()], READLONGS);
          } else {
            List<Long> longList = (List<Long>) field.val();
            pojo.longs = new long[longList.size()];
            for (int i = 0; i < longList.size(); i++) {
              pojo.longs[i] = longList.get(i);

            }

          }
        }

      }


      @Override
      public Object startChildDoc(Object parentDocObj) {
        Pojo parent = (Pojo) parentDocObj;
        Pojo child = new Pojo();
        parent.children.add(child);
        return child;
      }
    });
    binaryResponseParser.processResponse(new FastInputStream(null, baos.getbuf(), 0, baos.size()), null);
    for (int i = 0; i < sdocs.size(); i++) {
      l.get(i).compare(sdocs.get(i));
    }

    l.clear();

    useListener[0] = false;
    binaryResponseParser.processResponse(new FastInputStream(null, baos.getbuf(), 0, baos.size()), null);
    for (int i = 0; i < sdocs.size(); i++) {
      l.get(i).compare(sdocs.get(i));
    }


  }

  static final DataEntry.EntryListener READLONGS = e -> {
    if (e.type() != DataEntry.Type.LONG) return;
    long[] array = (long[]) e.ctx();
    array[(int) e.index()] = e.longVal();

  };
}
