/*
 * @(#)Response.java
 *
 * Copyright (c) 2004 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.inet.ftps;

import com.jscape.inet.ftp.FtpException;
import com.jscape.util.Assert;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/**
 * The FTP server response abstraction.
 *
 * @author Denis Bondarenko
 * @version 1.0
 */
public final class Response {

    /**
     * Positive Preliminary reply starting code
     */
    public static final int POSITIVE_PRELIMINARY = 100;

    /**
     * Positive Completion reply starting code
     */
    public static final int POSITIVE_COMPLETION = 200;

    /**
     * Positive Intermediate reply starting code
     */
    public static final int POSITIVE_INTERMEDIATE = 300;

    /**
     * Transient Negative Completion reply starting code
     */
    public static final int NEGATIVE_TRANSIENT = 400;

    /**
     * Permanent Negative Completion reply starting code
     */
    public static final int NEGATIVE_PERMANENT = 500;

    /* Response code length = three digit + SP */
    private static final int CODE_LENGTH = 4;

    private final int code;
    private final String message;

    //--------------------------------------------------------------------------

    /**
     * Reads the FTP response from the input stream.
     *
     * @param in the target input stream
     *
     * @return the readed response
     *
     * @throws FtpException if an I/O or protocol error occurs
     * @throws IOException  if an I/O or protocol error occurs
     */
    public static Response readFrom(BufferedReader in)
            throws IOException, FtpException {
        String line = in.readLine();
        if (line != null && line.trim().equals("")) {
            do {
                line = in.readLine();
            } while (line != null && line.equals(""));
        }
        int code = getResponseCode(line);
        StringBuffer buffer = new StringBuffer(getResponseText(line));

        if (line != null && isMultiLineResponse(line)) {
            readMultiLineResponse(in, buffer, code);
        }

        return new Response(code, buffer.toString());
    }

    //--------------------------------------------------------------------------

    /**
     * Checks if the specified line is multi-line response.
     *
     * @param line the line to check
     *
     * @return <code>true</code> if the specified line is the multi-line
     *         response first line; <code>false</code> otherwise
     */
    private static boolean isMultiLineResponse(String line) {
        return (line.length() >= CODE_LENGTH && line.charAt(3) == '-');
    }

    //--------------------------------------------------------------------------

    /**
     * Reads the multi-line response.
     *
     * @param in     the input stream
     * @param buffer the message buffer buffer
     * @param code   the response code
     *
     * @throws IOException if an I/O error occurs
     */
    private static void readMultiLineResponse(BufferedReader in, StringBuffer buffer,
                                              int code)
            throws IOException {
        String lastLineSign = code + " ";
        String line;

        for (line = in.readLine(); line != null && !line.startsWith(lastLineSign); line = in.readLine()) {
            if (line.length() > 0) {
                buffer.append("\r\n").append(line);
            }
        }


        if (line != null) {
            String lastLine = getResponseText(line);

            if (lastLine.length() > 0) {
                buffer.append("\r\n").append(lastLine);
            }
        }
    }

    //--------------------------------------------------------------------------

    /**
     * Extracts the response message assuming it comes after three-digit code plus
     * space sign.
     *
     * @param line the full response line
     *
     * @return extracted response message
     */
    private static String getResponseText(String line) {
        return (line.length() > CODE_LENGTH)
               ? line.substring(CODE_LENGTH, line.length())
               : "";
    }

    //--------------------------------------------------------------------------

    /**
     * Obtains response code from the specified response line.
     * The FTP response code contains three digit.
     *
     * @param line response line
     *
     * @return response code as <code>int</code>
     *
     * @throws FtpException if the code cannot be obtained
     */
    private static int getResponseCode(String line)
            throws FtpException {

        if (line == null || line.length() < 3) {
            throw new FtpException("bad line length");
        }

        try {
            return (Integer.parseInt(line.substring(0, 3)));
        } catch (Exception e) {
            throw new FtpException("bad response code");
        }
    }

    //--------------------------------------------------------------------------

    /**
     * Creates a new response instance.
     *
     * @param code the response code
     * @param text the response message
     */
    public Response(int code, String text) {
        Assert.notNull(text);

        this.code = code;
        this.message = text;
    }

    //--------------------------------------------------------------------------

    /**
     * Returns the response code.
     *
     * @return the response code
     */
    public int getCode() {
        return this.code;
    }

    //--------------------------------------------------------------------------

    /**
     * Returns the response message.
     *
     * @return the response message
     */
    public String getMessage() {
        return this.message;
    }

    //--------------------------------------------------------------------------

    /**
     * Provides the human-readable response representation.
     *
     * @return the human-readable response representation
     */
    public String toString() {
        return this.code + " " + this.message;
    }

    //--------------------------------------------------------------------------

    /**
     * Writes the current response to the specified output stream.
     *
     * @param out target output stream
     *
     * @throws IOException if an I/O error occurs
     */
    public void writeTo(OutputStream out)
            throws IOException {
        out.write(toString().getBytes(StandardCharsets.UTF_8));
        out.write(FtpsClient.EOL.getBytes(StandardCharsets.US_ASCII));
    }

    //--------------------------------------------------------------------------

    /**
     * Checks two responses equality. The FTP responces are equal if
     * they have the same code.
     *
     * @param o object to check
     *
     * @return <code>true</code> if both responses are equal;
     *         <code>false</code> otherwise
     */
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Response)) {
            return false;
        }

        final Response that = (Response) o;

        return (this.code == that.code);
    }

    //--------------------------------------------------------------------------

    /**
     * Returns response hash code.
     *
     * @return response hash code
     */
    public int hashCode() {
        return this.code;
    }

    //--------------------------------------------------------------------------

    /**
     * Checks if the current response is negative in the meaning of the
     * possible FTP response codes. This method may be used for
     * general error check.
     *
     * @return <code>true</code> if the current response is negative;
     *         <code>false</code> otherwise
     */
    public boolean isNegative() {
        return (this.code >= NEGATIVE_TRANSIENT);
    }

    //--------------------------------------------------------------------------

    /**
     * Checks if the current response is preliminary in the meaning of the
     * possible FTP response codes.
     *
     * @return <code>true</code> if the current response is preliminary;
     *         <code>false</code> otherwise
     */
    public boolean isPreliminary() {
        return (this.code <= 120);  // code 110 or 120 means that the true response follows
    }

}
