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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Stack;
import java.util.Collection;
import java.util.Arrays;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

@RunWith(Parameterized.class)
public class TestBlockingIO {
  private static final String UTF_8 = "UTF-8";

  private final int iSize;
  private final int iDepth;
  private final String sInput;

  public TestBlockingIO (int sz, int dp, String inp) {
    this.iSize = sz;
    this.iDepth = dp;
    this.sInput = inp;
  }
  
  private static class Tests {
    private final JsonParser parser;
    private final Decoder input;
    private final int depth;
    public Tests(int bufferSize, int depth, String input)
      throws IOException {
  
      this.depth = depth;
      byte[] in = input.getBytes("UTF-8");
      JsonFactory f = new JsonFactory();
      JsonParser p = f.createJsonParser(
          new ByteArrayInputStream(input.getBytes("UTF-8")));
      
      ByteArrayOutputStream os = new ByteArrayOutputStream();
      EncoderFactory factory = new EncoderFactory()
          .configureBlockSize(bufferSize);
      Encoder cos = factory.blockingBinaryEncoder(os, null);
      serialize(cos, p, os);
      cos.flush();
      
      byte[] bb = os.toByteArray();
      // dump(bb);
      this.input = DecoderFactory.get().binaryDecoder(bb, null);
      this.parser =  f.createJsonParser(new ByteArrayInputStream(in));
    }
    
    public void scan() throws IOException {
      Stack<S> countStack = new Stack<S>();
      long count = 0;
      while (parser.nextToken() != null) {
        switch (parser.getCurrentToken()) {
        case END_ARRAY:
          assertEquals(0, count);
          assertTrue(countStack.peek().isArray);
          count = countStack.pop().count;
          break;
        case END_OBJECT:
          assertEquals(0, count);
          assertFalse(countStack.peek().isArray);
          count = countStack.pop().count;
          break;
        case START_ARRAY:
          countStack.push(new S(count, true));
          count = input.readArrayStart();
          continue;
        case VALUE_STRING:
        {
          String s = parser.getText();
          int n = s.getBytes(UTF_8).length;
          checkString(s, input, n);
          break;
        }
        case FIELD_NAME:
        {
          String s = parser.getCurrentName();
          int n = s.getBytes(UTF_8).length;
          checkString(s, input, n);
          continue;
        }
        case START_OBJECT:
          countStack.push(new S(count, false));
          count = input.readMapStart();
          if (count < 0) {
            count = -count;
            input.readLong();  // byte count
          }
          continue;
        default:
          throw new RuntimeException("Unsupported: " + parser.getCurrentToken());
        }
        count--;
        if (count == 0) {
          count = countStack.peek().isArray ? input.arrayNext() :
            input.mapNext();
        }
      }
    }

    public void skip(int skipLevel) throws IOException {
      Stack<S> countStack = new Stack<S>();
      long count = 0;
      while (parser.nextToken() != null) {
        switch (parser.getCurrentToken()) {
        case END_ARRAY:
          // assertEquals(0, count);
          assertTrue(countStack.peek().isArray);
          count = countStack.pop().count;
          break;
        case END_OBJECT:
          // assertEquals(0, count);
          assertFalse(countStack.peek().isArray);
          count = countStack.pop().count;
          break;
        case START_ARRAY:
          if (countStack.size() == skipLevel) {
            skipArray(parser, input, depth - skipLevel);
            break;
          } else {
            countStack.push(new S(count, true));
            count = input.readArrayStart();
            continue;
          }
        case VALUE_STRING:
        {
          if (countStack.size() == skipLevel) {
            input.skipBytes();
          } else {
            String s = parser.getText();
            int n = s.getBytes(UTF_8).length;
            checkString(s, input, n);
          }
          break;
        }
        case FIELD_NAME:
        {
          String s = parser.getCurrentName();
          int n = s.getBytes(UTF_8).length;
          checkString(s, input, n);
          continue;
        }
        case START_OBJECT:
          if (countStack.size() == skipLevel) {
            skipMap(parser, input, depth - skipLevel);
            break;
          } else {
            countStack.push(new S(count, false));
            count = input.readMapStart();
            if (count < 0) {
              count = -count;
              input.readLong();  // byte count
            }
            continue;
          }
        default:
          throw new RuntimeException("Unsupported: " + parser.getCurrentToken());
        }
        count--;
        if (count == 0) {
          count = countStack.peek().isArray ? input.arrayNext() :
            input.mapNext();
        }
      }
    }
  }

  protected static void dump(byte[] bb) {
    int col = 0;
    for (byte b : bb) {
      if (col % 16 == 0) {
        System.out.println();
      }
      col++;
      System.out.print(Integer.toHexString(b & 0xff) + " ");
    }
    System.out.println();
  }

  private static class S {
    public final long count;
    public final boolean isArray;
    
    public S(long count, boolean isArray) {
      this.count = count;
      this.isArray = isArray;
    }
  }

  @Test
  public void testScan() throws IOException {
    Tests t = new Tests(iSize, iDepth, sInput);
    t.scan();
  }

  @Test
  public void testSkip1() throws IOException {
    testSkip(iSize, iDepth, sInput, 0);
  }

