/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.construct;

import com.google.common.base.Preconditions;
import org.gnunet.construct.parsers.*;
import org.grothoff.Runabout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.*;


/*
Wanted syntax (not fully implemented yet)
- @(U)Int<n> => signed or unsigned fixnum, represented by n bits
- @NestedMessage => nested message
- @FillWith @(U)Int<n> => fill the rest of the message with the specified fixnum, annotation valid on primitive arrays
- @FillWith @NestedMessage => fill the rest of the message with the specified fixnum, annotation valid on message arrays
 of the wanted type
- @VariableSizeArray(lengthField = "<field>") => same syntax as @FillWith
- @FixedSizeArray(length = n) => same syntax as @FillWith
- @DoubleValue / @Float => floating point number, should also work with the array annotations
- @FrameSize => specifies the fixnum that determines the containing frame's size
- @ZeroTerminatedString => self-explanatory
- @Constructable => annotation on a class that implements the ConstructableMessage interface,
 providing methods to serialize/unserialize itself.
*/


/**
 * Parse and write the binary representation of java classes, as defined by org.gnunet.construct.*-annotations
 * on their members.
 *
 * @author Christian Grothoff
 * @author Florian Dold
 */
@SuppressWarnings("unchecked")
public class Construct {
    private static final Logger logger = LoggerFactory
            .getLogger(Construct.class);


    private static Map<Class<? extends Message>, Parser> parserCache = new HashMap<Class<? extends Message>,
            Parser>(100);


    /**
     * The class Construct is not intended to be instantiated, this its constructor is private.
     */
    private Construct() {

    }


    /**
     * Given a byte buffer with a message, parse it into an object of type c. The
     * fields of the class are expected to be annotated with annotations from
     * the construct package.
     *
     * @param srcBuf buffer with the serialized binary data
     * @param c      desired object type to return
     * @return instance of the desired object type
     */
    public static <T extends Message> T parseAs(ByteBuffer srcBuf, Class<T> c) {
        try {
            T m = ReflectUtil.justInstantiate(c);

            try {
                getParser(c).parse(srcBuf, 0, m, m, null);
            } catch (ProtocolViolationException e) {
                e.augmentPath("on " + c);
            }

            return m;
        } catch (Error e) {
            System.err.println(
                    String.format("Exception while parsing message for class '%s':", c.getCanonicalName()));
            throw e;
        }
    }

    /**
     * Given a byte array with a message, parse it into an object of type c. The
     * fields of the class are expected to be annotated with annotations from
     * the construct package.
     *
     * @param srcBuf buffer with the serialized binary data
     * @param c      desired object type to return
     * @return instance of the desired object type
     */
    public static <T extends Message> T parseAs(byte[] srcBuf, Class<T> c) {
        return parseAs(ByteBuffer.wrap(srcBuf), c);
    }

    /**
     * Create a Parser for a sub-class of Message. The result is always cached.
     *
     * @param c annotated sub-class of message
     * @return a parser
     */
    public static Parser getParser(Class<? extends Message> c) {

        if (parserCache.containsKey(c)) {
            return parserCache.get(c);
        }

        Parser p = getParser(c, new ParserGenerator());

        parserCache.put(c, p);

        return p;
    }

    private static List<Field> getMessageFields(Class c) {
        LinkedList<Field> fields = new LinkedList<Field>(Arrays.asList(c.getDeclaredFields()));
        while ((c = c.getSuperclass()) != null && Message.class.isAssignableFrom(c)) {
            // fields of the superclass have to be parsed *before* the subclass
            fields.addAll(0, Arrays.asList(c.getDeclaredFields()));
        }
        return fields;
    }

    private static Parser getParser(Class<? extends Message> c,
                                    final ParserGenerator pg) {

        Preconditions.checkNotNull(c);
        SequenceParser parser = new SequenceParser();

        if (!Modifier.isPublic(c.getModifiers())) {
            throw new AssertionError(String.format("Construct Message %s not declared public", c));
        }

        for (Field f : getMessageFields(c)) {
            pg.c = c;
            Annotation[] as = f.getAnnotations();
            if (as.length == 0 || f.isSynthetic() || Modifier.isStatic(f.getModifiers())) {
                continue;
            }
            if (!Modifier.isPublic(f.getModifiers())) {
                throw new AssertionError(String.format("Field %s of Message %s not declared public", f, c));
            }
            pg.field = f;
            pg.annotations = as;
            pg.annotationsIdx = 0;

            pg.visitAppropriate(as[0]);

            parser.add(pg.parser);
        }

        parser.setFrameSizePath(pg.frameSizePath);

        return parser;
    }

    @SuppressWarnings("UnusedDeclaration")
    static class ParserGenerator extends Runabout {

