/*
   D-Bus Java Implementation
   Copyright (c) 2005-2006 Matthew Johnson

   This program is free software; you can redistribute it and/or modify it
   under the terms of either the GNU Lesser General Public License Version 2 or the
   Academic Free Licence Version 2.1.

   Full licence texts are included in the COPYING file with this program.
*/
package org.freedesktop.dbus;

import static org.freedesktop.dbus.Gettext._;

import java.lang.reflect.Constructor;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.MessageFormatException;

import cx.ath.matthew.debug.Debug;

public class DBusSignal extends Message
{
   DBusSignal() { }
   public DBusSignal(String source, String path, String iface, String member, String sig, Object... args) throws DBusException
   {
      super(Message.Endian.BIG, Message.MessageType.SIGNAL, (byte) 0);

      if (null == path || null == member || null == iface)
         throw new MessageFormatException(_("Must specify object path, interface and signal name to Signals."));
      headers.put(Message.HeaderField.PATH,path);
      headers.put(Message.HeaderField.MEMBER,member);
      headers.put(Message.HeaderField.INTERFACE,iface);

      Vector<Object> hargs = new Vector<Object>();
      hargs.add(new Object[] { Message.HeaderField.PATH, new Object[] { ArgumentType.OBJECT_PATH_STRING, path } });
      hargs.add(new Object[] { Message.HeaderField.INTERFACE, new Object[] { ArgumentType.STRING_STRING, iface } });
      hargs.add(new Object[] { Message.HeaderField.MEMBER, new Object[] { ArgumentType.STRING_STRING, member } });
      
      if (null != source) {
         headers.put(Message.HeaderField.SENDER,source);
         hargs.add(new Object[] { Message.HeaderField.SENDER, new Object[] { ArgumentType.STRING_STRING, source } });
      }
 
      if (null != sig) {
         hargs.add(new Object[] { Message.HeaderField.SIGNATURE, new Object[] { ArgumentType.SIGNATURE_STRING, sig } });
         headers.put(Message.HeaderField.SIGNATURE,sig);
         setArgs(args);
      }

      blen = new byte[4];
      appendBytes(blen);
      append("ua(yv)", ++serial, hargs.toArray());
      pad((byte)8);

      long c = bytecounter;
      if (null != sig) append(sig, args);
      marshallint(bytecounter-c, blen, 0, 4);
      bodydone = true;
   }
   static class internalsig extends DBusSignal
   {
      public internalsig(String source, String objectpath, String type, String name, String sig, Object[] parameters, long serial) throws DBusException
      {
         super(source, objectpath, type, name, sig, parameters, serial);
      }
   }
   private static Map<Class<? extends DBusSignal>, Type[]> typeCache = new HashMap<Class<? extends DBusSignal>, Type[]>();
   private static Map<String, Class<? extends DBusSignal>> classCache = new HashMap<String, Class<? extends DBusSignal>>();
   private static Map<Class<? extends DBusSignal>, Constructor<? extends DBusSignal>> conCache = new HashMap<Class<? extends DBusSignal>, Constructor<? extends DBusSignal>>();
   private static Map<String, String> signames = new HashMap<String, String>();
   private static Map<String, String> intnames = new HashMap<String, String>();
   private Class<? extends DBusSignal> c;
   private boolean bodydone = false;
   private byte[] blen;

   static void addInterfaceMap(String java, String dbus)
   {
      intnames.put(dbus, java);
   }
   static void addSignalMap(String java, String dbus)
   {
      signames.put(dbus, java);
   }
   
