/*
 *  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.apache.tomcat.jni;

import java.io.InputStream;
import java.util.Properties;

/** Echo server example
 */

public class Echo {

    public static String echoEcho = null;
    public static String echoAddr = null;
    public static int echoPort    = 0;
    public static int echoNmax    = 0;
    public static int echoNrun    = 0;
    public static long echoPool   = 0;

    private static Poller echoPoller     = null;
    private static Acceptor echoAcceptor = null;

    private static Object threadLock = new Object();

    static {

        try {
            InputStream is = Echo.class.getResourceAsStream
                ("/org/apache/tomcat/jni/Echo.properties");
            Properties props = new Properties();
            props.load(is);
            is.close();
            echoAddr = props.getProperty("echo.ip", "127.0.0.1");
            echoPort = Integer.decode(props.getProperty("echo.port", "8023")).intValue();
            echoNmax = Integer.decode(props.getProperty("echo.max", "1")).intValue();
        }
        catch (Throwable t) {
            // NO-OP
        }
    }

    /* Acceptor thread. Listens for new connections */
    private class Acceptor extends java.lang.Thread {
        private long serverSock = 0;
        private long inetAddress = 0;
        private long pool = 0;
        public Acceptor() throws Exception {
            try {

                pool = Pool.create(Echo.echoPool);
                System.out.println("Accepting: " +  Echo.echoAddr + ":" +
                                   Echo.echoPort);
                inetAddress = Address.info(Echo.echoAddr, Socket.APR_INET,
                                           Echo.echoPort, 0,
                                           pool);
                serverSock = Socket.create(Socket.APR_INET, Socket.SOCK_STREAM,
                                           Socket.APR_PROTO_TCP, pool);
                long sa = Address.get(Socket.APR_LOCAL, serverSock);
                Sockaddr addr = new Sockaddr();
                if (Address.fill(addr, sa)) {
                    System.out.println("Host: " + addr.hostname);
                    System.out.println("Server: " + addr.servname);
                    System.out.println("IP: " + Address.getip(sa) +
                                       ":" + addr.port);
                }
                int rc = Socket.bind(serverSock, inetAddress);
                if (rc != 0) {
                  throw(new Exception("Can't create Acceptor: bind: " + Error.strerror(rc)));
                }
                Socket.listen(serverSock, 5);
            }
            catch( Exception ex ) {
                ex.printStackTrace();
                throw(new Exception("Can't create Acceptor"));
            }
        }

        @Override
        public void run() {
            int i = 0;
            try {
                while (true) {
                    long clientSock = Socket.accept(serverSock);
                    System.out.println("Accepted id: " +  i);

                    try {
                        long sa = Address.get(Socket.APR_REMOTE, clientSock);
                        Sockaddr raddr = new Sockaddr();
                        if (Address.fill(raddr, sa)) {
                            System.out.println("Remote Host: " + Address.getnameinfo(sa, 0));
                            System.out.println("Remote IP: " + Address.getip(sa) +
                                               ":" + raddr.port);
                        }
                        sa = Address.get(Socket.APR_LOCAL, clientSock);
                        Sockaddr laddr = new Sockaddr();
                        if (Address.fill(laddr, sa)) {
                            System.out.println("Local Host: " + laddr.hostname);
                            System.out.println("Local Server: " + Address.getnameinfo(sa, 0));
                            System.out.println("Local IP: " + Address.getip(sa) +
                                               ":" + laddr.port);
                        }

                    } catch (Exception e) {
                        // Ignore
                        e.printStackTrace();
                    }

                    Socket.timeoutSet(clientSock, 10000000);
                    Worker worker = new Worker(clientSock, i++,
                                               this.getClass().getName());
                    Echo.incThreads();
                    worker.start();
                }
            }
            catch( Exception ex ) {
                ex.printStackTrace();
            }
        }
    }

    /* Poller thread. Listens for new recycled connections */
    private class Poller extends java.lang.Thread {
        private long serverPollset = 0;
        private long pool = 0;
        private int nsocks = 0;
        public Poller() {
            try {

                pool = Pool.create(Echo.echoPool);
                serverPollset = Poll.create(16, pool, 0, 10000000);
            }
            catch( Exception ex ) {
                ex.printStackTrace();
            }
        }

        public void add(long socket) {
            int rv = Poll.add(serverPollset, socket,
                              Poll.APR_POLLIN);
            if (rv == Status.APR_SUCCESS) {
                System.out.println("Added worker to pollset");
                nsocks++;
            }
        }

        public void remove(long socket) {
            int rv = Poll.remove(serverPollset, socket);
            if (rv == Status.APR_SUCCESS) {
               nsocks--;
               System.out.println("Removed worker from pollset");
            }
            else {
               System.out.println("Failed removing worker from pollset");
            }
        }

