/*
 *  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.coyote.ajp;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Locale;

import javax.net.SocketFactory;

import org.apache.tomcat.util.http.Method;

/**
 * AJP client that is not (yet) a full AJP client implementation as it just provides the functionality required for the
 * unit tests. The client uses blocking IO throughout.
 */
public class SimpleAjpClient {

    private static final int DEFAULT_AJP_PACKET_SIZE = 8192;
    private static final byte[] AJP_CPING;

    static {
        TesterAjpMessage ajpCping = new TesterAjpMessage(16);
        ajpCping.reset();
        ajpCping.appendByte(Constants.JK_AJP13_CPING_REQUEST);
        ajpCping.end();
        AJP_CPING = new byte[ajpCping.getLen()];
        System.arraycopy(ajpCping.getBuffer(), 0, AJP_CPING, 0, ajpCping.getLen());
    }

    private final int packetSize;
    private String host = "localhost";
    private int port = -1;
    /* GET == 2 */
    private int method = 2;
    private String protocol = "http";
    private String uri = "/";
    private String remoteAddr = "192.168.0.1";
    private String remoteHost = "client.example.com";
    private String serverName = "www.example.com";
    private int serverPort = 80;
    private boolean ssl = false;
    private Socket socket = null;

    public SimpleAjpClient() {
        this(DEFAULT_AJP_PACKET_SIZE);
    }