        // the field we are currently generating a parser for
        Field field;
        // all annotations on the field
        Annotation[] annotations;
        // the index of the annotation we are supposed to process right now
        int annotationsIdx;

        // the message class for which the parser is generated
        Class c;

        // the parser we are actually generating, used by the caller, set as
        // return value of
        // the runabout invocation
        Parser parser;

        // where are we currently, seen from the root message object
        List<Field> path = new LinkedList<Field>();

        // path of the object that has a frame size field
        List<Field> frameSizePath;

        private ParserGenerator() {
        }

        public void visit(Union u) {
            parser = new UnionParser(u.optional(), (Class<MessageUnion>) field.getType(),
                    ReflectUtil.getFieldPathFromString(u.tag(), c), field);
        }

        public void visit(FrameSize ts) {

            frameSizePath = new LinkedList<Field>(path);
            frameSizePath.add(field);

            if (annotationsIdx != 0) {
                throw new AssertionError(
                        "FrameSize must be the first annotation on a Field");
            }

            annotationsIdx++;
            if (annotationsIdx >= annotations.length) {
                throw new AssertionError(
                        "FrameSize must be followed by an numeric parser");
            }
            visitAppropriate(annotations[annotationsIdx]);

        }

        public void visit(UInt8 i) {
            parser = new IntegerParser(1, IntegerParser.UNSIGNED, field);
        }

        public void visit(UInt16 i) {
            parser = new IntegerParser(2, IntegerParser.UNSIGNED, field);
        }

        public void visit(UInt32 i) {
            parser = new IntegerParser(4, IntegerParser.UNSIGNED, field);
        }

        public void visit(UInt64 i) {
            parser = new IntegerParser(8, IntegerParser.UNSIGNED, field);
        }

        public void visit(Int8 i) {
            parser = new IntegerParser(1, IntegerParser.SIGNED, field);
        }

        public void visit(Int16 i) {
            parser = new IntegerParser(2, IntegerParser.SIGNED, field);
        }

        public void visit(Int32 i) {
            parser = new IntegerParser(4, IntegerParser.SIGNED, field);
        }

        public void visit(Int64 i) {
            parser = new IntegerParser(8, IntegerParser.SIGNED, field);
        }


        public void visit(ZeroTerminatedString zts) {
            parser = new StringParser(zts.charset(), zts.optional(), field);
        }

        public void visit(IntegerFill i) {
            parser = new IntegerFillParser(field, i.signed(), i.bitSize() / 8);
        }

        public void visit(NestedMessage n) {
            if (!Message.class.isAssignableFrom(field.getType())) {
                throw new AssertionError("@NestedMessage only works on messages, " + field.getType()
                        + " is not a message (origin: " + c + ")");
            }

            Field nestedField = field;

            if (n.newFrame()) {
                Parser p = getParser((Class<Message>) nestedField.getType());

                parser = new NestedParser(p, n.optional(), nestedField, true);

            } else {
                Field oldField = field;
                List<Field> oldPath = new ArrayList<Field>(path);
                Class oldClass = c;

                path.add(field);

                Parser p = getParser((Class<Message>) nestedField.getType(), this);

                path = oldPath;
                c = oldClass;

                parser = new NestedParser(p, n.optional(), oldField, false);
            }
        }

        public void visit(FixedSizeArray fsa) {
            Field f = field;
            int elemNumber = fsa.length();

            getParser((Class<? extends Message>) field.getType()
                    .getComponentType(), this);

            parser = new FixedSizeArrayParser(elemNumber, parser, f);
        }


        public void visit(FixedSizeIntegerArray fsa) {
            Field f = field;
            int elemNumber = fsa.length();
            parser = new FixedSizeIntegerArrayParser(elemNumber, fsa.signed(), fsa.bitSize() / 8, f);
        }

        public void visit(DoubleValue d) {
            if (!field.getType().equals(java.lang.Double.TYPE)) {
                throw new AssertionError("@DoubleValue target must be a primitive 'double' field");
            }
            parser = new DoubleParser(field);
        }

        public void visit(FillWith fw) {
            annotationsIdx++;
            // if there's no further annotation, act like there is @Nested
            if (annotationsIdx >= annotations.length) {
                Parser p = getParser((Class<? extends Message>) field.getType().getComponentType());
                parser = new FillParser(p, field);
            } else {
                FillWithParserRunabout r = new FillWithParserRunabout(field);
                r.visitAppropriate(annotations[annotationsIdx]);
                if (r.p == null) {
                    throw new AssertionError();
                }
                parser = r.p;
            }
        }

