package com.jsoniter.output;

import com.jsoniter.spi.JsonException;
import com.jsoniter.any.Any;
import com.jsoniter.spi.Encoder;
import com.jsoniter.spi.TypeLiteral;

import java.io.IOException;
import java.io.OutputStream;

public class JsonStream extends OutputStream {

    public static int defaultIndentionStep = 0;
    public int indentionStep = defaultIndentionStep;
    private int indention = 0;
    private OutputStream out;
    byte buf[];
    int count;

    public JsonStream(OutputStream out, int bufSize) {
        if (bufSize < 32) {
            throw new JsonException("buffer size must be larger than 32: " + bufSize);
        }
        this.out = out;
        this.buf = new byte[bufSize];
    }

    public void reset(OutputStream out) {
        this.out = out;
        this.count = 0;
    }

    public final void write(int b) throws IOException {
        if (count == buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte) b;
    }

    public final void write(byte b1, byte b2) throws IOException {
        if (count >= buf.length - 1) {
            flushBuffer();
        }
        buf[count++] = b1;
        buf[count++] = b2;
    }

    public final void write(byte b1, byte b2, byte b3) throws IOException {
        if (count >= buf.length - 2) {
            flushBuffer();
        }
        buf[count++] = b1;
        buf[count++] = b2;
        buf[count++] = b3;
    }

    public final void write(byte b1, byte b2, byte b3, byte b4) throws IOException {
        if (count >= buf.length - 3) {
            flushBuffer();
        }
        buf[count++] = b1;
        buf[count++] = b2;
        buf[count++] = b3;
        buf[count++] = b4;
    }

    public final void write(byte b1, byte b2, byte b3, byte b4, byte b5) throws IOException {
        if (count >= buf.length - 4) {
            flushBuffer();
        }
        buf[count++] = b1;
        buf[count++] = b2;
        buf[count++] = b3;
        buf[count++] = b4;
        buf[count++] = b5;
    }

    public final void write(byte b1, byte b2, byte b3, byte b4, byte b5, byte b6) throws IOException {
        if (count >= buf.length - 5) {
            flushBuffer();
        }
        buf[count++] = b1;
        buf[count++] = b2;
        buf[count++] = b3;
        buf[count++] = b4;
        buf[count++] = b5;
        buf[count++] = b6;
    }

