/*
 * Copyright 2011 The IEC61850bean Authors
 *
 * Licensed 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 com.beanit.iec61850bean;

import com.beanit.josistack.AcseAssociation;
import com.beanit.josistack.ServerAcseSap;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import javax.net.ServerSocketFactory;

/**
 * The <code>ServerSap</code> class represents the IEC 61850 service access point for server
 * applications. It corresponds to the AccessPoint defined in the ICD/SCL file. A server application
 * that is to listen for client connections should first get an instance of <code>ServerSap</code>
 * using the static function ServerSap.getSapsFromSclFile(). Next all the necessary configuration
 * parameters can be set. Finally the <code>startListening</code> function is called to listen for
 * client associations. Changing properties of a ServerSap after starting to listen is not
 * recommended and has unknown effects.
 */
public final class ServerSap {

  static final int MINIMUM_MMS_PDU_SIZE = 64;
  private static final int MAXIMUM_MMS_PDU_SIZE = 65000;
  final ServerModel serverModel;
  final List<ServerAssociation> associations = new ArrayList<>();
  byte[] servicesSupportedCalled =
      new byte[] {(byte) 0xee, 0x1c, 0, 0, 0x04, 0x08, 0, 0, 0x79, (byte) 0xef, 0x18};
  byte[] cbbBitString = {(byte) 0xfb, 0x00};
  ServerEventListener serverEventListener;
  Timer timer;
  boolean listening = false;
  private int proposedMaxMmsPduSize = 65000;
  private int proposedMaxServOutstandingCalling = 5;
  private int proposedMaxServOutstandingCalled = 5;
  private int proposedDataStructureNestingLevel = 10;
  private int maxAssociations = 100;
  private ServerAcseSap acseSap;
  private int port = 102;
  private int backlog = 0;
  private InetAddress bindAddr = null;
  private ServerSocketFactory serverSocketFactory = null;

  /**
   * Creates a ServerSap.
   *
   * @param port local port to listen on for new connections
   * @param backlog The maximum queue length for incoming connection indications (a request to
   *     connect) is set to the backlog parameter. If a connection indication arrives when the queue
   *     is full, the connection is refused. Set to 0 or less for the default value.
   * @param bindAddr local IP address to bind to, pass null to bind to all
   * @param serverModel the server model
   * @param serverSocketFactory the factory class to generate the ServerSocket. Could be used to
   *     create SSLServerSockets. null = default
   */
  public ServerSap(
      int port,
      int backlog,
      InetAddress bindAddr,
      ServerModel serverModel,
      ServerSocketFactory serverSocketFactory) {
    this.port = port;
    this.backlog = backlog;
    this.bindAddr = bindAddr;
    this.serverSocketFactory = serverSocketFactory;
    this.serverModel = serverModel;
  }

  public int getPort() {
    return port;
  }

  /**
   * Sets local port to listen on for new connections.
   *
   * @param port local port to listen on for new connections
   */
  public void setPort(int port) {
    this.port = port;
  }

  public int getBacklog() {
    return backlog;
  }

  /**
   * Sets the maximum queue length for incoming connection indications (a request to connect) is set
   * to the backlog parameter. If a connection indication arrives when the queue is full, the
   * connection is refused. Set to 0 or less for the default value.
   *
   * @param backlog the maximum queue length for incoming connections.
   */
  public void setBacklog(int backlog) {
    this.backlog = backlog;
  }

  public InetAddress getBindAddress() {
    return bindAddr;
  }

  /**
   * Sets the local IP address to bind to, pass null to bind to all
   *
   * @param bindAddr the local IP address to bind to
   */
  public void setBindAddress(InetAddress bindAddr) {
    this.bindAddr = bindAddr;
  }

