/*
 *
 * This file is part of GNUnet.
 * (C) 2011 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 2, 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.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;


/**
 * Load message maps, which contain the information the parse/write unions.
 */
public class MessageLoader {
    private static final Logger logger = LoggerFactory
            .getLogger(MessageLoader.class);


    /**
     * Thrown when trying to serialize an object that is not registered as a union type.
     */
    public static class UnknownUnionException extends RuntimeException {
        public UnknownUnionException(String msg) {
            super(msg);
        }
    }


    /**
     * Thrown when parsing a union whose ID is not known.
     */
    public static class UnknownUnionIdException extends RuntimeException {

    }

    /**
     * Maps a class and tag to the corresponding union case.
     * <p/>
     * XXX: how much of generics is too much?
     */
    private static Map<Class<? extends MessageUnion>, Map<Integer, Class<? extends MessageUnion>>> unionmap
            = new HashMap<Class<? extends MessageUnion>, Map<Integer, Class<? extends MessageUnion>>>(100);

    /*
     * Maps a union interface and union case to the corresponding tag.
     */
    private static Map<Class<? extends MessageUnion>, Map<Class<? extends MessageUnion>, Integer>> tagmap
            = new HashMap<Class<? extends MessageUnion>, Map<Class<? extends MessageUnion>, Integer>>(100);


    static {
        ClassLoader classLoader = MessageLoader.class.getClassLoader();
        Enumeration<URL> resources;
        try {
            resources = classLoader.getResources("org/gnunet/construct/MsgMap.txt");
        } catch (IOException e) {
            throw new RuntimeException("something went wrong with loading MsgMap.txt");
        }

        while (resources.hasMoreElements()) {
            loadMessageMap(resources.nextElement());
        }

        if (tagmap.isEmpty()) {
            logger.warn("message map empty");
        }

    }

    public static void loadMessageMap(URL loc) {
        if (loc == null) {
            throw new RuntimeException("could not load message map");
        }
        BufferedReader in = null;
        try {
            in = new BufferedReader(new InputStreamReader(loc.openStream(), Charsets.UTF_8));
            String line;
            while ((line = in.readLine()) != null) {
                // skip empty lines and comments
                if (line.isEmpty() || line.charAt(0) == '#') {
                    continue;
                }
                String[] m = line.split("=");
                if (m.length != 2) {
                    throw new RuntimeException("invalid message map format (separation by '=')");
                }
                String[] left = m[0].split("[|]");
                if (left.length != 2) {
                    logger.debug(m[0]);
                    logger.debug(m[1]);
                    logger.debug("split in " + left.length);
                    throw new RuntimeException("invalid message map format (left hand side)");
                }
                int id = java.lang.Integer.parseInt(left[1].trim());
                String unionCaseName = m[1].trim();
                String unionInterfaceName = left[0];

                Class<? extends MessageUnion> unionInterface = loadClass(unionInterfaceName);
                Class<? extends MessageUnion> unionCase = loadClass(unionCaseName);

                if (!unionmap.containsKey(unionInterface)) {
                    unionmap.put(unionInterface, new HashMap<Integer, Class<? extends MessageUnion>>(5));
                }
                unionmap.get(unionInterface).put(id, unionCase);


                if (!tagmap.containsKey(unionInterface)) {
                    tagmap.put(unionInterface, new HashMap<Class<? extends MessageUnion>, Integer>(5));
                }
                tagmap.get(unionInterface).put(unionCase, id);

            }
        } catch (IOException e) {
            throw new RuntimeException("could not read message map");
        } finally {
            maybeClose(in);
        }
    }

    private static void maybeClose(Closeable in) {
        try {
            if (in != null) {
                in.close();
            }
        } catch (IOException e) {
            throw new RuntimeException("error closing stream: " + e.getMessage());
        }
    }


    @SuppressWarnings("unchecked")
    private static Class<? extends MessageUnion> loadClass(String className) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        Class<MessageUnion> msgClass;
        try {
            msgClass = (Class<MessageUnion>) cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new AssertionError(String.format("message class '%s' not found in classpath", className));
        } catch (ClassCastException e) {
            throw new AssertionError(String.format("Class %s does not inherit from MessageUnion", className));
        }
        return msgClass;
    }

    public static Class<? extends MessageUnion> getUnionClass(Class<? extends MessageUnion> unionInterface, int tag) {
        Map<Integer, Class<? extends MessageUnion>> map = unionmap.get(unionInterface);
        if (map == null) {
            throw new UnknownUnionException("don't know how to handle unions of type '" + unionInterface + "'");
        }

        Class<? extends MessageUnion> cls = map.get(tag);
        if (cls == null) {
            throw new ProtocolViolationException("don't know how to translate message of type " + tag);
        }

        return cls;
    }


    public static int getUnionTag(Class<? extends MessageUnion> unionInterface, Class<? extends MessageUnion> unionCase) {
        Map<Class<? extends MessageUnion>, Integer> map = tagmap.get(unionInterface);
        if (map == null) {
            throw new AssertionError(String.format("%s is not a known union type", unionInterface));
        }
        if (!map.containsKey(unionCase)) {
            throw new AssertionError(String.format("%s is not a known instance of %s", unionCase, unionInterface));
        }
        return map.get(unionCase);
    }

    public static Class<? extends MessageUnion>[] getUnionCases(Class<? extends MessageUnion> unionInterface) {
        Map<Class<? extends MessageUnion>, Integer> map = tagmap.get(unionInterface);
        //noinspection unchecked
        return (Class<? extends MessageUnion>[]) map.keySet().toArray(new Class[map.keySet().size()]);
    }

    public static void registerUnionCase(Class<? extends MessageUnion> unionInterface,
                                         Class<? extends MessageUnion> unionCase, int tag) {
        if (!unionmap.containsKey(unionInterface)) {
            unionmap.put(unionInterface, new HashMap<Integer, Class<? extends MessageUnion>>(5));
        }
        unionmap.get(unionInterface).put(tag, unionCase);


        if (!tagmap.containsKey(unionInterface)) {
            tagmap.put(unionInterface, new HashMap<Class<? extends MessageUnion>, Integer>(5));
        }
        tagmap.get(unionInterface).put(unionCase, tag);


    }
}