    public final void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length - count) {
            if (len >= buf.length) {
            /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
                flushBuffer();
                out.write(b, off, len);
                return;
            }
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }

    public void flush() throws IOException {
        flushBuffer();
        out.flush();
    }

    @Override
    public void close() throws IOException {
        if (count > 0) {
            flushBuffer();
        }
        out.close();
        this.out = null;
        count = 0;
    }

    final void flushBuffer() throws IOException {
        out.write(buf, 0, count);
        count = 0;
    }

    public final void writeVal(String val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            StreamImplString.writeString(this, val);
        }
    }

    public final void writeRaw(String val) throws IOException {
        writeRaw(val, val.length());
    }

    public final void writeRaw(String val, int remaining) throws IOException {
        int i = 0;
        for (; ; ) {
            int available = buf.length - count;
            if (available < remaining) {
                remaining -= available;
                int j = i + available;
                val.getBytes(i, j, buf, count);
                count = buf.length;
                flushBuffer();
                i = j;
            } else {
                int j = i + remaining;
                val.getBytes(i, j, buf, count);
                count += remaining;
                return;
            }
        }
    }

    public final void writeVal(Boolean val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            if (val) {
                writeTrue();
            } else {
                writeFalse();
            }
        }
    }

    public final void writeVal(boolean val) throws IOException {
        if (val) {
            writeTrue();
        } else {
            writeFalse();
        }
    }

    public final void writeTrue() throws IOException {
        write((byte) 't', (byte) 'r', (byte) 'u', (byte) 'e');
    }

    public final void writeFalse() throws IOException {
        write((byte) 'f', (byte) 'a', (byte) 'l', (byte) 's', (byte) 'e');
    }

    public final void writeVal(Short val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            writeVal(val.intValue());
        }
    }

    public final void writeVal(short val) throws IOException {
        writeVal((int) val);
    }

    public final void writeVal(Integer val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            writeVal(val.intValue());
        }
    }

    public final void writeVal(int val) throws IOException {
        StreamImplNumber.writeInt(this, val);
    }


    public final void writeVal(Long val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            writeVal(val.longValue());
        }
    }

    public final void writeVal(long val) throws IOException {
        StreamImplNumber.writeLong(this, val);
    }


    public final void writeVal(Float val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            writeVal(val.floatValue());
        }
    }

    public final void writeVal(float val) throws IOException {
        StreamImplNumber.writeFloat(this, val);
    }

    public final void writeVal(Double val) throws IOException {
        if (val == null) {
            writeNull();
        } else {
            writeVal(val.doubleValue());
        }
    }

    public final void writeVal(double val) throws IOException {
        StreamImplNumber.writeDouble(this, val);
    }

    public final void writeVal(Any val) throws IOException {
        val.writeTo(this);
    }

    public final void writeNull() throws IOException {
        write((byte) 'n', (byte) 'u', (byte) 'l', (byte) 'l');
    }

    public final void writeEmptyObject() throws IOException {
        write((byte) '{', (byte) '}');
    }

    public final void writeEmptyArray() throws IOException {
        write((byte) '[', (byte) ']');
    }

    public final void writeArrayStart() throws IOException {
        indention += indentionStep;
        write('[');
        writeIndention();
    }

    public final void writeMore() throws IOException {
        write(',');
        writeIndention();
    }

    private void writeIndention() throws IOException {
        writeIndention(0);
    }

    private void writeIndention(int delta) throws IOException {
        if (indention == 0) {
            return;
        }
        write('\n');
        int toWrite = indention - delta;
        int i = 0;
        for (; ; ) {
            for (; i < toWrite && count < buf.length; i++) {
                buf[count++] = ' ';
            }
            if (i == toWrite) {
                break;
            } else {
                flushBuffer();
            }
        }
    }

    public final void writeArrayEnd() throws IOException {
        writeIndention(indentionStep);
        indention -= indentionStep;
        write(']');
    }

    public final void writeObjectStart() throws IOException {
        indention += indentionStep;
        write('{');
        writeIndention();
    }

    public final void writeObjectField(String field) throws IOException {
        writeVal(field);
        write(':');
    }

    public final void writeObjectEnd() throws IOException {
        writeIndention(indentionStep);
        indention -= indentionStep;
        write('}');
    }

    public final void writeVal(Object obj) throws IOException {
        if (obj == null) {
            writeNull();
            return;
        }
        Class<?> clazz = obj.getClass();
        String cacheKey = TypeLiteral.create(clazz).getEncoderCacheKey();
        Codegen.getEncoder(cacheKey, clazz).encode(obj, this);
    }

    public final <T> void writeVal(TypeLiteral<T> typeLiteral, T obj) throws IOException {
        if (null == obj) {
            writeNull();
        } else {
            Codegen.getEncoder(typeLiteral.getEncoderCacheKey(), typeLiteral.getType()).encode(obj, this);
        }
    }

    private final static ThreadLocal<JsonStream> tlsStream = new ThreadLocal<JsonStream>() {
        @Override
        protected JsonStream initialValue() {
            return new JsonStream(null, 4096);
        }
    };

    public static void serialize(Object obj, OutputStream out) {
        JsonStream stream = tlsStream.get();
        try {
            try {
                stream.reset(out);
                stream.writeVal(obj);
            } finally {
                stream.close();
            }
        } catch (IOException e) {
            throw new JsonException(e);
        }
    }

    private final static ThreadLocal<AsciiOutputStream> tlsAsciiOutputStream = new ThreadLocal<AsciiOutputStream>() {
        @Override
        protected AsciiOutputStream initialValue() {
            return new AsciiOutputStream();
        }
    };

    public static String serialize(Object obj) {
        AsciiOutputStream asciiOutputStream = tlsAsciiOutputStream.get();
        asciiOutputStream.reset();
        serialize(obj, asciiOutputStream);
        return asciiOutputStream.toString();
    }

    public static void setMode(EncodingMode mode) {
        Codegen.setMode(mode);
    }

    public static void registerNativeEncoder(Class clazz, Encoder encoder) {
        CodegenImplNative.NATIVE_ENCODERS.put(clazz, encoder);
    }
}
