/**
 * 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.fusesource.hawtbuf.proto;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.fusesource.hawtbuf.Buffer;
import org.fusesource.hawtbuf.BufferOutputStream;


abstract public class BaseMessage<T> implements Message<T> {

    protected int memoizedSerializedSize = -1;

    abstract public T clone() throws CloneNotSupportedException;

    public void clear() {
        memoizedSerializedSize = -1;
    }

    public boolean isInitialized() {
        return missingFields().isEmpty();
    }

    @SuppressWarnings("unchecked")
    public T assertInitialized() throws UninitializedMessageException {
        java.util.ArrayList<String> missingFields = missingFields();
        if (!missingFields.isEmpty()) {
            throw new UninitializedMessageException(missingFields);
        }
        return getThis();
    }

    @SuppressWarnings("unchecked")
    protected T checktInitialized() throws InvalidProtocolBufferException {
        java.util.ArrayList<String> missingFields = missingFields();
        if (!missingFields.isEmpty()) {
            throw new UninitializedMessageException(missingFields).asInvalidProtocolBufferException();
        }
        return getThis();
    }

    public ArrayList<String> missingFields() {
        load();
        return new ArrayList<String>();
    }

    protected void loadAndClear() {
        memoizedSerializedSize = -1;
    }

    protected void load() {
    }

    @SuppressWarnings("unchecked")
    public T mergeFrom(T other) {
        return getThis();
    }

    public void writeUnframed(CodedOutputStream output) throws java.io.IOException {
        // if (encodedForm == null) {
        // encodedForm = new byte[serializedSizeUnframed()];
        // com.google.protobuf.CodedOutputStream original = output;
        // output =
        // com.google.protobuf.CodedOutputStream.newInstance(encodedForm);
        // if (hasField1()) {
        // output.writeInt32(1, getField1());
        // }
        // if (hasField2()) {
        // output.writeInt64(2, getField2());
        // }
        // if (hasField3()) {
        // writeMessage(output, 3, getField3());
        // }
        // output.checkNoSpaceLeft();
        // output = original;
        // }
        // output.writeRawBytes(encodedForm);
    }

    // /////////////////////////////////////////////////////////////////
    // Write related helpers.
    // /////////////////////////////////////////////////////////////////

    public void writeFramed(CodedOutputStream output) throws IOException {
        output.writeRawVarint32(serializedSizeUnframed());
        writeUnframed(output);
    }

    public Buffer toUnframedBuffer() {
        try {
            int size = serializedSizeUnframed();
            BufferOutputStream baos = new BufferOutputStream(size);
            CodedOutputStream output = new CodedOutputStream(baos);
            writeUnframed(output);
            Buffer rc = baos.toBuffer();
            if( rc.length != size ) {
                throw new IllegalStateException("Did not write as much data as expected.");
            }
            return rc;
        } catch (IOException e) {
            throw new RuntimeException("Serializing to a byte array threw an IOException " + "(should never happen).", e);
        }
    }

    public Buffer toFramedBuffer() {
        try {
            int size = serializedSizeFramed();
            BufferOutputStream baos = new BufferOutputStream(size);
            CodedOutputStream output = new CodedOutputStream(baos);
            writeFramed(output);
            Buffer rc = baos.toBuffer();
            if( rc.length != size ) {
                throw new IllegalStateException("Did not write as much data as expected.");
            }
            return rc;
        } catch (IOException e) {
            throw new RuntimeException("Serializing to a byte array threw an IOException " + "(should never happen).", e);
        }
    }

    public byte[] toUnframedByteArray() {
        return toUnframedBuffer().toByteArray();
    }

    public byte[] toFramedByteArray() {
        return toFramedBuffer().toByteArray();
    }

    public void writeFramed(OutputStream output) throws IOException {
        CodedOutputStream codedOutput = new CodedOutputStream(output);
        writeFramed(codedOutput);
        codedOutput.flush();
    }

    public void writeUnframed(OutputStream output) throws IOException {
        CodedOutputStream codedOutput = new CodedOutputStream(output);
        writeUnframed(codedOutput);
        codedOutput.flush();
    }

    public int serializedSizeFramed() {
        int t = serializedSizeUnframed();
        return CodedOutputStream.computeRawVarint32Size(t) + t;

    }

    // /////////////////////////////////////////////////////////////////
    // Read related helpers.
    // /////////////////////////////////////////////////////////////////

    public T mergeFramed(CodedInputStream input) throws IOException {
        int length = input.readRawVarint32();
        int oldLimit = input.pushLimit(length);
        T rc = mergeUnframed(input);
        input.checkLastTagWas(0);
        input.popLimit(oldLimit);
        return rc;
    }

    public T mergeUnframed(Buffer data) throws InvalidProtocolBufferException {
        try {
            CodedInputStream input = new CodedInputStream(data);
            mergeUnframed(input);
            input.checkLastTagWas(0);
            return getThis();
        } catch (InvalidProtocolBufferException e) {
            throw e;
        } catch (IOException e) {
            throw new RuntimeException("An IOException was thrown (should never happen in this method).", e);
        }
    }

    @SuppressWarnings("unchecked")
    private T getThis() {
        return (T) this;
    }

    public T mergeFramed(Buffer data) throws InvalidProtocolBufferException {
        try {
            CodedInputStream input = new CodedInputStream(data);
            mergeFramed(input);
            input.checkLastTagWas(0);
            return getThis();
        } catch (InvalidProtocolBufferException e) {
            throw e;
        } catch (IOException e) {
            throw new RuntimeException("An IOException was thrown (should never happen in this method).", e);
        }
    }

    public T mergeUnframed(byte[] data) throws InvalidProtocolBufferException {
        return mergeUnframed(new Buffer(data));
    }

    public T mergeFramed(byte[] data) throws InvalidProtocolBufferException {
        return mergeFramed(new Buffer(data));
    }

    public T mergeUnframed(InputStream input) throws IOException {
        CodedInputStream codedInput = new CodedInputStream(input);
        mergeUnframed(codedInput);
        return getThis();
    }

    public T mergeFramed(InputStream input) throws IOException {
        int length = readRawVarint32(input);
        byte[] data = new byte[length];
        int pos = 0;
        while (pos < length) {
            int r = input.read(data, pos, length - pos);
            if (r < 0) {
                throw new InvalidProtocolBufferException("Input stream ended before a full message frame could be read.");
            }
            pos += r;
        }
        return mergeUnframed(data);
    }

    // /////////////////////////////////////////////////////////////////
    // Internal implementation methods.
    // /////////////////////////////////////////////////////////////////
    static protected <T> void addAll(Iterable<T> values, Collection<? super T> list) {
        if (values instanceof Collection) {
            @SuppressWarnings("unsafe")
            Collection<T> collection = (Collection<T>) values;
            list.addAll(collection);
        } else {
            for (T value : values) {
                list.add(value);
            }
        }
    }

    static protected void writeGroup(CodedOutputStream output, int tag, BaseMessage message) throws IOException {
        output.writeTag(tag, WireFormat.WIRETYPE_START_GROUP);
        message.writeUnframed(output);
        output.writeTag(tag, WireFormat.WIRETYPE_END_GROUP);
    }

    static protected <T extends BaseMessage> T readGroup(CodedInputStream input, int tag, T group) throws IOException {
        group.mergeUnframed(input);
        input.checkLastTagWas(WireFormat.makeTag(tag, WireFormat.WIRETYPE_END_GROUP));
        return group;
    }

    static protected int computeGroupSize(int tag, BaseMessage message) {
        return CodedOutputStream.computeTagSize(tag) * 2 + message.serializedSizeUnframed();
    }

    static protected void writeMessage(CodedOutputStream output, int tag, BaseMessage message) throws IOException {
        output.writeTag(tag, WireFormat.WIRETYPE_LENGTH_DELIMITED);
        message.writeFramed(output);
    }

    static protected int computeMessageSize(int tag, BaseMessage message) {
        return CodedOutputStream.computeTagSize(tag) + message.serializedSizeFramed();
    }

    protected List<String> prefix(List<String> missingFields, String prefix) {
        ArrayList<String> rc = new ArrayList<String>(missingFields.size());
        for (String v : missingFields) {
            rc.add(prefix + v);
        }
        return rc;
    }

    /**
     * Read a raw Varint from the stream. If larger than 32 bits, discard the
     * upper bits.
     */
    static public int readRawVarint32(InputStream is) throws IOException {
        byte tmp = readRawByte(is);
        if (tmp >= 0) {
            return tmp;
        }
        int result = tmp & 0x7f;
        if ((tmp = readRawByte(is)) >= 0) {
            result |= tmp << 7;
        } else {
            result |= (tmp & 0x7f) << 7;
            if ((tmp = readRawByte(is)) >= 0) {
                result |= tmp << 14;
            } else {
                result |= (tmp & 0x7f) << 14;
                if ((tmp = readRawByte(is)) >= 0) {
                    result |= tmp << 21;
                } else {
                    result |= (tmp & 0x7f) << 21;
                    result |= (tmp = readRawByte(is)) << 28;
                    if (tmp < 0) {
                        // Discard upper 32 bits.
                        for (int i = 0; i < 5; i++) {
                            if (readRawByte(is) >= 0)
                                return result;
                        }
                        throw new InvalidProtocolBufferException("CodedInputStream encountered a malformed varint.");
                    }
                }
            }
        }
        return result;
    }

    static protected byte readRawByte(InputStream is) throws IOException {
        int rc = is.read();
        if (rc == -1) {
            throw new InvalidProtocolBufferException("While parsing a protocol message, the input ended unexpectedly " + "in the middle of a field.  This could mean either than the " + "input has been truncated or that an embedded message "
                    + "misreported its own length.");
        }
        return (byte) rc;
    }

}
