/*
 *  Copyright 2006- Yonik Seeley
 *
 * 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.noggit;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

public class JSONWriter {

  /** Implement this interface on your class to support serialization */
  public static interface Writable {
    public void write(JSONWriter writer);
  }

  protected int level;
  protected int indent;
  protected final CharArr out;

  /**
   * @param out the CharArr to write the output to.
   * @param indentSize The number of space characters to use as an indent (default 2). 0=newlines
   *     but no spaces, -1=no indent at all.
   */
  public JSONWriter(CharArr out, int indentSize) {
    this.out = out;
    this.indent = indentSize;
  }

  public JSONWriter(CharArr out) {
    this(out, 2);
  }

  public void setIndentSize(int indentSize) {
    this.indent = indentSize;
  }

  public void indent() {
    if (indent >= 0) {
      out.write('\n');
      if (indent > 0) {
        int spaces = level * indent;
        out.reserve(spaces);
        for (int i = 0; i < spaces; i++) {
          out.unsafeWrite(' ');
        }
      }
    }
  }

  public void write(Object o) {
    // NOTE: an instance-of chain was about 50% faster than hashing on the classes, even with
    // perfect hashing.
    if (o == null) {
      writeNull();
    } else if (o instanceof Writable) {
      ((Writable) o).write(this);
    } else if (o instanceof String) {
      writeString((String) o);
    } else if (o instanceof Number) {
      if (o instanceof Integer || o instanceof Long) {
        write(((Number) o).longValue());
      } else if (o instanceof Float || o instanceof Double) {
        write(((Number) o).doubleValue());
      } else {
        CharArr arr = new CharArr();
        arr.write(o.toString());
        writeNumber(arr);
      }
    } else if (o instanceof Map) {
      write((Map<?, ?>) o);
    } else if (o instanceof Collection) {
      write((Collection<?>) o);
    } else if (o instanceof Iterator) {
      write((Iterator<?>) o);
    } else if (o instanceof Boolean) {
      write(((Boolean) o).booleanValue());
    } else if (o instanceof CharSequence) {
      writeString((CharSequence) o);
    } else if (o instanceof Object[]) {
      write(Arrays.asList((Object[]) o));
    } else if (o instanceof int[]) {
      write((int[]) o);
    } else if (o instanceof float[]) {
      write((float[]) o);
    } else if (o instanceof long[]) {
      write((long[]) o);
    } else if (o instanceof double[]) {
      write((double[]) o);
    } else if (o instanceof short[]) {
      write((short[]) o);
    } else if (o instanceof boolean[]) {
      write((boolean[]) o);
    } else if (o instanceof char[]) {
      write((char[]) o);
    } else if (o instanceof byte[]) {
      write((byte[]) o);
    } else {
      handleUnknownClass(o);
    }
  }

  /**
   * Override this method for custom handling of unknown classes. Also see the Writable interface.
   */
  public void handleUnknownClass(Object o) {
    writeString(o.toString());
  }

  public void write(Map<?, ?> val) {
    startObject();
    int sz = val.size();
    boolean first = true;
    for (Map.Entry<?, ?> entry : val.entrySet()) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      if (sz > 1) indent();
      writeString(entry.getKey().toString());
      writeNameSeparator();
      write(entry.getValue());
    }
    endObject();
  }

  public void write(Collection<?> val) {
    startArray();
    int sz = val.size();
    boolean first = true;
    for (Object o : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      if (sz > 1) indent();
      write(o);
    }
    endArray();
  }

  public void write(Iterator<?> val) {
    startArray();
    boolean first = true;
    while (val.hasNext()) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(val.next());
    }
    endArray();
  }

  /**
   * A byte[] may be either a single logical value, or a list of small integers. It's up to the
   * implementation to decide.
   */
  public void write(byte[] val) {
    startArray();
    boolean first = true;
    for (short v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(short[] val) {
    startArray();
    boolean first = true;
    for (short v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(int[] val) {
    startArray();
    boolean first = true;
    for (int v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(long[] val) {
    startArray();
    boolean first = true;
    for (long v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(float[] val) {
    startArray();
    boolean first = true;
    for (float v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(double[] val) {
    startArray();
    boolean first = true;
    for (double v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(boolean[] val) {
    startArray();
    boolean first = true;
    for (boolean v : val) {
      if (first) {
        first = false;
      } else {
        writeValueSeparator();
      }
      write(v);
    }
    endArray();
  }

  public void write(short number) {
    write((int) number);
  }

  public void write(byte number) {
    write((int) number);
  }

  public void writeNull() {
    JSONUtil.writeNull(out);
  }

  public void writeString(String str) {
    JSONUtil.writeString(str, 0, str.length(), out);
  }

  public void writeString(CharSequence str) {
    JSONUtil.writeString(str, 0, str.length(), out);
  }

  public void writeString(CharArr str) {
    JSONUtil.writeString(str, out);
  }

  public void writeStringStart() {
    out.write('"');
  }

  public void writeStringChars(CharArr partialStr) {
    JSONUtil.writeStringPart(
        partialStr.getArray(), partialStr.getStart(), partialStr.getEnd(), out);
  }

  public void writeStringEnd() {
    out.write('"');
  }

  public void write(long number) {
    JSONUtil.writeNumber(number, out);
  }

  public void write(int number) {
    JSONUtil.writeNumber(number, out);
  }

  public void write(double number) {
    JSONUtil.writeNumber(number, out);
  }

  public void write(float number) {
    JSONUtil.writeNumber(number, out);
  }

  public void write(boolean bool) {
    JSONUtil.writeBoolean(bool, out);
  }

  public void write(char[] val) {
    JSONUtil.writeString(val, 0, val.length, out);
  }

  public void writeNumber(CharArr digits) {
    out.write(digits);
  }

  public void writePartialNumber(CharArr digits) {
    out.write(digits);
  }

  public void startObject() {
    out.write('{');
    level++;
  }

  public void endObject() {
    out.write('}');
    level--;
  }

  public void startArray() {
    out.write('[');
    level++;
  }

  public void endArray() {
    out.write(']');
    level--;
  }

  public void writeValueSeparator() {
    out.write(',');
  }

  public void writeNameSeparator() {
    out.write(':');
  }
}