        @Override
        public void run() {
            while (true) {
                try {
                    if (nsocks < 1) {
                        java.lang.Thread.sleep(1);
                        continue;
                    }
                    /* Two times size then  created pollset */
                    long [] desc = new long[64];
                    /* USe 1 second poll timeout */
                    int rv = Poll.poll(serverPollset, 1000000, desc, false);
                    if (rv > 0) {
                        for (int n = 0; n < rv; n++) {
                            long clientSock = desc[n*2+1];
                            System.out.println("Poll flags " + desc[n*2]);
                            remove(clientSock);
                            Worker worker = new Worker(clientSock, n,
                                                       this.getClass().getName());
                            Echo.incThreads();
                            worker.start();
                        }
                    }
                    else {
                        if (Status.APR_STATUS_IS_TIMEUP(-rv))
                            System.out.println("Timeup");
                        else {
                            System.out.println("Error " + (-rv));
                        }
                    }
                }
                /* XXX: JFC quick hack
                catch(Error err ) {
                    if (Status.APR_STATUS_IS_TIMEUP(err.getError())) {
                        /0 TODO: deal with timeout 0/
                    }
                    else {
                        err.printStackTrace();
                        break;
                    }
                }
                 */
                catch( Exception ex ) {
                    ex.printStackTrace();
                    break;
                }
            }
        }
    }

    private class Worker extends java.lang.Thread {
        private int workerId = 0;
        private long clientSock = 0;
        private byte [] wellcomeMsg = null;
        public Worker(long clientSocket, int workerId, String from) {
            this.clientSock = clientSocket;
            this.workerId = workerId;
            wellcomeMsg = ("Echo server id: " + this.workerId + " from " +
                           from + "\r\n").getBytes();
        }

        @Override
        public void run() {
            boolean doClose = false;
            try {
                Socket.send(clientSock, wellcomeMsg, 0, wellcomeMsg.length);
                /* Do a blocking read byte at a time */
                byte [] buf = new byte[1];
                while (Socket.recv(clientSock, buf, 0, 1) == 1) {
                    if (buf[0] == '\n')
                        break;
                    else if (buf[0] == '!') {
                        doClose = true;
                        break;
                    }
                }
                if (doClose) {
                    try {
                        byte [] msg = ("Bye from worker: " + workerId + "\r\n").getBytes();
                        Socket.send(clientSock, msg, 0, msg.length);
                    } catch(Exception e) { }

                    Socket.close(clientSock);
                }
                else {
                    try {
                        byte [] msg = ("Recycling worker: " + workerId + "\r\n").getBytes();
                        Socket.send(clientSock, msg, 0, msg.length);
                    } catch(Exception e) { }
                    /* Put the socket to the keep-alive poll */
                    Echo.echoPoller.add(clientSock);
                }
            } catch (Exception e) {
                Socket.close(clientSock);
                e.printStackTrace();
            }
            Echo.decThreads();
            System.out.println("Worker: " +  workerId + " finished");
        }
    }

    public Echo()
    {
        echoPool = Pool.create(0);
        try {
            echoAcceptor = new Acceptor();
            echoAcceptor.start();
            echoPoller = new Poller();
            echoPoller.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void incThreads() {
        synchronized(threadLock) {
            echoNrun++;
        }
    }

    public static void decThreads() {
        synchronized(threadLock) {
            echoNrun--;
        }
    }

    public static void main(String [] args) {
        try {
            Library.initialize(null);
            long [] inf = new long[16];
            System.out.println("Info ...");
            System.out.println("  Native        " + Library.versionString());
            System.out.println("  APR           " + Library.aprVersionString());
            OS.info(inf);
            System.out.println("OS Info ...");
            System.out.println("  Physical      " + inf[0]);
            System.out.println("  Avail         " + inf[1]);
            System.out.println("  Swap          " + inf[2]);
            System.out.println("  Swap free     " + inf[3]);
            System.out.println("  Shared        " + inf[4]);
            System.out.println("  Buffers size  " + inf[5]);
            System.out.println("  Load          " + inf[6]);

            System.out.println("  Idle          " + inf[7]);
            System.out.println("  Kernel        " + inf[8]);
            System.out.println("  User          " + inf[9]);

            System.out.println("  Proc creation " + inf[10]);
            System.out.println("  Proc kernel   " + inf[11]);
            System.out.println("  Proc user     " + inf[12]);
            System.out.println("  Curr working  " + inf[13]);
            System.out.println("  Peak working  " + inf[14]);
            System.out.println("  Page faults   " + inf[15]);

            SSL.initialize(null);
            System.out.println("OpenSSL ...");
            System.out.println("  version       " + SSL.versionString());
            System.out.println("  number        " + SSL.version());

            System.out.println("Starting Native Echo server example on port " +
                               echoAddr + ":" + echoPort);
            @SuppressWarnings("unused")
            Echo echo = new Echo();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
