/*
 * ApplicationVMManager.java  $Revision: 1.3 $  $Date: 2007/10/03 04:39:39 $
 *
 * Copyright (C) 2007 Meldcraft Corporation
 *
 * This file is part of Scaffold.
 * 
 * Scaffold is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Scaffold 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Scaffold.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.meldcraft.application.container;

import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.util.StringTokenizer;
import java.util.logging.Level;

import com.meldcraft.util.ConfigurationManager;
import com.meldcraft.util.PropertiesHelper;
import com.meldcraft.util.StringUtil;
import com.meldcraft.util.logging.LogUtil;

/**
 * @version $Revision: 1.3 $ $Date: 2007/10/03 04:39:39 $
 * @author rob
 */
public class ApplicationVMManager
{
// Static variables
    public static final int     MIN_PORT = PropertiesHelper.getInt(ConfigurationManager.getConfiguration(ApplicationVMManager.class), "minPort", 36600);
    public static final int     MAX_PORT = MIN_PORT + Math.abs(PropertiesHelper.getInt(ConfigurationManager.getConfiguration(ApplicationVMManager.class), "portRange", 100));
    public static final String  LOCAL_HOST = "127.0.0.1";
    public static final String  RESPONSE = "200 OK";

// Member variables
    private SocketHandler   mSocketHandler;
    private int             mPort;
    private String          mVMName;

// Constructors and initialization
    public ApplicationVMManager(String vmName)
    {
        mVMName = vmName;
        initialize();
    }

// Interface implemenations
// Public methods
    public boolean openRemoteApplicationInstance(String appName, String[] args)
    {
        boolean ret = false;

        for (int i=mPort-1 ; i >= MIN_PORT ; i--)
        {
            try
            {
                ret = openRemoteApplicationInstance(i, appName, args);
                if (ret)
                {
                    break;
                }
            }
            catch (Throwable e)
            {
                LogUtil.getLogger(this).log(Level.WARNING, "ApplicationVMManager: Error opening external application.", e);
            }
        }

        return ret;
    }

// Protected methods
    protected void initialize()
    {
        for (mPort = MIN_PORT ; mPort <= MAX_PORT ; mPort++)
        {
            try
            {
                ServerSocket sock = ServerSocketChannel.open().socket();
                sock.bind(new InetSocketAddress(LOCAL_HOST, mPort));
                mSocketHandler = new SocketHandler(sock, mVMName);
                Thread th = new Thread(mSocketHandler, "ApplicationVMManager-Thread");
                th.start();
                break;
            }
            catch (Exception e)
            {
                // ignored - try next port
            }
        }
    }

    protected boolean openRemoteApplicationInstance(int port, String appName, String[] args)
        throws IOException
    {
        boolean ret = false;

        Socket sock = new Socket(LOCAL_HOST, port);

        String cmd = AppInfo.encode(mVMName, appName, args);
        BufferedWriter writer = null;
        BufferedReader reader = null;
        try
        {
            writer = getOutput(sock);
            writer.write(cmd);
            writer.write("\n");
            writer.flush();

            reader = getInput(sock);
            String str = reader.readLine();

            if (str != null)
            {
                str = str.trim();
                LogUtil.getLogger(this).info("ApplicationVMManager: Response: "+str);
                if (str.equals(RESPONSE))
                {
                    ret = true;
                }
            }
        }
        finally
        {
            try
            {
                if (writer != null)
                {
                    writer.close();
                }
            }
            catch (Throwable e)
            {
            }

            try
            {
                if (reader != null)
                {
                    reader.close();
                }
            }
            catch (Throwable e)
            {
            }

            try
            {
                if (sock != null)
                {
                    sock.close();
                }
            }
            catch (Throwable e)
            {
            }
        }


        return ret;
    }

    protected static BufferedReader getInput(Socket sock)
        throws IOException, UnsupportedEncodingException
    {
        BufferedReader reader = new BufferedReader(
                                    new InputStreamReader(
                                        sock.getInputStream(),
                                        "US-ASCII"));
        return reader;
    }

    protected static BufferedWriter getOutput(Socket sock)
        throws IOException, UnsupportedEncodingException
    {
        BufferedWriter writer = new BufferedWriter(
                                    new OutputStreamWriter(
                                        sock.getOutputStream(),
                                        "US-ASCII"));
        return writer;
    }

// Private methods
// Inner classes
    public static class SocketHandler implements Runnable
    {
        private ServerSocket    mSocket;
        private String          mVMName;
        public SocketHandler(ServerSocket sock, String vmName)
        {
            mSocket = sock;
            mVMName = vmName;
        }