  /**
   * Sets the factory class to generate the ServerSocket. The ServerSocketFactory could be used to
   * create SSLServerSockets. Set to <code>null</code> to use <code>ServerSocketFactory.getDefault()
   * </code>.
   *
   * @param serverSocketFactory the factory class to generate the ServerSocket.
   */
  public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
    this.serverSocketFactory = serverSocketFactory;
  }

  /**
   * Gets the maximum MMS PDU size.
   *
   * @return the maximum MMS PDU size.
   */
  public int getMaxMmsPduSize() {
    return proposedMaxMmsPduSize;
  }

  /**
   * Sets the maximum MMS PDU size in bytes that the server will support. If the client requires the
   * use of a smaller maximum MMS PDU size, then the smaller size will be accepted by the server.
   * The default size is 65000.
   *
   * @param size cannot be less than 64. The upper limit is 65000 so that segmentation at the lower
   *     transport layer is avoided. The Transport Layer's maximum PDU size is 65531.
   */
  public void setMaxMmsPduSize(int size) {
    if (size >= MINIMUM_MMS_PDU_SIZE && size <= MAXIMUM_MMS_PDU_SIZE) {
      proposedMaxMmsPduSize = size;
    } else {
      throw new IllegalArgumentException("maximum size is out of bound");
    }
  }

  /**
   * Set the maximum number of associations that are allowed in parallel by the server.
   *
   * @param maxAssociations the number of associations allowed (default is 100)
   */
  public void setMaxAssociations(int maxAssociations) {
    this.maxAssociations = maxAssociations;
  }

  /**
   * Sets the message fragment timeout. This is the timeout that the socket timeout is set to after
   * the first byte of a message has been received. If such a timeout is thrown, the
   * association/socket is closed.
   *
   * @param timeout the message fragment timeout in milliseconds. The default is 60000.
   */
  public void setMessageFragmentTimeout(int timeout) {
    acseSap.serverTSap.setMessageFragmentTimeout(timeout);
  }

  /**
   * Gets the ProposedMaxServOutstandingCalling parameter.
   *
   * @return the ProposedMaxServOutstandingCalling parameter.
   */
  public int getProposedMaxServOutstandingCalling() {
    return proposedMaxServOutstandingCalling;
  }

  /**
   * Sets the ProposedMaxServOutstandingCalling parameter. The given parameter has no affect on the
   * functionality of this server.
   *
   * @param maxCalling the ProposedMaxServOutstandingCalling parameter. The default is 5.
   */
  public void setProposedMaxServOutstandingCalling(int maxCalling) {
    proposedMaxServOutstandingCalling = maxCalling;
  }

  /**
   * Gets the ProposedMaxServOutstandingCalled parameter.
   *
   * @return the ProposedMaxServOutstandingCalled parameter.
   */
  public int getProposedMaxServOutstandingCalled() {
    return proposedMaxServOutstandingCalled;
  }

  /**
   * Sets the ProposedMaxServOutstandingCalled parameter.The given parameter has no affect on the
   * functionality of this server.
   *
   * @param maxCalled the ProposedMaxServOutstandingCalled parameter. The default is 5.
   */
  public void setProposedMaxServOutstandingCalled(int maxCalled) {
    proposedMaxServOutstandingCalled = maxCalled;
  }

  /**
   * Gets the ProposedDataStructureNestingLevel parameter.
   *
   * @return the ProposedDataStructureNestingLevel parameter.
   */
  public int getProposedDataStructureNestingLevel() {
    return proposedDataStructureNestingLevel;
  }

  /**
   * Sets the ProposedDataStructureNestingLevel parameter. The given parameter has no affect on the
   * functionality of this server.runServer
   *
   * @param nestingLevel the ProposedDataStructureNestingLevel parameter. The default is 10.
   */
  public void setProposedDataStructureNestingLevel(int nestingLevel) {
    proposedDataStructureNestingLevel = nestingLevel;
  }

  /**
   * Gets the ServicesSupportedCalled parameter.
   *
   * @return the ServicesSupportedCalled parameter.
   */
  public byte[] getServicesSupportedCalled() {
    return servicesSupportedCalled;
  }

  /**
   * Sets the SevicesSupportedCalled parameter. The given parameter has no affect on the
   * functionality of this server.
   *
   * @param services the ServicesSupportedCalled parameter
   */
  public void setServicesSupportedCalled(byte[] services) {
    if (services.length != 11) {
      throw new IllegalArgumentException("The services parameter needs to be of lenth 11");
    }
    servicesSupportedCalled = services;
  }

  /**
   * Creates a server socket waiting on the configured port for incoming association requests.
   *
   * @param serverEventListener the listener that is notified of incoming writes and when the server
   *     stopped listening for new connections.
   * @throws IOException if an error occurs binding to the port.
   */
  public void startListening(ServerEventListener serverEventListener) throws IOException {
    timer = new Timer();
    if (serverSocketFactory == null) {
      serverSocketFactory = ServerSocketFactory.getDefault();
    }
    acseSap =
        new ServerAcseSap(port, backlog, bindAddr, new AcseListener(this), serverSocketFactory);
    acseSap.serverTSap.setMaxConnections(maxAssociations);
    this.serverEventListener = serverEventListener;
    listening = true;
    acseSap.startListening();
  }

  /** Stops listening for new connections and closes all existing connections/associations. */
  public void stop() {
    acseSap.stopListening();
    synchronized (associations) {
      listening = false;
      for (ServerAssociation association : associations) {
        association.close();
      }
      associations.clear();
    }
    timer.cancel();
    timer.purge();
  }

  void connectionIndication(AcseAssociation acseAssociation, ByteBuffer psdu) {

    ServerAssociation association;
    synchronized (associations) {
      if (listening) {
        association = new ServerAssociation(this);
        associations.add(association);
      } else {
        acseAssociation.close();
        return;
      }
    }

    try {
      association.handleNewAssociation(acseAssociation, psdu);
    } catch (Exception e) {
      // Association closed because of an unexpected exception.
    }

    association.close();
    synchronized (associations) {
      associations.remove(association);
    }
  }

  void serverStoppedListeningIndication(IOException e) {
    if (serverEventListener != null) {
      serverEventListener.serverStoppedListening(this);
    }
  }

  public ServerModel getModelCopy() {
    return serverModel.copy();
  }

  public void setValues(List<BasicDataAttribute> bdas) {
    synchronized (serverModel) {
      for (BasicDataAttribute bda : bdas) {
        // if (bda.getFunctionalConstraint() != FunctionalConstraint.ST) {
        // logger.debug("fc:" + bda.getFunctionalConstraint());
        // throw new IllegalArgumentException(
        // "One can only set values of BDAs with Functional Constraint ST(status)");
        // }

        BasicDataAttribute bdaMirror = bda.mirror;

        if (bdaMirror.dchg && bdaMirror.chgRcbs.size() != 0 && !bda.equals(bdaMirror)) {
          bdaMirror.setValueFrom(bda);
          synchronized (bdaMirror.chgRcbs) {
            for (Urcb urcb : bdaMirror.chgRcbs) {
              if (bdaMirror.dupd && urcb.getTrgOps().isDataUpdate()) {
                urcb.report(bdaMirror, true, false, true);
              } else {
                urcb.report(bdaMirror, true, false, false);
              }
            }
          }
        } else if (bdaMirror.dupd && bdaMirror.dupdRcbs.size() != 0) {
          bdaMirror.setValueFrom(bda);
          synchronized (bdaMirror.dupdRcbs) {
            for (Urcb urcb : bdaMirror.dupdRcbs) {
              urcb.report(bdaMirror, false, false, true);
            }
          }
        } else if (bdaMirror.qchg && bdaMirror.chgRcbs.size() != 0 && !bda.equals(bdaMirror)) {
          bdaMirror.setValueFrom(bda);
          synchronized (bdaMirror.chgRcbs) {
            for (Urcb urcb : bdaMirror.chgRcbs) {
              urcb.report(bdaMirror, false, true, false);
            }
          }
        } else {
          bdaMirror.setValueFrom(bda);
        }
      }
    }
  }
}