/*
 * 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.Writer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.solr.common.EnumFieldValue;
import org.apache.solr.common.IteratorWriter;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.PushWriter;

//Base interface for all text based writers
public interface TextWriter extends PushWriter {

  default void writeVal(String name, Object val) throws IOException {

    // if there get to be enough types, perhaps hashing on the type
    // to get a handler might be faster (but types must be exact to do that...)
    //    (see a patch on LUCENE-3041 for inspiration)

    // go in order of most common to least common, however some of the more general types like Map belong towards the end
    if (val == null) {
      writeNull(name);
    } else if (val instanceof CharSequence) {
      writeStr(name, val.toString(), true);
      // micro-optimization... using toString() avoids a cast first
    } else if (val instanceof Number) {
      writeNumber(name, (Number) val);
    } else if (val instanceof Boolean) {
      writeBool(name, (Boolean) val);
    } else if (val instanceof AtomicBoolean)  {
      writeBool(name, ((AtomicBoolean) val).get());
    } else if (val instanceof Date) {
      writeDate(name, (Date) val);
    } else if (val instanceof NamedList) {
      writeNamedList(name, (NamedList)val);
    } else if (val instanceof Path) {
      writeStr(name, ((Path) val).toAbsolutePath().toString(), true);
    } else if (val instanceof IteratorWriter) {
      writeIterator((IteratorWriter) val);
    } else if (val instanceof MapWriter) {
      writeMap((MapWriter) val);
    } else if (val instanceof MapSerializable) {
      //todo find a better way to reuse the map more efficiently
      writeMap(name, ((MapSerializable) val).toMap(new LinkedHashMap<>()), false, true);
    } else if (val instanceof Map) {
      writeMap(name, (Map)val, false, true);
    } else if (val instanceof Iterator) { // very generic; keep towards the end
      writeArray(name, (Iterator) val);
    } else if (val instanceof Iterable) { // very generic; keep towards the end
      writeArray(name,((Iterable)val).iterator());
    } else if (val instanceof Object[]) {
      writeArray(name,(Object[])val);
    } else if (val instanceof byte[]) {
      byte[] arr = (byte[])val;
      writeByteArr(name, arr, 0, arr.length);
    } else if (val instanceof EnumFieldValue) {
      writeStr(name, val.toString(), true);
    } else if (val instanceof WriteableValue) {
      ((WriteableValue)val).write(name, this);
    } else {
      // default... for debugging only.  Would be nice to "assert false" ?
      writeStr(name, val.getClass().getName() + ':' + val.toString(), true);
    }
  }

  void writeStr(String name, String val, boolean needsEscaping) throws IOException;

  void writeMap(String name, Map val, boolean excludeOuter, boolean isFirstVal) throws IOException;

  void writeArray(String name, Iterator val) throws IOException;

  void writeNull(String name) throws IOException;


  /** if this form of the method is called, val is the Java string form of an int */
  void writeInt(String name, String val) throws IOException;


  /** if this form of the method is called, val is the Java string form of a long */
  void writeLong(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a boolean */
  void writeBool(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a float */
  void writeFloat(String name, String val) throws IOException;


  /** if this form of the method is called, val is the Java string form of a double */
  void writeDouble(String name, String val) throws IOException;


  /** if this form of the method is called, val is the Solr ISO8601 based date format */
  void writeDate(String name, String val) throws IOException;

  void writeNamedList(String name, NamedList val) throws IOException;

  Writer getWriter();

  default void writeNumber(String name, Number val) throws IOException {
    if (val instanceof Integer) {
      writeInt(name, val.toString());
    } else if (val instanceof Long) {
      writeLong(name, val.toString());
    } else if (val instanceof Float) {
      // we pass the float instead of using toString() because
      // it may need special formatting. same for double.
      writeFloat(name, val.floatValue());
    } else if (val instanceof Double) {
      writeDouble(name, val.doubleValue());
    } else if (val instanceof Short) {
      writeInt(name, val.toString());
    } else if (val instanceof Byte) {
      writeInt(name, val.toString());
    } else if (val instanceof AtomicInteger) {
      writeInt(name, ((AtomicInteger) val).get());
    } else if (val instanceof AtomicLong) {
      writeLong(name, ((AtomicLong) val).get());
    } else {
      // default... for debugging only
      writeStr(name, val.getClass().getName() + ':' + val.toString(), true);
    }
  }

  default void writeArray(String name, Object[] val) throws IOException {
    writeArray(name, Arrays.asList(val));
  }

  default void writeArray(String name, List l) throws IOException {
    writeArray(name, l.iterator());
  }


  default void writeDate(String name, Date val) throws IOException {
    writeDate(name, val.toInstant().toString());
  }

  default void writeByteArr(String name, byte[] buf, int offset, int len) throws IOException {
    writeStr(name, Base64.byteArrayToBase64(buf, offset, len), false);
  }

  default void writeInt(String name, int val) throws IOException {
    writeInt(name,Integer.toString(val));
  }

  default void writeLong(String name, long val) throws IOException {
    writeLong(name,Long.toString(val));
  }


  default void writeBool(String name, boolean val) throws IOException {
    writeBool(name,Boolean.toString(val));
  }


  default void writeFloat(String name, float val) throws IOException {
    String s = Float.toString(val);
    // If it's not a normal number, write the value as a string instead.
    // The following test also handles NaN since comparisons are always false.
    if (val > Float.NEGATIVE_INFINITY && val < Float.POSITIVE_INFINITY) {
      writeFloat(name,s);
    } else {
      writeStr(name,s,false);
    }
  }


  default void writeDouble(String name, double val) throws IOException {
    String s = Double.toString(val);
    // If it's not a normal number, write the value as a string instead.
    // The following test also handles NaN since comparisons are always false.
    if (val > Double.NEGATIVE_INFINITY && val < Double.POSITIVE_INFINITY) {
      writeDouble(name,s);
    } else {
      writeStr(name,s,false);
    }
  }
  default void writeBool(String name , Boolean val) throws IOException {
    writeBool(name, val.toString());
  }

  @Override
  default void writeMap(MapWriter mw) throws IOException {
    //todo
  }

  @Override
  default void writeIterator(IteratorWriter iw) throws IOException {
    /*todo*/
  }
  default void indent() throws IOException {
    if (doIndent()) indent(level());
  }
  int incLevel();
  int decLevel();
  TextWriter setIndent(boolean doIndent);
  int level();
  boolean doIndent();
  default void indent(int lev) throws IOException {
    getWriter().write(SolrJSONWriter.indentChars, 0, Math.min((lev<<1)+1, SolrJSONWriter.indentChars.length));
  }

}
