/*
 * Copyright (c) 2013, 2020 Oracle and/or its affiliates and others.
 * All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.ts.tests.websocket.common.client;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;

public abstract class ClientEndpoint<T extends Object> extends Endpoint
    implements MessageHandler.Whole<T> {

  /**
   * This structure is static, because the original API was unable to set an
   * instance of endpoint when connecting to server, the only available option
   * as an argument of
   * {@link WebSocketContainer#connectToServer(Class, ClientEndpointConfig, java.net.URI)}
   * was Class. The instance of endpoint was created by websocket API, and no
   * information could be injected into the instance.
   */
  public static class ClientEndpointData {
    protected static volatile StringBuffer sb = new StringBuffer();

    protected static volatile Throwable websocketError = null;

    protected static EndpointCallback callback;

    protected static volatile CountDownLatch messageLatch;

    protected static volatile Object lastMessage = null;

    protected static final Object LOCK = new Object();

    protected static volatile CountDownLatch onCloseLatch = null;

    public static Throwable getError() {
      return websocketError;
    }

    private static void setError(Throwable error) {
      websocketError = error;
    }

    public static String getResponseAsString() {
      WebSocketTestCase.logMsg("Response:", sb.toString());
      return sb.toString();
    }

    public static void resetData() {
      synchronized (LOCK) {
        WebSocketCommonClient.logTrace("Reseting callback and message", "");
        sb = new StringBuffer();
        websocketError = null;
        callback = null;
        lastMessage = null;
        messageLatch = null;
        onCloseLatch = null;
      }
    }

    public static void newCountDown(int count) {
      messageLatch = new CountDownLatch(count);
    };

    public static void awaitCountDown(int seconds) {
      try {
        messageLatch.await(seconds, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }

    public static void newOnCloseCountDown() {
      if (onCloseLatch == null)
        onCloseLatch = new CountDownLatch(1);
    }

    public static long getCount() {
      return messageLatch.getCount();
    }

    public static void awaitOnClose() {
      try {
        onCloseLatch.await(2000L, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
        e.printStackTrace(); // If it does not wait,
        // the test should still pass
      }
    }

    public static Object getOriginalMessage() {
      return lastMessage;
    }

    private static void setOriginalMessage(Object message) {
      lastMessage = message;
    }

  }

  /**
   * Hopefully this approach might be changed when api allows for passing the
   * instance instead of class.
   */

  @Override
  public void onOpen(Session session, EndpointConfig config) {
    onOpen(session, config, true);
  }

  public void onOpen(Session session, EndpointConfig config,
      boolean addMessageHandler) {
    WebSocketCommonClient.logTrace("On open on session id", session.getId());
    String uri = session.getRequestURI() == null ? "NULL"
        : session.getRequestURI().toASCIIString();
    WebSocketCommonClient.logTrace("RequestUri:", uri);
    if (session.isOpen()) {
      if (addMessageHandler)
        session.addMessageHandler(this);
    } else
      WebSocketCommonClient.logTrace("Session is closed!!!!", "");
    synchronized (ClientEndpointData.LOCK) {
      if (ClientEndpointData.callback != null && session.isOpen())
        ClientEndpointData.callback.onOpen(session, config);
    }
  }

  @Override
  public void onError(Session session, Throwable t) {
    ClientEndpointData.setError(t);
    t.printStackTrace();
    synchronized (ClientEndpointData.LOCK) {
      if (ClientEndpointData.callback != null)
        ClientEndpointData.callback.onError(session, t);
    }
  }

  protected void appendMessage(T message) {
    ClientEndpointData.sb.append(message.toString());
  }

  @Override
  public void onMessage(T message) {
    ClientEndpointData.setOriginalMessage(message);
    appendMessage(message);
    WebSocketCommonClient.logTrace("Received message so far",
        ClientEndpointData.sb.toString());
    synchronized (ClientEndpointData.LOCK) {
      if (ClientEndpointData.callback != null)
        ClientEndpointData.callback.onMessage(message);
    }
    WebSocketTestCase.logTrace("CountDownLatch hit");
    if (ClientEndpointData.messageLatch.getCount() == 0)
      throw new IllegalStateException("CountDownLatch.getCount == 0 already");
    ClientEndpointData.messageLatch.countDown();
  }

  @Override
  public void onClose(Session session, CloseReason closeReason) {
    synchronized (ClientEndpointData.LOCK) {
      WebSocketTestCase.logTrace("On close on session id", session.getId(),
          "reason", closeReason);
      if (ClientEndpointData.lastMessage == null)
        WebSocketTestCase
            .logTrace("onClose has been called before a message was received");
      else
        WebSocketTestCase.logTrace("onClose has been called");
      if (ClientEndpointData.callback != null)
        ClientEndpointData.callback.onClose(session, closeReason);
      // onCloseLatch == null when close has not been called by client
      if (ClientEndpointData.onCloseLatch == null)
        ClientEndpointData.newOnCloseCountDown();
      ClientEndpointData.onCloseLatch.countDown();
    }
  }

  public static CountDownLatch getCountDownLatch() {
    return ClientEndpointData.messageLatch;
  }

  public static StringBuffer getMessageBuilder() {
    return ClientEndpointData.sb;
  }

  @SuppressWarnings({ "unchecked", "unused" })
  public T getLastMessage(Class<T> messageType) {
    return (T) ClientEndpointData.lastMessage;
  }

  public static Throwable getLastError() {
    return ClientEndpointData.websocketError;
  }

}