  @Test
  public void testSkip2() throws IOException {
    testSkip(iSize, iDepth, sInput, 1);
  }

  @Test
  public void testSkip3() throws IOException {
    testSkip(iSize, iDepth, sInput, 2);
  }

  private void testSkip(int bufferSize, int depth, String input,
      int skipLevel)
    throws IOException {
    Tests t = new Tests(bufferSize, depth, input);
    t.skip(skipLevel);
  }

  private static void skipMap(JsonParser parser, Decoder input, int depth)
    throws IOException {
    for (long l = input.skipMap(); l != 0; l = input.skipMap()) {
      for (long i = 0; i < l; i++) {
        if (depth == 0) {
          input.skipBytes();
        } else {
          skipArray(parser, input, depth - 1);
        }
      }
    }
    parser.skipChildren();
  }

  private static void skipArray(JsonParser parser, Decoder input, int depth)
    throws IOException {
    for (long l = input.skipArray(); l != 0; l = input.skipArray()) {
      for (long i = 0; i < l; i++) {
        if (depth == 1) {
          input.skipBytes();
        } else {
          skipArray(parser, input, depth - 1);
        }
      }
    }
    parser.skipChildren();
  }
 
  private static void checkString(String s, Decoder input, int n)
    throws IOException {
    ByteBuffer buf = input.readBytes(null);
    assertEquals(n, buf.remaining());
    String s2 = new String(buf.array(), buf.position(),
        buf.remaining(), UTF_8);
    assertEquals(s, s2);
  }
  
  private static void serialize(Encoder cos, JsonParser p,
      ByteArrayOutputStream os)
    throws IOException {
    boolean[] isArray = new boolean[100];
    int[] counts = new int[100];
    int stackTop = -1;
    
    while (p.nextToken() != null) {
      switch (p.getCurrentToken()) {
      case END_ARRAY:
        assertTrue(isArray[stackTop]);
        cos.writeArrayEnd();
        stackTop--;
        break;
      case END_OBJECT:
        assertFalse(isArray[stackTop]);
        cos.writeMapEnd();
        stackTop--;
        break;
      case START_ARRAY:
        if (stackTop >= 0 && isArray[stackTop]) {
          cos.setItemCount(1);
          cos.startItem();
          counts[stackTop]++;
        }
        cos.writeArrayStart();
        isArray[++stackTop] = true;
        counts[stackTop] = 0;
        continue;
      case VALUE_STRING:
        if (stackTop >= 0 && isArray[stackTop]) {
          cos.setItemCount(1);
          cos.startItem();
          counts[stackTop]++;
        }
        byte[] bb = p.getText().getBytes(UTF_8);
        cos.writeBytes(bb);
        break;
      case START_OBJECT:
        if (stackTop >= 0 && isArray[stackTop]) {
          cos.setItemCount(1);
          cos.startItem();
          counts[stackTop]++;
        }
        cos.writeMapStart();
        isArray[++stackTop] = false;
        counts[stackTop] = 0;
        continue;
      case FIELD_NAME:
        cos.setItemCount(1);
        cos.startItem();
        counts[stackTop]++;
        cos.writeBytes(p.getCurrentName().getBytes(UTF_8));
        break;
     default:
       throw new RuntimeException("Unsupported: " + p.getCurrentToken());
      }
    }
  }