   static DBusSignal createSignal(Class<? extends DBusSignal> c, String source, String objectpath, String sig, long serial, Object... parameters) throws DBusException
   {
      String type = "";
      if (null != c.getEnclosingClass()) {
         if (null != c.getEnclosingClass().getAnnotation(DBusInterfaceName.class))
            type = c.getEnclosingClass().getAnnotation(DBusInterfaceName.class).value();
         else
            type = AbstractConnection.dollar_pattern.matcher(c.getEnclosingClass().getName()).replaceAll(".");

      } else
         throw new DBusException(_("Signals must be declared as a member of a class implementing DBusInterface which is the member of a package."));
      DBusSignal s = new internalsig(source, objectpath, type, c.getSimpleName(), sig, parameters, serial);
      s.c = c;
      return s;
   }
   @SuppressWarnings("unchecked")
   private static Class<? extends DBusSignal> createSignalClass(String intname, String signame) throws DBusException
   {
		String name = intname+'$'+signame;
      Class<? extends DBusSignal> c = classCache.get(name);
		if (null == c) c = DBusMatchRule.getCachedSignalType(name);
		if (null != c) return c;
      do {
         try {
            c = (Class<? extends DBusSignal>) Class.forName(name);
         } catch (ClassNotFoundException CNFe) {}
         name = name.replaceAll("\\.([^\\.]*)$", "\\$$1");
      } while (null == c && name.matches(".*\\..*"));
		if (null == c) 
			throw new DBusException(_("Could not create class from signal ")+intname+'.'+signame);
		classCache.put(name, c);
      return c;
   }
   @SuppressWarnings("unchecked")
   DBusSignal createReal(AbstractConnection conn) throws DBusException
   {
      String intname = intnames.get(getInterface());
      String signame = signames.get(getName());
      if (null == intname) intname = getInterface();
      if (null == signame) signame = getName();
      if (null == c) 
         c = createSignalClass(intname,signame);
      if (Debug.debug) Debug.print(Debug.DEBUG, "Converting signal to type: "+c);
      Type[] types = typeCache.get(c);
      Constructor<? extends DBusSignal> con = conCache.get(c);
      if (null == types) {
         con = (Constructor<? extends DBusSignal>) c.getDeclaredConstructors()[0];
         conCache.put(c, con);
         Type[] ts = con.getGenericParameterTypes();
         types = new Type[ts.length-1];
         for (int i = 1; i < ts.length; i++)
            if (ts[i] instanceof TypeVariable)
               for (Type b: ((TypeVariable<GenericDeclaration>) ts[i]).getBounds())
                  types[i-1] = b;
            else
               types[i-1] = ts[i];
         typeCache.put(c, types);
      }

      try {
         DBusSignal s;
         Object[] args = Marshalling.deSerializeParameters(getParameters(), types, conn);
         if (null == args) s = (DBusSignal) con.newInstance(getPath());
         else {
            Object[] params = new Object[args.length + 1];
            params[0] = getPath();
            System.arraycopy(args, 0, params, 1, args.length);

            if (Debug.debug) Debug.print(Debug.DEBUG, "Creating signal of type "+c+" with parameters "+Arrays.deepToString(params));
            s = (DBusSignal) con.newInstance(params);
         }
         s.headers = headers;
         s.wiredata = wiredata;
         s.bytecounter = wiredata.length;
         return s;
      } catch (Exception e) { 
         if (AbstractConnection.EXCEPTION_DEBUG && Debug.debug) Debug.print(Debug.ERR, e);
         throw new DBusException(e.getMessage());
      }
   }
   /** 
    * Create a new signal.
    * This contructor MUST be called by all sub classes.
    * @param objectpath The path to the object this is emitted from.
    * @param args The parameters of the signal.
    * @throws DBusException This is thrown if the subclass is incorrectly defined.
    */
   @SuppressWarnings("unchecked")
   protected DBusSignal(String objectpath, Object... args) throws DBusException
   {
      super(Message.Endian.BIG, Message.MessageType.SIGNAL, (byte) 0);

      if (!objectpath.matches(AbstractConnection.OBJECT_REGEX)) throw new DBusException(_("Invalid object path: ")+objectpath);

      Class<? extends DBusSignal> tc = getClass();
      String member;
      if (tc.isAnnotationPresent(DBusMemberName.class))
         member = tc.getAnnotation(DBusMemberName.class).value();
      else
         member = tc.getSimpleName();
      String iface = null;
      Class<? extends Object> enc = tc.getEnclosingClass();
      if (null == enc ||
            !DBusInterface.class.isAssignableFrom(enc) ||
            enc.getName().equals(enc.getSimpleName()))
         throw new DBusException(_("Signals must be declared as a member of a class implementing DBusInterface which is the member of a package."));
      else
         if (null != enc.getAnnotation(DBusInterfaceName.class))
            iface = enc.getAnnotation(DBusInterfaceName.class).value();
         else
            iface = AbstractConnection.dollar_pattern.matcher(enc.getName()).replaceAll(".");

      headers.put(Message.HeaderField.PATH,objectpath);
      headers.put(Message.HeaderField.MEMBER,member);
      headers.put(Message.HeaderField.INTERFACE,iface);

      Vector<Object> hargs = new Vector<Object>();
      hargs.add(new Object[] { Message.HeaderField.PATH, new Object[] { ArgumentType.OBJECT_PATH_STRING, objectpath } });
      hargs.add(new Object[] { Message.HeaderField.INTERFACE, new Object[] { ArgumentType.STRING_STRING, iface } });
      hargs.add(new Object[] { Message.HeaderField.MEMBER, new Object[] { ArgumentType.STRING_STRING, member } });

      String sig = null;
      if (0 < args.length) {
         try {
            Type[] types = typeCache.get(tc);
            if (null == types) {
               Constructor<? extends DBusSignal> con = (Constructor<? extends DBusSignal>) tc.getDeclaredConstructors()[0];
               conCache.put(tc, con);
               Type[] ts = con.getGenericParameterTypes();
               types = new Type[ts.length-1];
               for (int i = 1; i <= types.length; i++) 
                  if (ts[i] instanceof TypeVariable)
                     types[i-1] = ((TypeVariable<GenericDeclaration>) ts[i]).getBounds()[0];
                  else
                     types[i-1] = ts[i];
               typeCache.put(tc, types);
            }
            sig = Marshalling.getDBusType(types);
            hargs.add(new Object[] { Message.HeaderField.SIGNATURE, new Object[] { ArgumentType.SIGNATURE_STRING, sig } });
            headers.put(Message.HeaderField.SIGNATURE,sig);
            setArgs(args);
         } catch (Exception e) {
            if (AbstractConnection.EXCEPTION_DEBUG && Debug.debug) Debug.print(Debug.ERR, e);
            throw new DBusException(_("Failed to add signal parameters: ")+e.getMessage());
         }
      }

      blen = new byte[4];
      appendBytes(blen);
      append("ua(yv)", ++serial, hargs.toArray());
      pad((byte)8);
   }
   void appendbody(AbstractConnection conn) throws DBusException
   {
      if (bodydone) return;

      Type[] types = typeCache.get(getClass());
      Object[] args = Marshalling.convertParameters(getParameters(), types, conn);
      setArgs(args);
      String sig = getSig();

      long c = bytecounter;
      if (null != args && 0 < args.length) append(sig, args);
      marshallint(bytecounter-c, blen, 0, 4);
      bodydone = true;
   }
}
