/*
 * 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.
 */

/**
 * @author Vitaly A. Provodin
 */

/**
 * Created on 29.01.2005
 */
package org.apache.harmony.jpda.tests.share;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.UnknownHostException;

import org.apache.harmony.jpda.tests.framework.DebuggeeSynchronizer;
import org.apache.harmony.jpda.tests.framework.LogWriter;
import org.apache.harmony.jpda.tests.framework.TestErrorException;
import org.apache.harmony.jpda.tests.framework.TestOptions;

/**
 * This class implements <code>DebuggeeSynchronizer</code> interface using
 * TCP/IP sockets. All operations can be timed out according to default timeout.
 */
public class JPDADebuggeeSynchronizer implements DebuggeeSynchronizer {

    public final static String SGNL_READY = "ready";

    public final static String SGNL_CONTINUE = "continue";

    protected Socket clientSocket = null;

    protected ServerSocket serverSocket = null;

    protected DataOutputStream out;

    protected DataInputStream in;

    protected TestOptions settings;

    protected LogWriter logWriter;

    /**
     * A constructor that initializes an instance of the class with specified
     * <code>LogWriter</code> and <code>TestOptions</code>.
     * 
     * @param logWriter
     *            The instance of implementation of LogWriter.
     * @param settings
     *            Instance of test options.
     */
    public JPDADebuggeeSynchronizer(LogWriter logWriter, TestOptions settings) {
        super();
        this.logWriter = logWriter;
        this.settings = settings;
    }

    /**
     * Sends specified message to synchronization channel.
     * 
     * @param message
     *            a message to be sent.
     */
    @Override
    public synchronized void sendMessage(String message) {
        try {
            out.writeUTF(message);
            out.flush();
            logWriter.println("[SYNC] Message sent: " + message);
        } catch (IOException e) {
            throw new TestErrorException(e);
        }
    }

    /**
     * Receives message from synchronization channel and compares it with the
     * expected <code>message</code>.
     * 
     * @param message
     *            expected message.
     * @return <code>true</code> if received string is equals to
     *         <code>message</code> otherwise - <code>false</code>.
     * 
     */
    @Override
    public synchronized boolean receiveMessage(String message) {
        String msg;
        try {
            logWriter.println("[SYNC] Waiting for message: " + message);
            msg = in.readUTF();
            logWriter.println("[SYNC] Received message: " + msg);
        } catch (EOFException e) {
            return false;
        } catch (IOException e) {
            logWriter.printError(e);
            return false;
        }
        return message.equalsIgnoreCase(msg);
    }

    /**
     * Receives message from synchronization channel.
     * 
     * @return received string or null if connection was closed.
     */
    @Override
    public synchronized String receiveMessage() {
        String msg;
        try {
            logWriter.println("[SYNC] Waiting for any messsage");
            msg = in.readUTF();
            logWriter.println("[SYNC] Received message: " + msg);
        } catch (EOFException e) {
            return null;
        } catch (IOException e) {
            throw new TestErrorException(e);
        }
        return msg;
    }

    /**
     * Receives message from synchronization channel without Exception.
     * 
     * @return received string
     */
    public synchronized String receiveMessageWithoutException(String invoker) {
        String msg;
        try {
            logWriter.println("[SYNC] Waiting for any message");
            msg = in.readUTF();
            logWriter.println("[SYNC] Received message: " + msg);
        } catch (Throwable thrown) {
            if (invoker != null) {
                logWriter.println("#### receiveMessageWithoutException: Exception occurred:");
                logWriter.println("#### " + thrown);
                logWriter.println("#### Invoker = " + invoker);
            }
            msg = "" + thrown;
        }
        return msg;
    }

    /**
     * Returns socket address for connecting to the server.
     * 
     * If <code>serverAddress.getPort()</code> returns 0 (i.e.,
     * <code>org.apache.harmony.jpda.tests.framework.TestOptions.DEFAULT_SYNC_PORT</code>),
     * a port will automatically be chosen by the OS when the server is bound to a socket.
     * 
     * @return socket address
     */
    public InetSocketAddress getSyncServerAddress() {
        // Use the LOOPBACK directly instead of doing a DNS lookup.
        int port = settings.getSyncPortNumber();
        try {
            // Use IPv4 to ensure we do not depend on IPv6 to run these tests.
            // TODO(25178637): Use InetAddress.getLoopbackAddress() instead.
            return new InetSocketAddress(
                InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }), port);
        } catch (UnknownHostException e) {
            throw new TestErrorException(
                    "[SYNC] Exception in binding for socket sync connection", e);
        }
    }

    /**
     * Binds server to listen socket port.
     * 
     * If <code>serverAddress.getPort()</code> returns 0 (i.e.,
     * <code>org.apache.harmony.jpda.tests.framework.TestOptions.DEFAULT_SYNC_PORT</code>),
     * the OS will choose a port automatically for this server socket.
     * 
     * @return port number
     */
    public synchronized int bindServer() {
        InetSocketAddress serverAddress = getSyncServerAddress();
        try {
            logWriter.println("[SYNC] Binding socket on: " + serverAddress);
            serverSocket = new ServerSocket(serverAddress.getPort(), 0, serverAddress.getAddress());
            int localPort = serverSocket.getLocalPort();
            logWriter.println("[SYNC] Bound socket on: " + serverAddress
                    + " (local port: " + localPort + ")" );
            return localPort;
        } catch (IOException e) {
            throw new TestErrorException(
                    "[SYNC] Exception in binding for socket sync connection", e);
        }
    }

    /**
     * Accepts sync connection form server side.
     */
    public synchronized void startServer() {
        long timeout = settings.getTimeout();
        try {
            serverSocket.setSoTimeout((int) timeout);
            logWriter.println("[SYNC] Accepting socket connection");
            clientSocket = serverSocket.accept();
            logWriter.println("[SYNC] Accepted socket connection");

            clientSocket.setSoTimeout((int) timeout);
            out = new DataOutputStream(clientSocket.getOutputStream());
            in = new DataInputStream(clientSocket.getInputStream());
        } catch (IOException e) {
            throw new TestErrorException(
                    "[SYNC] Exception in accepting socket sync connection", e);
        }
    }

    /**
     * Attaches for sync connection from client side..
     */
    public synchronized void startClient() {
        long timeout = settings.getTimeout();
        InetSocketAddress serverAddress = getSyncServerAddress();
        try {
            logWriter.println("[SYNC] Attaching socket to: " + serverAddress);
            clientSocket = new Socket(serverAddress.getAddress(), serverAddress.getPort());
            logWriter.println("[SYNC] Attached socket");

            clientSocket.setSoTimeout((int) timeout);
            out = new DataOutputStream(clientSocket.getOutputStream());
            in = new DataInputStream(clientSocket.getInputStream());
        } catch (IOException e) {
            throw new TestErrorException(
                    "[SYNC] Exception in attaching for socket sync connection", e);
        }
    }

    /**
     * Stops synchronization work. It ignores <code>IOException</code> but
     * prints a message.
     */
    public void stop() {
        try {
            if (out != null)
                out.close();
            if (in != null)
                in.close();
            if (clientSocket != null)
                clientSocket.close();
            if (serverSocket != null)
                serverSocket.close();
        } catch (IOException e) {
            logWriter.println
                    ("[SYNC] Ignoring exception in closing socket sync connection: " + e);
        }
        logWriter.println("[SYNC] Closed socket");
    }
}