        public void run()
        {
            while(true)
            {
                Socket sock = null;
                try
                {
                    LogUtil.getLogger(this).info("ApplicationVMManager: Listening for remote application " + mVMName + " on "+mSocket.getLocalPort());
                    sock = mSocket.accept();

                    BufferedReader reader = getInput(sock);
                    String str = reader.readLine();
                    str = str.trim();

                    AppInfo info = AppInfo.decode(mVMName, str);
                    ApplicationManager.openApp(info.getAppName(), info.getAppArgs());
                    LogUtil.getLogger(this).info("ApplicationVMManager: opened application: " + info);

                    BufferedWriter writer = getOutput(sock);
                    writer.write(RESPONSE);
                    writer.write("\n");
                    writer.flush();
                    LogUtil.getLogger(this).info("ApplicationVMManager: sent response: " + RESPONSE);
                }
                catch (ProtocolException e)
                {
                    LogUtil.getLogger(this).log(Level.WARNING, "ApplicationVMManager: Error handling external command: " + StringUtil.trim(e.getMessage()));
                }
                catch (Throwable t)
                {
                    LogUtil.getLogger(this).log(Level.WARNING, "ApplicationVMManager: Error handling external command.", t);
                }
                finally
                {
                    try
                    {
                        if (sock != null)
                        {
                            sock.close();
                        }
                    }
                    catch (Throwable e)
                    {
                        LogUtil.getLogger(this).log(Level.WARNING, "ApplicationVMManager: Error sending response.", e);
                    }
                }
            }
        }
    }

    public static class AppInfo
    {
        private static String   ENCODING = "UTF8";

        private String      mVMName;
        private String      mAppName;
        private String[]    mAppArgs;

        public AppInfo(String vmName, String name, String[] args)
        {
            mVMName = vmName;
            mAppName = name;
            mAppArgs = args;
        }

        public String getVMName()
        {
            return mVMName;
        }

        public String getAppName()
        {
            return mAppName;
        }

        public String[] getAppArgs()
        {
            return mAppArgs;
        }

        public static String encode(String vmName, String appName, String[] args)
        {
            String[] info = null;
            if (args == null)
            {
                info = new String[] {vmName, appName};
            }
            else
            {
                info = new String[args.length + 2];
                info[0] = vmName;
                info[1] = appName;
                System.arraycopy(args,0, info,2,args.length);
            }

            return encode(info);
        }

        public static AppInfo decode(String vmName, String infoStr)
            throws ProtocolException
        {
            AppInfo ret = null;

            if (!infoStr.startsWith("/"))
            {
                throw new ProtocolException("Not a valid message: "+infoStr);
            }

            int idx = infoStr.indexOf('?');
            if (idx <= 0)
            {
                throw new ProtocolException("Not a valid message: "+infoStr);
            }

            String prefix = infoStr.substring(1, idx);
            String argstr = "";
            if (idx < infoStr.length()-1)
            {
                argstr = infoStr.substring(idx+1, infoStr.length());
            }

            idx = prefix.indexOf("/");
            if ((idx <= 0) || (idx >= prefix.length()-1))
            {
                throw new ProtocolException("Not a valid message: "+infoStr);
            }

            int idx2 = prefix.indexOf("/", idx+1);
            if ((idx2 <= 0) || (idx2 >= prefix.length()-1))
            {
                throw new ProtocolException("Not a valid message: "+infoStr);
            }

            try
            {
                String user = prefix.substring(0, idx);
                String avmName = prefix.substring(idx+1, idx2);
                String appName = prefix.substring(idx2+1, prefix.length());
                if (!avmName.equals(vmName))
                {
                    throw new ProtocolException("Not same vmName: " + vmName + " " + avmName);
                }
                else if (user.equals(getUserName()))
                {
                    StringTokenizer tok = new StringTokenizer(argstr, "&");
                    String[] appArgs = new String[tok.countTokens()];
                    int cnt=0;
                    while(tok.hasMoreTokens())
                    {
                        String str = tok.nextToken();
                        appArgs[cnt++] = URLDecoder.decode(str, ENCODING);
                    }

                    ret = new AppInfo(avmName, appName, appArgs);
                }
                else
                {
                    // not same user
                    throw new ProtocolException("Not same user: "+getUserName()+" "+user);
                }
            }
            catch (UnsupportedEncodingException e)
            {
                throw new RuntimeException(e);
            }

            return ret;
        }

        public String toString()
        {
            String args = "";
            if ((mAppArgs != null) && (mAppArgs.length > 0))
            {
                args = java.util.Arrays.asList(mAppArgs).toString();
            }
            return mVMName + ":" + mAppName +  args;
        }

        protected static String encode(String[] args)
        {
            StringBuffer buf = new StringBuffer(256);

            try
            {
                buf.append(getMessagePrefix(args[0], args[1]));
                if (args.length > 2)
                {
                    buf.append(URLEncoder.encode(args[2], ENCODING));
                    for (int i=3 ; i < args.length ; i++)
                    {
                        buf.append("&").append(URLEncoder.encode(args[i], ENCODING));
                    }
                }
            }
            catch (UnsupportedEncodingException e)
            {
                throw new RuntimeException(e);
            }
            return buf.toString();
        }

        protected static String getMessagePrefix(String vmName, String appName)
        {
            try
            {
                String user = getUserName();
                return "/" + URLEncoder.encode(user, ENCODING) + "/" +
                       URLEncoder.encode(vmName, ENCODING) + "/" +
                       URLEncoder.encode(appName, ENCODING) + "?";
            }
            catch (UnsupportedEncodingException e)
            {
                throw new RuntimeException(e);
            }
        }

        protected static String getUserName()
        {
            String user = System.getProperty("user.name");
            if ((user == null) || user.equals(""))
            {
                user = "_nouser_";
            }
            return user;
        }
    }

    public static class ProtocolException extends Exception
    {
        public ProtocolException(String message)
        {
            super(message);
        }
    }
}