    public SimpleAjpClient(int packetSize) {
        this.packetSize = packetSize;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public int getPort() {
        return port;
    }

    public void setMethod(String method) {
        method = method.toUpperCase(Locale.ENGLISH);
        switch (method) {
            case Method.OPTIONS:
                this.method = 1;
                break;
            case Method.GET:
                this.method = 2;
                break;
            case Method.HEAD:
                this.method = 3;
                break;
            case Method.POST:
                this.method = 4;
                break;
            case Method.PUT:
                this.method = 5;
                break;
            case Method.DELETE:
                this.method = 6;
                break;
            case Method.TRACE:
                this.method = 7;
                break;
            case Method.PROPFIND:
                this.method = 8;
                break;
            case Method.PROPPATCH:
                this.method = 9;
                break;
            case Method.MKCOL:
                this.method = 10;
                break;
            case Method.COPY:
                this.method = 11;
                break;
            case Method.MOVE:
                this.method = 12;
                break;
            case Method.LOCK:
                this.method = 13;
                break;
            case Method.UNLOCK:
                this.method = 14;
                break;
            case "ACL":
                this.method = 15;
                break;
            case "REPORT":
                this.method = 16;
                break;
            case "VERSION-CONTROL":
                this.method = 17;
                break;
            case "CHECKIN":
                this.method = 18;
                break;
            case "CHECKOUT":
                this.method = 19;
                break;
            case "UNCHECKOUT":
                this.method = 20;
                break;
            case "SEARCH":
                this.method = 21;
                break;
            case "MKWORKSPACE":
                this.method = 22;
                break;
            case "UPDATE":
                this.method = 23;
                break;
            case "LABEL":
                this.method = 24;
                break;
            case "MERGE":
                this.method = 25;
                break;
            case "BASELINE-CONTROL":
                this.method = 26;
                break;
            case "MKACTIVITY":
                this.method = 27;
                break;
            default:
                this.method = 99;
        }
    }

    public String getMethod() {
        switch (method) {
            case 1:
                return Method.OPTIONS;
            case 2:
                return Method.GET;
            case 3:
                return Method.HEAD;
            case 4:
                return Method.POST;
            case 5:
                return Method.PUT;
            case 6:
                return Method.DELETE;
            case 7:
                return Method.TRACE;
            case 8:
                return Method.PROPFIND;
            case 9:
                return Method.PROPPATCH;
            case 10:
                return Method.MKCOL;
            case 11:
                return Method.COPY;
            case 12:
                return Method.MOVE;
            case 13:
                return Method.LOCK;
            case 14:
                return Method.UNLOCK;
            case 15:
                return "ACL";
            case 16:
                return "REPORT";
            case 17:
                return "VERSION-CONTROL";
            case 18:
                return "CHECKIN";
            case 19:
                return "CHECKOUT";
            case 20:
                return "UNCHECKOUT";
            case 21:
                return "SEARCH";
            case 22:
                return "MKWORKSPACE";
            case 23:
                return "UPDATE";
            case 24:
                return "LABEL";
            case 25:
                return "MERGE";
            case 26:
                return "BASELINE-CONTROL";
            case 27:
                return "MKACTIVITY";
            default:
                return "UNKNOWN";
        }
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public String getUri() {
        return uri;
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    public String getRemoteAddr() {
        return remoteAddr;
    }

    public void setRemoteHost(String remoteHost) {
        this.remoteHost = remoteHost;
    }

    public String getRemoteHost() {
        return remoteHost;
    }

    public void setServerName(String serverName) {
        this.serverName = serverName;
    }

    public String getServerName() {
        return serverName;
    }

    public void setServerPort(int serverPort) {
        this.serverPort = serverPort;
    }

    public int getServerPort() {
        return serverPort;
    }

    public void setSsl(boolean ssl) {
        this.ssl = ssl;
    }

    public boolean isSsl() {
        return ssl;
    }

    public void connect() throws IOException {
        socket = SocketFactory.getDefault().createSocket(host, port);
    }

    public void disconnect() throws IOException {
        socket.close();
        socket = null;
    }

    /*
     * Create a message to request the given URL.
     */
    public TesterAjpMessage createForwardMessage() {

        TesterAjpMessage message = new TesterAjpMessage(packetSize);
        message.reset();

        // Set the header bytes
        message.getBuffer()[0] = 0x12;
        message.getBuffer()[1] = 0x34;

        // Code 2 for forward request
        message.appendByte(Constants.JK_AJP13_FORWARD_REQUEST);

        // HTTP method, GET = 2
        message.appendByte(method);

        // Protocol
        message.appendString(protocol);

        // Request URI
        message.appendString(uri);

        // Client address
        message.appendString(remoteAddr);

        // Client host
        message.appendString(remoteHost);

        // Server name
        message.appendString(serverName);

        // Server port
        message.appendInt(serverPort);

        // Is ssl
        message.appendByte(ssl ? 0x01 : 0x00);

        return message;
    }

    public TesterAjpMessage createBodyMessage(byte[] data) {

        TesterAjpMessage message = new TesterAjpMessage(packetSize);
        message.reset();

        // Set the header bytes
        message.getBuffer()[0] = 0x12;
        message.getBuffer()[1] = 0x34;

        message.appendBytes(data, 0, data.length);
        message.end();

        return message;
    }


    /*
     * Sends an TesterAjpMessage to the server and returns the response message.
     */
    public TesterAjpMessage sendMessage(TesterAjpMessage headers) throws IOException {
        return sendMessage(headers, null);
    }

    public TesterAjpMessage sendMessage(TesterAjpMessage headers, TesterAjpMessage body) throws IOException {
        // Send the headers
        socket.getOutputStream().write(headers.getBuffer(), 0, headers.getLen());
        if (body != null) {
            // Send the body of present
            socket.getOutputStream().write(body.getBuffer(), 0, body.getLen());
        }
        // Read the response
        return readMessage();
    }

    /*
     * Tests the connection to the server and returns the CPONG response.
     */
    public TesterAjpMessage cping() throws IOException {
        // Send the ping message
        socket.getOutputStream().write(AJP_CPING);
        // Read the response
        return readMessage();
    }

    /*
     * Reads a message from the server.
     */
    public TesterAjpMessage readMessage() throws IOException {

        InputStream is = socket.getInputStream();

        TesterAjpMessage message = new TesterAjpMessage(packetSize);

        byte[] buf = message.getBuffer();

        read(is, buf, 0, Constants.H_SIZE);

        int messageLength = message.processHeader(false);
        if (messageLength < 0) {
            throw new IOException("Invalid AJP message length");
        } else if (messageLength == 0) {
            return message;
        } else {
            if (messageLength > buf.length) {
                throw new IllegalArgumentException("Message too long [" + Integer.valueOf(messageLength) +
                        "] for buffer length [" + Integer.valueOf(buf.length) + "]");
            }
            read(is, buf, Constants.H_SIZE, messageLength);
            return message;
        }
    }

    protected boolean read(InputStream is, byte[] buf, int pos, int n) throws IOException {

        int read = 0;
        int res = 0;
        while (read < n) {
            res = is.read(buf, read + pos, n - read);
            if (res > 0) {
                read += res;
            } else {
                throw new IOException("Read failed");
            }
        }
        return true;
    }
}
