/*
 * Copyright (c) 2010-2020 Nathan Rajlich
 *
 *  Permission is hereby granted, free of charge, to any person
 *  obtaining a copy of this software and associated documentation
 *  files (the "Software"), to deal in the Software without
 *  restriction, including without limitation the rights to use,
 *  copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following
 *  conditions:
 *
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *  OTHER DEALINGS IN THE SOFTWARE.
 */

package org.java_websocket.framing;

import java.nio.ByteBuffer;
import org.java_websocket.enums.Opcode;
import org.java_websocket.exceptions.InvalidDataException;
import org.java_websocket.exceptions.InvalidFrameException;
import org.java_websocket.util.ByteBufferUtils;
import org.java_websocket.util.Charsetfunctions;

/**
 * Class to represent a close frame
 */
public class CloseFrame extends ControlFrame {

  /**
   * indicates a normal closure, meaning whatever purpose the connection was established for has
   * been fulfilled.
   */
  public static final int NORMAL = 1000;
  /**
   * 1001 indicates that an endpoint is "going away", such as a server going down, or a browser
   * having navigated away from a page.
   */
  public static final int GOING_AWAY = 1001;
  /**
   * 1002 indicates that an endpoint is terminating the connection due to a protocol error.
   */
  public static final int PROTOCOL_ERROR = 1002;
  /**
   * 1003 indicates that an endpoint is terminating the connection because it has received a type of
   * data it cannot accept (e.g. an endpoint that understands only text data MAY send this if it
   * receives a binary message).
   */
  public static final int REFUSE = 1003;
  /*1004: Reserved. The specific meaning might be defined in the future.*/
  /**
   * 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
   * endpoint. It is designated for use in applications expecting a status code to indicate that no
   * status code was actually present.
   */
  public static final int NOCODE = 1005;
  /**
   * 1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
   * endpoint. It is designated for use in applications expecting a status code to indicate that the
   * connection was closed abnormally, e.g. without sending or receiving a Close control frame.
   */
  public static final int ABNORMAL_CLOSE = 1006;
  /**
   * 1007 indicates that an endpoint is terminating the connection because it has received data
   * within a message that was not consistent with the type of the message (e.g., non-UTF-8
   * [RFC3629] data within a text message).
   */
  public static final int NO_UTF8 = 1007;
  /**
   * 1008 indicates that an endpoint is terminating the connection because it has received a message
   * that violates its policy. This is a generic status code that can be returned when there is no
   * other more suitable status code (e.g. 1003 or 1009), or if there is a need to hide specific
   * details about the policy.
   */
  public static final int POLICY_VALIDATION = 1008;
  /**
   * 1009 indicates that an endpoint is terminating the connection because it has received a message
   * which is too big for it to process.
   */
  public static final int TOOBIG = 1009;
  /**
   * 1010 indicates that an endpoint (client) is terminating the connection because it has expected
   * the server to negotiate one or more extension, but the server didn't return them in the
   * response message of the WebSocket handshake. The list of extensions which are needed SHOULD
   * appear in the /reason/ part of the Close frame. Note that this status code is not used by the
   * server, because it can fail the WebSocket handshake instead.
   */
  public static final int EXTENSION = 1010;
  /**
   * 1011 indicates that a server is terminating the connection because it encountered an unexpected
   * condition that prevented it from fulfilling the request.
   **/
  public static final int UNEXPECTED_CONDITION = 1011;
  /**
   * 1012 indicates that the service is restarted. A client may reconnect, and if it choses to do,
   * should reconnect using a randomized delay of 5 - 30s. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html
   * for more information.
   *
   * @since 1.3.8
   **/
  public static final int SERVICE_RESTART = 1012;
  /**
   * 1013 indicates that the service is experiencing overload. A client should only connect to a
   * different IP (when there are multiple for the target) or reconnect to the same IP upon user
   * action. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html for more
   * information.
   *
   * @since 1.3.8
   **/
  public static final int TRY_AGAIN_LATER = 1013;
  /**
   * 1014 indicates that the server was acting as a gateway or proxy and received an invalid
   * response from the upstream server. This is similar to 502 HTTP Status Code See
   * https://www.ietf.org/mail-archive/web/hybi/current/msg10748.html fore more information.
   *
   * @since 1.3.8
   **/
  public static final int BAD_GATEWAY = 1014;
  /**
   * 1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an
   * endpoint. It is designated for use in applications expecting a status code to indicate that the
   * connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate
   * can't be verified).
   **/
  public static final int TLS_ERROR = 1015;

  /**
   * The connection had never been established
   */
  public static final int NEVER_CONNECTED = -1;

  /**
   * The connection had a buggy close (this should not happen)
   */
  public static final int BUGGYCLOSE = -2;