  @Parameterized.Parameters
  public static Collection<Object[]> data() {
    return Arrays.asList (new Object[][] {
        { 64, 0, "" },
        { 64, 0, jss(0, 'a') },
        { 64, 0, jss(3, 'a') },
        { 64, 0, jss(64, 'a') },
        { 64, 0, jss(65, 'a') },
        { 64, 0, jss(100, 'a') },
        { 64, 1, "[]" },
        { 64, 1, "[" + jss(0, 'a') + "]" },
        { 64, 1, "[" + jss(3, 'a') + "]" },
        { 64, 1, "[" + jss(61, 'a') + "]" },
        { 64, 1, "[" + jss(62, 'a') + "]" },
        { 64, 1, "[" + jss(64, 'a') + "]" },
        { 64, 1, "[" + jss(65, 'a') + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(0, '0') + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(10, '0') + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(63, '0') + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(64, '0') + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(65, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(0, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(10, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(51, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(52, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(54, '0') + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(55, '0') + "]" },

        { 64, 1, "[" + jss(0, 'a') + "," + jss(0, 'a') + "," + jss(0, '0')
               + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(0, 'a') + "," + jss(63, '0')
               + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(0, 'a') + "," + jss(64, '0')
               + "]" },
        { 64, 1, "[" + jss(0, 'a') + "," + jss(0, 'a') + "," + jss(65, '0')
                 + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(20, 'A') + "," + jss(10, '0')
                 + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(20, 'A') + "," + jss(23, '0')
                 + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(20, 'A') + "," + jss(24, '0')
                 + "]" },
        { 64, 1, "[" + jss(10, 'a') + "," + jss(20, 'A') + "," + jss(25, '0')
                 + "]" },
        { 64, 2, "[[]]"},
        { 64, 2, "[[" + jss(0, 'a') + "], []]" },
        { 64, 2, "[[" + jss(10, 'a') + "], []]" },
        { 64, 2, "[[" + jss(59, 'a') + "], []]" },
        { 64, 2, "[[" + jss(60, 'a') + "], []]" },
        { 64, 2, "[[" + jss(100, 'a') + "], []]" },
        { 64, 2, "[[" + jss(10, '0') + ", " + jss(53, 'a') + "], []]" },
        { 64, 2, "[[" + jss(10, '0') + ", "  + jss(54, 'a') + "], []]" },
        { 64, 2, "[[" + jss(10, '0') + ", "  + jss(55, 'a') + "], []]" },

        { 64, 2, "[[], [" + jss(0, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(10, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(63, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(64, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(65, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(10, '0') + ", " + jss(53, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(10, '0') + ", " + jss(54, 'a') + "]]" },
        { 64, 2, "[[], [" + jss(10, '0') + ", " + jss(55, 'a') + "]]" },

        { 64, 2, "[[" + jss(10, '0') + "]]"},
        { 64, 2, "[[" + jss(62, '0') + "]]"},
        { 64, 2, "[[" + jss(63, '0') + "]]"},
        { 64, 2, "[[" + jss(64, '0') + "]]"},
        { 64, 2, "[[" + jss(10, 'a') + ", " + jss(10, '0') + "]]"},
        { 64, 2, "[[" + jss(10, 'a') + ", " + jss(52, '0') + "]]"},
        { 64, 2, "[[" + jss(10, 'a') + ", " + jss(53, '0') + "]]"},
        { 64, 2, "[[" + jss(10, 'a') + ", " + jss(54, '0') + "]]"},
        { 64, 3, "[[[" + jss(10, '0') + "]]]"},
        { 64, 3, "[[[" + jss(62, '0') + "]]]"},
        { 64, 3, "[[[" + jss(63, '0') + "]]]"},
        { 64, 3, "[[[" + jss(64, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + ", " + jss(10, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + ", " + jss(52, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + ", " + jss(53, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + "], [" + jss(54, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + "], [" + jss(10, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + "], [" + jss(52, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + "], [" + jss(53, '0') + "]]]"},
        { 64, 3, "[[[" + jss(10, 'a') + "], [" + jss(54, '0') + "]]]"},

        { 64, 2, "[[\"p\"], [\"mn\"]]"},
        { 64, 2, "[[\"pqr\"], [\"mn\"]]"},
        { 64, 2, "[[\"pqrstuvwxyz\"], [\"mn\"]]"},
        { 64, 2, "[[\"abc\", \"pqrstuvwxyz\"], [\"mn\"]]"},
        { 64, 2, "[[\"mn\"], [\"\"]]"},
        { 64, 2, "[[\"mn\"], \"abc\"]"},
        { 64, 2, "[[\"mn\"], \"abcdefghijk\"]"},
        { 64, 2, "[[\"mn\"], \"pqr\", \"abc\"]"},
        { 64, 2, "[[\"mn\"]]"},
        { 64, 2, "[[\"p\"], [\"mnopqrstuvwx\"]]"},
        { 64, 2, "[[\"pqr\"], [\"mnopqrstuvwx\"]]"},
        { 64, 2, "[[\"pqrstuvwxyz\"], [\"mnopqrstuvwx\"]]"},
        { 64, 2, "[[\"abc\"], \"pqrstuvwxyz\", [\"mnopqrstuvwx\"]]"},
        { 64, 2, "[[\"mnopqrstuvwx\"], [\"\"]]"},
        { 64, 2, "[[\"mnopqrstuvwx\"], [\"abc\"]]"},
        { 64, 2, "[[\"mnopqrstuvwx\"], [\"abcdefghijk\"]]"},
        { 64, 2, "[[\"mnopqrstuvwx\"], [\"pqr\", \"abc\"]]"},
        { 100, 2, "[[\"pqr\", \"mnopqrstuvwx\"]]"},
        { 100, 2, "[[\"pqr\", \"ab\", \"mnopqrstuvwx\"]]"},
        { 64, 2, "[[[\"pqr\"]], [[\"ab\"], [\"mnopqrstuvwx\"]]]"},

        { 64, 1, "{}" },
        { 64, 1, "{\"n\": \"v\"}" },
        { 64, 1, "{\"n1\": \"v\", \"n2\": []}" },
        { 100, 1, "{\"n1\": \"v\", \"n2\": []}" },
        { 100, 1, "{\"n1\": \"v\", \"n2\": [\"abc\"]}" },
    });
  }

  /**
   * Returns a new JSON String {@code n} bytes long with
   * consecutive characters starting with {@code c}.
   */
  private static String jss(final int n, char c) {
    char[] cc = new char[n + 2];
    cc[0] = cc[n + 1] = '"';
    for (int i = 1; i < n + 1; i++) {
      if (c == 'Z') {
        c = 'a';
      } else if (c == 'z') {
        c = '0';
      } else if (c == '9') {
        c = 'A';
      } else {
        c++;
      }
      cc[i] = c;
    }
    return new String(cc);
  }
}