        public void visit(VariableSizeArray vsa) {
            Parser p = getParser((Class<? extends Message>) field.getType()
                    .getComponentType());

            if (!Message.class.isAssignableFrom(field.getType().getComponentType())) {
                throw new AssertionError("VariableSizeArray only valid on arrays of messages.");
            }

            try {
                parser = new VariableSizeArrayParser(p, c.getField(vsa
                        .lengthField()), field);

            } catch (SecurityException e) {
                throw new AssertionError(
                        String.format(
                                "VariableSizeArray: length field '%s' not declared public",
                                vsa.lengthField()));
            } catch (NoSuchFieldException e) {
                throw new AssertionError(String.format(
                        "VariableSizeArray: length field '%s' does not exist in class %s",
                        vsa.lengthField(), c));
            }
        }

        public void visit(VariableSizeString vss) {
            try {
                parser = new VariableSizeStringParser(vss.terminationType(), c.getField(vss
                        .lengthField()), field);

            } catch (SecurityException e) {
                throw new AssertionError(
                        String.format(
                                "VariableSizeString: length field '%s' not declared public",
                                vss.lengthField()));
            } catch (NoSuchFieldException e) {
                throw new AssertionError(String.format(
                        "VariableSizeString: length field '%s' does not exist in class %s",
                        vss.lengthField(), c));
            }
        }


        public void visit(VariableSizeIntegerArray a) {
            try {
                parser = new VariableSizeIntegerArrayParser(c.getField(a.lengthField()), field, a.signed(), a.bitSize() / 8);
            } catch (NoSuchFieldException e) {
                throw new AssertionError(String.format(
                        "VariableSizeIntegerArray: length field '%s' does not exist in class %s",
                        a.lengthField(), c));
            }
        }

        /*
         * We override this to improve the error message, otherwise obfuscated by internal java proxy objects
         */
        @Override
        public void visitDefault(Object obj) {
            if (obj instanceof Annotation) {
                Annotation ann = (Annotation) obj;
                throw new AssertionError("invalid Construct annotation: " + ann.annotationType().getName());
            } else {
                throw new AssertionError();
            }
        }
    }


    private static class FillWithParserRunabout extends Runabout {
        public Parser p;
        private Field f;

        public FillWithParserRunabout(Field f) {
            this.f = f;
        }

        public void visit(Int8 x) {
            p = new IntegerFillParser(f, true, 1);
        }
        public void visit(Int16 x) {
            p = new IntegerFillParser(f, true, 2);
        }
        public void visit(Int32 x) {
            p = new IntegerFillParser(f, true, 4);
        }
        public void visit(UInt8 x) {
            p = new IntegerFillParser(f, false, 1);
        }
        public void visit(UInt16 x) {
            p = new IntegerFillParser(f, false, 2);
        }
        public void visit(UInt32 x) {
            p = new IntegerFillParser(f, false, 4);
        }
        public void visit(NestedMessage n) {
            Parser componentParser = getParser((Class<? extends Message>) f.getType().getComponentType());
            p = new FillParser(componentParser, f);
        }

    }

    /**
     * Serialize a given message object to a binary byte array. The fields of
     * the object are expected to be annotated with annotations from the
     * construct package.
     *
     * @param dstBuf where to write the binary object data
     * @param msg    object to serialize
     * @return number of bytes written to data, -1 on error
     */
    public static int write(ByteBuffer dstBuf, Message msg) {
        Parser p = getParser(msg.getClass());
        return p.write(dstBuf, msg);
    }

    /**
     * Compute the exact size of a serialized message.
     *
     * @param m object to serialize
     * @return number of bytes required to store the message in binary form
     */
    public static int getSize(Message m) {
        if (m == null) {
            return 0;
        }
        Parser p = getParser(m.getClass());
        return p.getSize(m);
    }


    /**
     * Return the binary representation of the message m
     *
     * @param m the message to serialize
     * @return a byte array containing the serialized message
     */
    public static byte[] toBinary(Message m) {
        byte[] a = new byte[getSize(m)];
        ByteBuffer buf = ByteBuffer.wrap(a);
        write(buf, m);
        return a;
    }

    /**
     * Fill in all fields of a message that are inferable from existing information.
     *
     * Examples: The size field for variable size arrays, the type of unions, ...
     *
     * @param m the message that should be patched
     */
    public static void patch(Message m) {
        Parser p = getParser(m.getClass());
        p.patch(m, p.getSize(m), null, m);
    }

    /**
     * Get the minimum static size for the message, determinable even if the message's members
     * are not filled in.
     *
     * @param m the message of interest
     * @return the static minimum size of the message
     */
    public static int getStaticSize(Message m) {
        Parser p = getParser(m.getClass());
        return p.getStaticSize();
    }

}