  /**
   * The connection was flushed and closed
   */
  public static final int FLASHPOLICY = -3;


  /**
   * The close code used in this close frame
   */
  private int code;

  /**
   * The close message used in this close frame
   */
  private String reason;

  /**
   * Constructor for a close frame
   * <p>
   * Using opcode closing and fin = true
   */
  public CloseFrame() {
    super(Opcode.CLOSING);
    setReason("");
    setCode(CloseFrame.NORMAL);
  }

  /**
   * Set the close code for this close frame
   *
   * @param code the close code
   */
  public void setCode(int code) {
    this.code = code;
    // CloseFrame.TLS_ERROR is not allowed to be transferred over the wire
    if (code == CloseFrame.TLS_ERROR) {
      this.code = CloseFrame.NOCODE;
      this.reason = "";
    }
    updatePayload();
  }

  /**
   * Set the close reason for this close frame
   *
   * @param reason the reason code
   */
  public void setReason(String reason) {
    if (reason == null) {
      reason = "";
    }
    this.reason = reason;
    updatePayload();
  }

  /**
   * Get the used close code
   *
   * @return the used close code
   */
  public int getCloseCode() {
    return code;
  }

  /**
   * Get the message that closeframe is containing
   *
   * @return the message in this frame
   */
  public String getMessage() {
    return reason;
  }

  @Override
  public String toString() {
    return super.toString() + "code: " + code;
  }

  @Override
  public void isValid() throws InvalidDataException {
    super.isValid();
    if (code == CloseFrame.NO_UTF8 && reason.isEmpty()) {
      throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!");
    }
    if (code == CloseFrame.NOCODE && 0 < reason.length()) {
      throw new InvalidDataException(PROTOCOL_ERROR,
          "A close frame must have a closecode if it has a reason");
    }
    //Intentional check for code != CloseFrame.TLS_ERROR just to make sure even if the code earlier changes
    if ((code > CloseFrame.TLS_ERROR && code < 3000)) {
      throw new InvalidDataException(PROTOCOL_ERROR, "Trying to send an illegal close code!");
    }
    if (code == CloseFrame.ABNORMAL_CLOSE || code == CloseFrame.TLS_ERROR
        || code == CloseFrame.NOCODE || code > 4999 || code < 1000 || code == 1004) {
      throw new InvalidFrameException("closecode must not be sent over the wire: " + code);
    }
  }

  @Override
  public void setPayload(ByteBuffer payload) {
    code = CloseFrame.NOCODE;
    reason = "";
    payload.mark();
    if (payload.remaining() == 0) {
      code = CloseFrame.NORMAL;
    } else if (payload.remaining() == 1) {
      code = CloseFrame.PROTOCOL_ERROR;
    } else {
      if (payload.remaining() >= 2) {
        ByteBuffer bb = ByteBuffer.allocate(4);
        bb.position(2);
        bb.putShort(payload.getShort());
        bb.position(0);
        code = bb.getInt();
      }
      payload.reset();
      try {
        int mark = payload.position();// because stringUtf8 also creates a mark
        validateUtf8(payload, mark);
      } catch (InvalidDataException e) {
        code = CloseFrame.NO_UTF8;
        reason = null;
      }
    }
  }

  /**
   * Validate the payload to valid utf8
   *
   * @param mark    the current mark
   * @param payload the current payload
   * @throws InvalidDataException the current payload is not a valid utf8
   */
  private void validateUtf8(ByteBuffer payload, int mark) throws InvalidDataException {
    try {
      payload.position(payload.position() + 2);
      reason = Charsetfunctions.stringUtf8(payload);
    } catch (IllegalArgumentException e) {
      throw new InvalidDataException(CloseFrame.NO_UTF8);
    } finally {
      payload.position(mark);
    }
  }

  /**
   * Update the payload to represent the close code and the reason
   */
  private void updatePayload() {
    byte[] by = Charsetfunctions.utf8Bytes(reason);
    ByteBuffer buf = ByteBuffer.allocate(4);
    buf.putInt(code);
    buf.position(2);
    ByteBuffer pay = ByteBuffer.allocate(2 + by.length);
    pay.put(buf);
    pay.put(by);
    pay.rewind();
    super.setPayload(pay);
  }

  @Override
  public ByteBuffer getPayloadData() {
    if (code == NOCODE) {
      return ByteBufferUtils.getEmptyByteBuffer();
    }
    return super.getPayloadData();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    if (!super.equals(o)) {
      return false;
    }

    CloseFrame that = (CloseFrame) o;

    if (code != that.code) {
      return false;
    }
    return reason != null ? reason.equals(that.reason) : that.reason == null;
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + code;
    result = 31 * result + (reason != null ? reason.hashCode() : 0);
    return result;
  }
}
