/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.server.network.protocol.binary;

import com.orientechnologies.common.concur.OOfflineNodeException;
import com.orientechnologies.common.concur.lock.OInterruptedException;
import com.orientechnologies.common.concur.lock.OLockException;
import com.orientechnologies.common.exception.OErrorCode;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.client.binary.OBinaryRequestExecutor;
import com.orientechnologies.orient.client.remote.OBinaryRequest;
import com.orientechnologies.orient.client.remote.OBinaryResponse;
import com.orientechnologies.orient.client.remote.message.*;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.config.OContextConfiguration;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCoreException;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OSecurityAccessException;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializer;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializerFactory;
import com.orientechnologies.orient.core.serialization.serializer.record.OSerializationThreadLocal;
import com.orientechnologies.orient.core.serialization.serializer.record.binary.ORecordSerializerNetworkFactory;
import com.orientechnologies.orient.core.serialization.serializer.record.string.ORecordSerializerSchemaAware2CSV;
import com.orientechnologies.orient.core.storage.ridbag.sbtree.OSBTreeCollectionManager;
import com.orientechnologies.orient.enterprise.channel.binary.*;
import com.orientechnologies.orient.server.OClientConnection;
import com.orientechnologies.orient.server.OConnectionBinaryExecutor;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.OServerAware;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.network.OServerNetworkListener;
import com.orientechnologies.orient.server.network.protocol.ONetworkProtocol;
import com.orientechnologies.orient.server.plugin.OServerPluginHelper;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.function.Function;
import java.util.logging.Level;

public class ONetworkProtocolBinary extends ONetworkProtocol {
  protected final    Level                              logClientExceptions;
  protected final    boolean                            logClientFullStackTrace;
  protected          OChannelBinary                     channel;
  protected volatile int                                requestType;
  protected          int                                clientTxId;
  protected          boolean                            okSent;
  private            boolean                            tokenConnection = true;
  private            long                               requests        = 0;
  private            HandshakeInfo                      handshakeInfo;
  private volatile   OBinaryPushResponse                expectedPushResponse;
  private            BlockingQueue<OBinaryPushResponse> pushResponse    = new SynchronousQueue<OBinaryPushResponse>();

  private Function<Integer, OBinaryRequest<? extends OBinaryResponse>> factory = ONetworkBinaryProtocolFactory.defaultProtocol();

  public ONetworkProtocolBinary(OServer server) {
    this(server, "OrientDB <- BinaryClient/?");
  }

  public ONetworkProtocolBinary(OServer server, final String iThreadName) {
    super(server.getThreadGroup(), iThreadName);
    logClientExceptions = Level
        .parse(server.getContextConfiguration().getValueAsString(OGlobalConfiguration.SERVER_LOG_DUMP_CLIENT_EXCEPTION_LEVEL));
    logClientFullStackTrace = server.getContextConfiguration()
        .getValueAsBoolean(OGlobalConfiguration.SERVER_LOG_DUMP_CLIENT_EXCEPTION_FULLSTACKTRACE);
  }

  /**
   * Internal varialbe injection useful for testing.
   */
  public void initVariables(final OServer server, OChannelBinary channel) {
    this.server = server;
    this.channel = channel;
  }

  @Override
  public void config(final OServerNetworkListener iListener, final OServer iServer, final Socket iSocket,
      final OContextConfiguration iConfig) throws IOException {

    OChannelBinaryServer channel = new OChannelBinaryServer(iSocket, iConfig);
    initVariables(iServer, channel);

    // SEND PROTOCOL VERSION
    channel.writeShort((short) getVersion());

    channel.flush();

    OServerPluginHelper.invokeHandlerCallbackOnSocketAccepted(server, this);

    start();
    setName("OrientDB (" + iSocket.getLocalSocketAddress() + ") <- BinaryClient (" + iSocket.getRemoteSocketAddress() + ")");
  }

  @Override
  public void startup() {
    super.startup();
  }

  @Override
  public void shutdown() {
    sendShutdown();
    channel.close();

    OServerPluginHelper.invokeHandlerCallbackOnSocketDestroyed(server, this);
  }

  private boolean isHandshaking(int requestType) {
    return requestType == OChannelBinaryProtocol.REQUEST_CONNECT || requestType == OChannelBinaryProtocol.REQUEST_DB_OPEN
        || requestType == OChannelBinaryProtocol.REQUEST_SHUTDOWN || requestType == OChannelBinaryProtocol.REQUEST_DB_REOPEN
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_CONNECT;
  }

  private boolean isDistributed(int requestType) {
    return requestType == OChannelBinaryProtocol.DISTRIBUTED_REQUEST || requestType == OChannelBinaryProtocol.DISTRIBUTED_RESPONSE;
  }

  private boolean isCoordinated(int requestType) {
    return requestType == OChannelBinaryProtocol.DISTRIBUTED_SUBMIT_REQUEST
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_SUBMIT_RESPONSE
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_OPERATION_REQUEST
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_OPERATION_RESPONSE
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_STRUCTURAL_SUBMIT_REQUEST
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_STRUCTURAL_SUBMIT_RESPONSE
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_STRUCTURAL_OPERATION_REQUEST
        || requestType == OChannelBinaryProtocol.DISTRIBUTED_STRUCTURAL_OPERATION_RESPONSE;
  }

  @Override
  protected void execute() throws Exception {
    requestType = -1;

    if (server.rejectRequests()) {
      this.softShutdown();
      return;
    }
    // do not remove this or we will get deadlock upon shutdown.
    if (isShutdownFlag())
      return;

    clientTxId = 0;
    okSent = false;
    try {
      requestType = channel.readByte();

      if (server.rejectRequests()) {
        // MAKE SURE THAT IF THE SERVER IS GOING DOWN THE CONNECTIONS ARE TERMINATED BEFORE HANDLE ANY OPERATIONS
        this.softShutdown();
        if (requestType != OChannelBinaryProtocol.REQUEST_HANDSHAKE && isDistributed(requestType)
            && requestType != OChannelBinaryProtocol.REQUEST_OK_PUSH) {
          clientTxId = channel.readInt();
          channel.clearInput();
          sendError(null, clientTxId, new OOfflineNodeException("Node Shutting down"));
        }
        return;
      }

      if (requestType == OChannelBinaryProtocol.REQUEST_HANDSHAKE) {
        handleHandshake();
        return;
      }
      if (requestType == OChannelBinaryProtocol.REQUEST_OK_PUSH) {
        handlePushResponse();
        return;
      }

      clientTxId = channel.readInt();
      // GET THE CONNECTION IF EXIST
      OClientConnection connection = server.getClientConnectionManager().getConnection(clientTxId, this);
      if (isCoordinated(requestType)) {
        coordinatedRequest(connection, requestType, clientTxId);
      } else if (isDistributed(requestType)) {
        distributedRequest(connection, requestType, clientTxId);
      } else
        sessionRequest(connection, requestType, clientTxId);
    } catch (IOException e) {
      // if an exception arrive to this point we need to kill the current socket.
      sendShutdown();
      throw e;
    }
  }

  private void coordinatedRequest(OClientConnection connection, int requestType, int clientTxId) throws IOException {
    byte[] tokenBytes = channel.readBytes();
    connection = onBeforeOperationalRequest(connection, tokenBytes);
    ((OServerAware) server.getDatabases()).coordinatedRequest(connection, requestType, clientTxId, channel);
  }

  private void handleHandshake() throws IOException {
    short protocolVersion = channel.readShort();
    String driverName = channel.readString();
    String driverVersion = channel.readString();
    byte encoding = channel.readByte();
    byte errorEncoding = channel.readByte();
    OBinaryProtocolHelper.checkProtocolVersion(this, protocolVersion);
    this.handshakeInfo = new HandshakeInfo(protocolVersion, driverName, driverVersion, encoding, errorEncoding);
    this.factory = ONetworkBinaryProtocolFactory.matchProtocol(protocolVersion);
  }

  public void setHandshakeInfo(HandshakeInfo handshakeInfo) {
    this.handshakeInfo = handshakeInfo;
  }

  public boolean shouldReadToken(OClientConnection connection, int requestType) {
    if (handshakeInfo != null || requestType == OChannelBinaryProtocol.DISTRIBUTED_CONNECT) {
      return true;
    } else {
      if (connection == null) {
        return !isHandshaking(requestType) || requestType == OChannelBinaryProtocol.REQUEST_DB_REOPEN;
      } else {
        return Boolean.TRUE.equals(connection.getTokenBased()) && !isHandshaking(requestType);
      }
    }
  }

  private void sessionRequest(OClientConnection connection, int requestType, int clientTxId) {
    long timer = 0;

    timer = Orient.instance().getProfiler().startChrono();
    OLogManager.instance().debug(this, "Request id:" + clientTxId + " type:" + requestType);

    try {
      OBinaryRequest<? extends OBinaryResponse> request = factory.apply(requestType);
      if (request != null) {
        Exception exception = null;

        try {
          byte[] tokenBytes = null;
          if (shouldReadToken(connection, requestType)) {
            tokenBytes = channel.readBytes();
          }
          if (isHandshaking(requestType))
            connection = onBeforeHandshakeRequest(connection, tokenBytes);
          else
            connection = onBeforeOperationalRequest(connection, tokenBytes);
          if (connection != null) {
            connection.getData().commandInfo = request.getDescription();
            connection.setProtocol(this); // This is need for the request command
          }
        } catch (RuntimeException | IOException ex) {
          exception = ex;
        }
        // Also in case of session validation error i read the message from the socket.
        try {
          int protocolVersion = OChannelBinaryProtocol.CURRENT_PROTOCOL_VERSION;
          ORecordSerializer serializer = ORecordSerializerNetworkFactory.INSTANCE.forProtocol(protocolVersion);
          if (connection != null) {
            protocolVersion = connection.getData().protocolVersion;
            serializer = connection.getData().getSerializer();
          }
          request.read(channel, protocolVersion, serializer);
        } catch (IOException e) {
          if (connection != null) {
            connection.endOperation();
          }
          OLogManager.instance().debug(this, "I/O Error on client clientId=%d reqType=%d", clientTxId, requestType, e);
          sendShutdown();
          return;
        }
        if (connection == null && requestType == OChannelBinaryProtocol.REQUEST_DB_CLOSE) {
          //Backward compatible with old clients
          return;
        }

        OBinaryResponse response = null;
        if (exception == null) {
          try {
            if (request.requireServerUser()) {
              checkServerAccess(request.requiredServerRole(), connection);
            }

            if (connection == null)
              throw new ODatabaseException("Required session");

            if (request.requireDatabaseSession()) {
              if (connection.getDatabase() == null)
                throw new ODatabaseException("Required database session");
            }
            response = request.execute(connection.getExecutor());
          } catch (RuntimeException t) {
            // This should be moved in the execution of the command that manipulate data
            if (connection != null && connection.getDatabase() != null) {
              final OSBTreeCollectionManager collectionManager = connection.getDatabase().getSbTreeCollectionManager();
              if (collectionManager != null)
                collectionManager.clearChangedIds();
            }
            exception = t;
          } catch (Error err) {
            connection.release();
            throw err;
          }
        }
        if (exception != null) {
          // TODO: Replace this with build error response
          try {
            okSent = true;
            sendError(connection, clientTxId, exception);
          } catch (IOException e) {
            OLogManager.instance().debug(this, "I/O Error on client clientId=%d reqType=%d", clientTxId, requestType, e);
            sendShutdown();
          } finally {
            afterOperationRequest(connection);
          }
        } else {
          try {
            if (response != null) {
              beginResponse();
              try {
                sendOk(connection, clientTxId);
                response.write(channel, connection.getData().protocolVersion, connection.getData().getSerializer());
              } finally {
                endResponse();
              }
            }
          } catch (IOException e) {
            OLogManager.instance().debug(this, "I/O Error on client clientId=%d reqType=%d", clientTxId, requestType, e);
            sendShutdown();
          } finally {
            afterOperationRequest(connection);
          }
        }
        if (connection != null)
          tokenConnection = Boolean.TRUE.equals(connection.getTokenBased());
      } else {
        OLogManager.instance().error(this, "Request not supported. Code: " + requestType, null);
        handleConnectionError(connection, new ONetworkProtocolException("Request not supported. Code: " + requestType));
        sendShutdown();
      }

    } finally {

      Orient.instance().getProfiler()
          .stopChrono("server.network.requests", "Total received requests", timer, "server.network.requests");

      OSerializationThreadLocal.INSTANCE.get().clear();
    }

  }

  private OClientConnection onBeforeHandshakeRequest(OClientConnection connection, byte[] tokenBytes) {
    try {
      if (requestType != OChannelBinaryProtocol.REQUEST_DB_REOPEN) {
        if (clientTxId >= 0 && connection == null && (requestType == OChannelBinaryProtocol.REQUEST_DB_OPEN
            || requestType == OChannelBinaryProtocol.REQUEST_CONNECT
            || requestType == OChannelBinaryProtocol.DISTRIBUTED_CONNECT)) {
          // THIS EXCEPTION SHULD HAPPEN IN ANY CASE OF OPEN/CONNECT WITH SESSIONID >= 0, BUT FOR COMPATIBILITY IT'S ONLY IF THERE
          // IS NO CONNECTION
          shutdown();
          throw new OIOException("Found unknown session " + clientTxId);
        }
        connection = server.getClientConnectionManager().connect(this);
        connection.getData().sessionId = clientTxId;
        connection.setTokenBytes(null);
        connection.acquire();
      } else {
        connection.validateSession(tokenBytes, server.getTokenHandler(), this);
        server.getClientConnectionManager().disconnect(clientTxId);
        connection = server.getClientConnectionManager().reConnect(this, connection.getTokenBytes(), connection.getToken());
        connection.acquire();
        waitDistribuedIsOnline(connection);
        connection.init(server);

        if (connection.getData().serverUser) {
          connection.setServerUser(server.getUser(connection.getData().serverUsername));
        }
      }
    } catch (RuntimeException e) {
      if (connection != null)
        server.getClientConnectionManager().disconnect(connection);
      ODatabaseRecordThreadLocal.instance().remove();
      throw e;
    }

    connection.statsUpdate();

    OServerPluginHelper.invokeHandlerCallbackOnBeforeClientRequest(server, connection, (byte) requestType);
    return connection;
  }

  private void distributedRequest(OClientConnection connection, int requestType, int clientTxId) {
    long timer = 0;
    try {

      timer = Orient.instance().getProfiler().startChrono();
      byte[] tokenBytes = channel.readBytes();
      connection = onBeforeOperationalRequest(connection, tokenBytes);
      OLogManager.instance().debug(this, "Request id:" + clientTxId + " type:" + requestType);

      try {
        switch (requestType) {
        case OChannelBinaryProtocol.DISTRIBUTED_REQUEST:
          executeDistributedRequest(connection);
          break;

        case OChannelBinaryProtocol.DISTRIBUTED_RESPONSE:
          executeDistributedResponse(connection);
          break;
        }
      } finally {
        requests++;
        afterOperationRequest(connection);
      }

    } catch (Exception t) {
      // IN CASE OF DISTRIBUTED ANY EXCEPTION AT THIS POINT CAUSE THIS CONNECTION TO CLOSE
      OLogManager.instance().warn(this, "I/O Error on distributed channel  clientId=%d reqType=%d", t, clientTxId, requestType);
      sendShutdown();
    } finally {
      Orient.instance().getProfiler()
          .stopChrono("server.network.requests", "Total received requests", timer, "server.network.requests");
    }

  }

  private OClientConnection onBeforeOperationalRequest(OClientConnection connection, byte[] tokenBytes) {
    try {
      if (connection == null && requestType == OChannelBinaryProtocol.REQUEST_DB_CLOSE)
        return null;

      if (handshakeInfo != null) {
        if (connection == null) {
          throw new OTokenSecurityException("missing session and token");
        }
        connection.acquire();
        connection.validateSession(tokenBytes, server.getTokenHandler(), this);
        waitDistribuedIsOnline(connection);
        connection.init(server);
        if (connection.getData().serverUser) {
          connection.setServerUser(server.getUser(connection.getData().serverUsername));
        }
      } else {
        if (connection != null && !Boolean.TRUE.equals(connection.getTokenBased())) {
          // BACKWARD COMPATIBILITY MODE
          connection.setTokenBytes(null);
          connection.acquire();
        } else {
          // STANDARD FLOW
          if (!tokenConnection) {
            // ARRIVED HERE FOR DIRECT TOKEN CONNECTION, BUT OLD STYLE SESSION.
            throw new OIOException("Found unknown session " + clientTxId);
          }
          if (connection == null && tokenBytes != null && tokenBytes.length > 0) {
            // THIS IS THE CASE OF A TOKEN OPERATION WITHOUT HANDSHAKE ON THIS CONNECTION.
            connection = server.getClientConnectionManager().connect(this);
            connection.setDisconnectOnAfter(true);
          }
          if (connection == null) {
            throw new OTokenSecurityException("missing session and token");
          }
          connection.acquire();
          connection.validateSession(tokenBytes, server.getTokenHandler(), this);
          waitDistribuedIsOnline(connection);
          connection.init(server);
          if (connection.getData().serverUser) {
            connection.setServerUser(server.getUser(connection.getData().serverUsername));
          }
        }
      }

      connection.statsUpdate();
      OServerPluginHelper.invokeHandlerCallbackOnBeforeClientRequest(server, connection, (byte) requestType);
    } catch (RuntimeException e) {
      if (connection != null) {
        connection.endOperation();
        server.getClientConnectionManager().disconnect(connection);
      }
      ODatabaseRecordThreadLocal.instance().remove();
      throw e;
    }
    return connection;
  }

  private void waitDistribuedIsOnline(OClientConnection connection) {
    if (requests == 0) {
      final ODistributedServerManager manager = server.getDistributedManager();
      if (manager != null && connection.getDatabase() != null)
        try {
          if (manager.getMessageService() != null) {
            final ODistributedDatabase dDatabase = manager.getMessageService().getDatabase(connection.getDatabase().getName());
            if (dDatabase != null) {
              dDatabase.waitForOnline();
            } else
              manager.waitUntilNodeOnline(manager.getLocalNodeName(), connection.getToken().getDatabase());
          }
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw OException.wrapException(new OInterruptedException("Request interrupted"), e);
        }
    }
  }

  protected void afterOperationRequest(OClientConnection connection) {
    requests++;
    OServerPluginHelper.invokeHandlerCallbackOnAfterClientRequest(server, connection, (byte) requestType);

    if (connection != null) {
      setDataCommandInfo(connection, "Listening");
      connection.endOperation();
      if (connection.isDisconnectOnAfter()) {
        server.getClientConnectionManager().disconnect(connection);
      }
    }
  }

  protected void checkServerAccess(final String iResource, OClientConnection connection) {
    if (connection.getData().protocolVersion <= OChannelBinaryProtocol.PROTOCOL_VERSION_26) {
      if (connection.getServerUser() == null)
        throw new OSecurityAccessException("Server user not authenticated");

      if (!server.isAllowed(connection.getServerUser().name, iResource))
        throw new OSecurityAccessException(
            "User '" + connection.getServerUser().name + "' cannot access to the resource [" + iResource
                + "]. Use another server user or change permission in the file config/orientdb-server-config.xml");
    } else {
      if (!connection.getData().serverUser)
        throw new OSecurityAccessException("Server user not authenticated");

      if (!server.isAllowed(connection.getData().serverUsername, iResource))
        throw new OSecurityAccessException(
            "User '" + connection.getData().serverUsername + "' cannot access to the resource [" + iResource
                + "]. Use another server user or change permission in the file config/orientdb-server-config.xml");
    }
  }

  private void executeDistributedRequest(OClientConnection connection) throws IOException {
    setDataCommandInfo(connection, "Distributed request");

    checkServerAccess("server.replication", connection);

    final ODistributedServerManager manager = server.getDistributedManager();
    final ODistributedRequest req = new ODistributedRequest(manager);

    req.fromStream(channel.getDataInput());

    final String dbName = req.getDatabaseName();
    ODistributedDatabase ddb = null;
    if (dbName != null) {
      ddb = manager.getMessageService().getDatabase(dbName);
      if (ddb == null && req.getTask().isNodeOnlineRequired())
        throw new ODistributedException("Database configuration not found for database '" + req.getDatabaseName() + "'");
    }

    // SET THE SENDER IN THE TASK
    String senderNodeName = manager.getNodeNameById(req.getId().getNodeId());
    req.getTask().setNodeSource(senderNodeName);

    if (ddb != null)
      ddb.processRequest(req, true);
    else {
      manager.executeOnLocalNodeFromRemote(req);
    }
  }

  private void executeDistributedResponse(OClientConnection connection) throws IOException {
    setDataCommandInfo(connection, "Distributed response");

    checkServerAccess("server.replication", connection);

    final ODistributedServerManager manager = server.getDistributedManager();
    final ODistributedResponse response = new ODistributedResponse();

    response.fromStream(channel.getDataInput());

    if (ODistributedServerLog.isDebugEnabled())
      ODistributedServerLog
          .debug(this, manager.getLocalNodeName(), response.getExecutorNodeName(), ODistributedServerLog.DIRECTION.IN,
              "Executing distributed response %s", response);

    // WHILE MSG SERVICE IS UP & RUNNING
    while (manager.getMessageService() == null)
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        return;
      }

    manager.getMessageService().dispatchResponseToThread(response);
  }

  protected void sendError(final OClientConnection connection, final int iClientTxId, final Throwable t) throws IOException {
    channel.acquireWriteLock();
    try {

      channel.writeByte(OChannelBinaryProtocol.RESPONSE_STATUS_ERROR);
      channel.writeInt(iClientTxId);
      if (handshakeInfo != null) {
        byte[] renewedToken = null;
        if (connection != null && connection.getToken() != null) {
          renewedToken = server.getTokenHandler().renewIfNeeded(connection.getToken());
        }
        channel.writeBytes(renewedToken);
        channel.writeByte((byte) requestType);
      } else {
        if (tokenConnection && requestType != OChannelBinaryProtocol.REQUEST_CONNECT && (
            requestType != OChannelBinaryProtocol.REQUEST_DB_OPEN && requestType != OChannelBinaryProtocol.DISTRIBUTED_CONNECT
                && requestType != OChannelBinaryProtocol.REQUEST_SHUTDOWN || (connection != null && connection.getData() != null
                && connection.getData().protocolVersion <= OChannelBinaryProtocol.PROTOCOL_VERSION_32))
            || requestType == OChannelBinaryProtocol.REQUEST_DB_REOPEN) {
          // TODO: Check if the token is expiring and if it is send a new token

          if (connection != null && connection.getToken() != null) {
            byte[] renewedToken = server.getTokenHandler().renewIfNeeded(connection.getToken());
            channel.writeBytes(renewedToken);
          } else
            channel.writeBytes(new byte[] {});
        }
      }
      final Throwable current;
      if (t instanceof OException && t.getCause() instanceof InterruptedException && !server.isActive()) {
        current = new OOfflineNodeException("Node shutting down");
      } else if (t instanceof OLockException && t.getCause() instanceof ODatabaseException)
        // BYPASS THE DB POOL EXCEPTION TO PROPAGATE THE RIGHT SECURITY ONE
        current = t.getCause();
      else
        current = t;

      Map<String, String> messages = new HashMap<>();
      Throwable it = current;
      while (it != null) {
        messages.put(current.getClass().getName(), current.getMessage());
        it = it.getCause();
      }
      final byte[] result;
      if (handshakeInfo == null || handshakeInfo.getErrorEncoding() == OChannelBinaryProtocol.ERROR_MESSAGE_JAVA) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(current);
        objectOutputStream.flush();
        objectOutputStream.close();
        result = outputStream.toByteArray();
      } else if (handshakeInfo.getErrorEncoding() == OChannelBinaryProtocol.ERROR_MESSAGE_STRING) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        current.printStackTrace(new PrintStream(outputStream));
        result = outputStream.toByteArray();
      } else {
        result = new byte[] {};
      }
      OBinaryResponse error;
      if (handshakeInfo != null) {
        OErrorCode code;
        if (current instanceof OCoreException) {
          code = ((OCoreException) current).getErrorCode();
          if (code == null)
            code = OErrorCode.GENERIC_ERROR;
        } else {
          code = OErrorCode.GENERIC_ERROR;
        }
        error = new OError37Response(code, 0, messages, result);
      } else {
        error = new OErrorResponse(messages, result);
      }
      int protocolVersion = OChannelBinaryProtocol.CURRENT_PROTOCOL_VERSION;
      ORecordSerializer serializationImpl = ORecordSerializerNetworkFactory.INSTANCE.current();
      if (connection != null) {
        protocolVersion = connection.getData().protocolVersion;
        serializationImpl = connection.getData().getSerializer();
      }
      error.write(channel, protocolVersion, serializationImpl);
      channel.flush();

      if (OLogManager.instance().isLevelEnabled(logClientExceptions)) {
        if (logClientFullStackTrace)
          OLogManager.instance().log(this, logClientExceptions, "Sent run-time exception to the client %s: %s", t, true, null,
              channel.socket.getRemoteSocketAddress(), t.toString());
        else
          OLogManager.instance().log(this, logClientExceptions, "Sent run-time exception to the client %s: %s", null, true, null,
              channel.socket.getRemoteSocketAddress(), t.toString());
      }
    } catch (Exception e) {
      if (e instanceof SocketException)
        shutdown();
      else
        OLogManager.instance().error(this, "Error during sending an error to client", e);
    } finally {
      if (channel.getLockWrite().isHeldByCurrentThread())
        // NO EXCEPTION SO FAR: UNLOCK IT
        channel.releaseWriteLock();
    }
  }

  protected void beginResponse() {
    channel.acquireWriteLock();
  }

  protected void endResponse() throws IOException {
    channel.flush();
    channel.releaseWriteLock();
  }

  protected void setDataCommandInfo(OClientConnection connection, final String iCommandInfo) {
    if (connection != null)
      connection.getData().commandInfo = iCommandInfo;
  }

  protected void sendOk(OClientConnection connection, final int iClientTxId) throws IOException {
    channel.writeByte(OChannelBinaryProtocol.RESPONSE_STATUS_OK);
    channel.writeInt(iClientTxId);
    okSent = true;
    if (handshakeInfo != null) {
      byte[] renewedToken = null;
      if (connection != null && connection.getToken() != null) {
        renewedToken = server.getTokenHandler().renewIfNeeded(connection.getToken());
      }
      channel.writeBytes(renewedToken);
      channel.writeByte((byte) requestType);
    } else {
      if (connection != null && Boolean.TRUE.equals(connection.getTokenBased()) && connection.getToken() != null
          && requestType != OChannelBinaryProtocol.REQUEST_CONNECT && requestType != OChannelBinaryProtocol.DISTRIBUTED_CONNECT
          && requestType != OChannelBinaryProtocol.REQUEST_DB_OPEN) {
        // TODO: Check if the token is expiring and if it is send a new token
        byte[] renewedToken = server.getTokenHandler().renewIfNeeded(connection.getToken());
        channel.writeBytes(renewedToken);
      }
    }
  }

  protected void handleConnectionError(OClientConnection connection, final Throwable e) {
    try {
      channel.flush();
    } catch (IOException e1) {
      OLogManager.instance().debug(this, "Error during channel flush", e1);
    }
    OLogManager.instance().error(this, "Error executing request", e);
    OServerPluginHelper.invokeHandlerCallbackOnClientError(server, connection, e);
  }

  public static String getRecordSerializerName(OClientConnection connection) {
    return connection.getData().getSerializationImpl();
  }

  @Override
  public int getVersion() {
    return OChannelBinaryProtocol.CURRENT_PROTOCOL_VERSION;
  }

  @Override
  public OChannelBinary getChannel() {
    return channel;
  }

  /**
   * Write a OIdentifiable instance using this format:<br> - 2 bytes: class id [-2=no record, -3=rid, -1=no class id, > -1 = valid]
   * <br> - 1 byte: record type [d,b,f] <br> - 2 bytes: cluster id <br> - 8 bytes: position in cluster <br> - 4 bytes: record
   * version <br> - x bytes: record content <br>
   *
   * @param channel TODO
   */
  public static void writeIdentifiable(OChannelBinary channel, OClientConnection connection, final OIdentifiable o)
      throws IOException {
    if (o == null)
      channel.writeShort(OChannelBinaryProtocol.RECORD_NULL);
    else if (o instanceof ORecordId) {
      channel.writeShort(OChannelBinaryProtocol.RECORD_RID);
      channel.writeRID((ORID) o);
    } else {
      writeRecord(channel, connection, o.getRecord());
    }
  }

  public String getType() {
    return "binary";
  }

  protected void sendErrorOrDropConnection(OClientConnection connection, final int iClientTxId, final Throwable t)
      throws IOException {
    if (okSent || requestType == OChannelBinaryProtocol.REQUEST_DB_CLOSE) {
      handleConnectionError(connection, t);
      sendShutdown();
    } else {
      okSent = true;
      sendError(connection, iClientTxId, t);
    }
  }

  public static byte[] getRecordBytes(OClientConnection connection, final ORecord iRecord) {
    final byte[] stream;
    String dbSerializerName = null;
    if (ODatabaseRecordThreadLocal.instance().getIfDefined() != null)
      dbSerializerName = ((ODatabaseDocumentInternal) iRecord.getDatabase()).getSerializer().toString();
    String name = connection.getData().getSerializationImpl();
    if (ORecordInternal.getRecordType(iRecord) == ODocument.RECORD_TYPE && (dbSerializerName == null || !dbSerializerName
        .equals(name))) {
      ((ODocument) iRecord).deserializeFields();
      ORecordSerializer ser = ORecordSerializerFactory.instance().getFormat(name);
      stream = ser.toStream(iRecord, false);
    } else
      stream = iRecord.toStream();

    return stream;
  }

  private static void writeRecord(OChannelBinary channel, OClientConnection connection, final ORecord iRecord) throws IOException {
    channel.writeShort((short) 0);
    channel.writeByte(ORecordInternal.getRecordType(iRecord));
    channel.writeRID(iRecord.getIdentity());
    channel.writeVersion(iRecord.getVersion());
    try {
      final byte[] stream = getRecordBytes(connection, iRecord);

      // TODO: This Logic should not be here provide an api in the Serializer if asked for trimmed content.
      int realLength = trimCsvSerializedContent(connection, stream);

      channel.writeBytes(stream, realLength);
    } catch (Exception e) {
      channel.writeBytes(null);
      final String message = "Error on unmarshalling record " + iRecord.getIdentity().toString() + " (" + e + ")";

      throw OException.wrapException(new OSerializationException(message), e);
    }
  }

  protected static int trimCsvSerializedContent(OClientConnection connection, final byte[] stream) {
    int realLength = stream.length;
    final ODatabaseDocumentInternal db = ODatabaseRecordThreadLocal.instance().getIfDefined();
    if (db != null && db instanceof ODatabaseDocument) {
      if (ORecordSerializerSchemaAware2CSV.NAME.equals(connection.getData().getSerializationImpl())) {
        // TRIM TAILING SPACES (DUE TO OVERSIZE)
        for (int i = stream.length - 1; i > -1; --i) {
          if (stream[i] == 32)
            --realLength;
          else
            break;
        }

      }
    }
    return realLength;
  }

  public int getRequestType() {
    return requestType;
  }

  public String getRemoteAddress() {
    final Socket socket = getChannel().socket;
    if (socket != null) {
      final InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
      return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort();
    }
    return null;
  }

  @Override
  public OBinaryRequestExecutor executor(OClientConnection connection) {
    return new OConnectionBinaryExecutor(connection, server, handshakeInfo);
  }

  public OBinaryPushResponse push(OBinaryPushRequest request) throws IOException {
    expectedPushResponse = request.createResponse();
    channel.acquireWriteLock();
    try {
      channel.writeByte(OChannelBinaryProtocol.PUSH_DATA);
      channel.writeByte(request.getPushCommand());
      request.write(channel);
      channel.flush();
      if (expectedPushResponse != null) {
        try {
          return pushResponse.take();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        }
      }
    } finally {
      channel.releaseWriteLock();
    }
    return null;
  }

  private void handlePushResponse() throws IOException {
    expectedPushResponse.read(channel);
    this.pushResponse.offer(expectedPushResponse);
  }

}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.config;

import com.orientechnologies.common.concur.lock.OReadersWriterSpinLock;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.conflict.ORecordConflictStrategyFactory;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.id.OImmutableRecordId;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.OBlob;
import com.orientechnologies.orient.core.serialization.OSerializableStream;
import com.orientechnologies.orient.core.storage.disk.OLocalPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;

import java.io.IOException;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Versions:
 *

 *
3 = introduced file directory in physical segments and data-segment id in clusters

 *
4 = ??

 *
5 = ??

 *
6 = ??

 *
7 = ??

 *
8 = introduced cluster selection strategy as string

 *
9 = introduced minimumclusters as string

 *
12 = introduced record conflict strategy as string in both storage and paginated clusters

 *
13 = introduced cluster status to manage cluster as "offline" with the new command "alter cluster status offline". Removed
 * data segments

 *
14 = no changes, but version was incremented

 *
15 = introduced encryption and encryptionKey

 *
18 = we keep version of product release under which storage was created

 *
19 = Page size and related parameters are stored inside configuration

 *

 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
@SuppressWarnings("serial")
public class OStorageConfigurationImpl implements OSerializableStream, OStorageConfiguration {
  private static final ORecordId CONFIG_RID = new OImmutableRecordId(0, 0);

  protected final OReadersWriterSpinLock lock = new OReadersWriterSpinLock();

  private                 String                                 charset;
  private final           List<OStorageEntryConfiguration>       properties = new ArrayList<>();
  private final transient OAbstractPaginatedStorage              storage;
  private                 OContextConfiguration                  configuration;
  private                 int                                    version;
  private                 String                                 name;
  private                 String                                 schemaRecordId;
  private                 String                                 dictionaryRecordId;
  private                 String                                 indexMgrRecordId;
  private                 String                                 dateFormat;
  private                 String                                 dateTimeFormat;
  private                 int                                    binaryFormatVersion;
  private                 OStorageSegmentConfiguration           fileTemplate;
  private                 List<OStorageClusterConfiguration>     clusters;
  private                 String                                 localeLanguage;
  private                 String                                 localeCountry;
  private                 TimeZone                               timeZone;
  private transient       Locale                                 localeInstance;
  private                 String                                 clusterSelection;
  private                 String                                 conflictStrategy;
  private                 String                                 recordSerializer;
  private                 int                                    recordSerializerVersion;
  private                 ConcurrentMap<String, IndexEngineData> indexEngines;

  /*
   * Page size and parameters which depend on page size.
   * Such as minimum size of page in free list and maximum size of key in b-tree.
   */
  private int pageSize         = -1;
  private int freeListBoundary = -1;
  private int maxKeySize       = -1;

  private transient boolean                             validation = true;
  protected         OStorageConfigurationUpdateListener updateListener;
  /**
   * Version of product release under which storage was created
   */
  private           String                              createdAtVersion;

  protected final Charset streamCharset;

  public OStorageConfigurationImpl(final OAbstractPaginatedStorage iStorage, Charset streamCharset) {
    lock.acquireWriteLock();
    try {
      this.streamCharset = streamCharset;

      storage = iStorage;

      initConfiguration(new OContextConfiguration());
      clear();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public int getPageSize() {
    lock.acquireReadLock();
    try {
      return pageSize;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setPageSize(int pageSize) {
    lock.acquireWriteLock();
    try {
      this.pageSize = pageSize;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public int getFreeListBoundary() {
    lock.acquireReadLock();
    try {
      return freeListBoundary;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setFreeListBoundary(int freeListBoundary) {
    lock.acquireWriteLock();
    try {
      this.freeListBoundary = freeListBoundary;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public int getMaxKeySize() {
    lock.acquireReadLock();
    try {
      return maxKeySize;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setMaxKeySize(int maxKeySize) {
    lock.acquireWriteLock();
    try {
      this.maxKeySize = maxKeySize;
    } finally {
      lock.releaseWriteLock();
    }
  }

  /**
   * Sets version of product release under which storage was created.
   */
  public void setCreationVersion(String version) {
    lock.acquireWriteLock();
    try {
      this.createdAtVersion = version;
    } finally {
      lock.releaseWriteLock();
    }
  }

  /**
   * @return version of product release under which storage was created.
   */
  public String getCreatedAtVersion() {
    lock.acquireReadLock();
    try {
      return createdAtVersion;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void initConfiguration(OContextConfiguration conf) {
    lock.acquireWriteLock();
    try {
      this.configuration = conf;
    } finally {
      lock.releaseWriteLock();
    }
  }

  private void clear() {
    fileTemplate = new OStorageSegmentConfiguration();

    charset = DEFAULT_CHARSET;
    synchronized (properties) {
      properties.clear();
    }

    version = -1;
    name = null;
    schemaRecordId = null;
    dictionaryRecordId = null;
    indexMgrRecordId = null;
    dateFormat = DEFAULT_DATE_FORMAT;
    dateTimeFormat = DEFAULT_DATETIME_FORMAT;
    binaryFormatVersion = 0;
    clusters = Collections.synchronizedList(new ArrayList<>());
    localeLanguage = Locale.getDefault().getLanguage();
    localeCountry = Locale.getDefault().getCountry();
    timeZone = TimeZone.getDefault();
    localeInstance = null;
    clusterSelection = null;
    conflictStrategy = null;

    getContextConfiguration().setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS,
        OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS.getValueAsInteger()); // 0 = AUTOMATIC

    autoInitClusters();

    recordSerializer = null;
    recordSerializerVersion = 0;
    indexEngines = new ConcurrentHashMap<>();
    validation = getContextConfiguration().getValueAsBoolean(OGlobalConfiguration.DB_VALIDATION);

    binaryFormatVersion = CURRENT_BINARY_FORMAT_VERSION;
  }

  private void autoInitClusters() {
    if (getContextConfiguration().getValueAsInteger(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS) == 0) {
      final int cpus = Runtime.getRuntime().availableProcessors();
      getContextConfiguration().setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS, cpus > 64 ? 64 : cpus);
    }
  }

  public String getConflictStrategy() {
    lock.acquireReadLock();
    try {
      return conflictStrategy;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setConflictStrategy(String conflictStrategy) {
    lock.acquireWriteLock();
    try {
      this.conflictStrategy = conflictStrategy;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public OContextConfiguration getContextConfiguration() {
    lock.acquireReadLock();
    try {
      return configuration;
    } finally {
      lock.releaseReadLock();
    }
  }

  /**
   * This method load the record information by the internal cluster segment. It's for compatibility with older database than
   * 0.9.25.
   */
  public OStorageConfigurationImpl load(final OContextConfiguration configuration) throws OSerializationException {
    lock.acquireWriteLock();
    try {
      initConfiguration(configuration);

      final byte[] record = storage.readRecord(CONFIG_RID, null, false, false, null).getResult().buffer;

      if (record == null)
        throw new OStorageException("Cannot load database configuration. The database seems corrupted");

      fromStream(record, 0, record.length, streamCharset);
    } finally {
      lock.releaseWriteLock();
    }

    return this;
  }

  public void update() throws OSerializationException {
    lock.acquireWriteLock();
    try {
      final byte[] record = toStream(streamCharset);
      storage.updateRecord(CONFIG_RID, true, record, -1, OBlob.RECORD_TYPE, 0, null);
      if (updateListener != null) {
        updateListener.onUpdate(this);
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getDirectory() {
    lock.acquireReadLock();
    try {
      if (fileTemplate.location != null) {
        return fileTemplate.getLocation();
      }
    } finally {
      lock.releaseReadLock();
    }

    if (storage instanceof OLocalPaginatedStorage) {
      return ((OLocalPaginatedStorage) storage).getStoragePath().toString();
    } else {
      return null;
    }
  }

  public Locale getLocaleInstance() {
    lock.acquireReadLock();
    try {
      if (localeInstance == null) {
        try {
          localeInstance = new Locale(localeLanguage, localeCountry);
        } catch (RuntimeException e) {
          localeInstance = Locale.getDefault();
          OLogManager.instance()
              .errorNoDb(this, "Error during initialization of locale, default one %s will be used", e, localeInstance);

        }
      }

      return localeInstance;
    } finally {
      lock.releaseReadLock();
    }
  }

  public SimpleDateFormat getDateFormatInstance() {
    lock.acquireReadLock();
    try {
      final SimpleDateFormat dateFormatInstance = new SimpleDateFormat(dateFormat);
      dateFormatInstance.setLenient(false);
      dateFormatInstance.setTimeZone(timeZone);

      return dateFormatInstance;
    } finally {
      lock.releaseReadLock();
    }
  }

  public SimpleDateFormat getDateTimeFormatInstance() {
    lock.acquireReadLock();
    try {
      final SimpleDateFormat dateTimeFormatInstance = new SimpleDateFormat(dateTimeFormat);
      dateTimeFormatInstance.setLenient(false);
      dateTimeFormatInstance.setTimeZone(timeZone);
      return dateTimeFormatInstance;
    } finally {
      lock.releaseReadLock();
    }
  }

  @SuppressWarnings("ConstantConditions")
  public void fromStream(final byte[] stream, int offset, int length, Charset charset) {
    lock.acquireWriteLock();
    try {
      clear();

      final String[] values = new String(stream, offset, length, charset).split("\\|");
      int index = 0;
      version = Integer.parseInt(read(values[index++]));

      if (version < 14)
        throw new OStorageException("cannot open database created with a version before 2.0 ");

      name = read(values[index++]);

      schemaRecordId = read(values[index++]);
      dictionaryRecordId = read(values[index++]);

      indexMgrRecordId = read(values[index++]);

      localeLanguage = read(values[index++]);
      localeCountry = read(values[index++]);

      //@COMPATIBILITY with 2.1 version, in this version locale was not mandatory
      if (localeLanguage == null || localeCountry == null) {
        final Locale locale = Locale.getDefault();

        if (localeLanguage == null)
          OLogManager.instance().warn(this,
              "Information about storage locale is undefined (language is undefined) default locale " + locale + " will be used");

        if (localeCountry == null)
          OLogManager.instance().warn(this,
              "Information about storage locale is undefined (country is undefined) default locale " + locale + " will be used");
      }

      dateFormat = read(values[index++]);
      dateTimeFormat = read(values[index++]);

      timeZone = TimeZone.getTimeZone(read(values[index++]));
      this.charset = read(values[index++]);

      final ORecordConflictStrategyFactory conflictStrategyFactory = Orient.instance().getRecordConflictStrategy();
      conflictStrategy = conflictStrategyFactory.getStrategy(read(values[index++])).getName();

      // @COMPATIBILITY
      index = phySegmentFromStream(values, index, fileTemplate);

      int size = Integer.parseInt(read(values[index++]));

      // PREPARE THE LIST OF CLUSTERS
      clusters.clear();

      String determineStorageCompression = null;

      for (int i = 0; i < size; ++i) {
        final int clusterId = Integer.parseInt(read(values[index++]));

        if (clusterId == -1)
          continue;

        final String clusterName = read(values[index++]);
        //noinspection ResultOfMethodCallIgnored
        read(values[index++]);

        final String clusterType = read(values[index++]);

        final OStorageClusterConfiguration currentCluster;

        switch (clusterType) {
        case "d":
          final boolean cc = Boolean.valueOf(read(values[index++]));
          final float bb = Float.valueOf(read(values[index++]));
          final float aa = Float.valueOf(read(values[index++]));

          final String clusterCompression = read(values[index++]);

          if (determineStorageCompression == null)
            // TRY TO DETERMINE THE STORAGE COMPRESSION. BEFORE VERSION 11 IT WASN'T STORED IN STORAGE CFG, SO GET FROM THE FIRST
            // CLUSTER
            determineStorageCompression = clusterCompression;

          String clusterEncryption = null;
          if (version >= 15) {
            clusterEncryption = read(values[index++]);
          }

          final String clusterConflictStrategy = read(values[index++]);

          OStorageClusterConfiguration.STATUS status;
          status = OStorageClusterConfiguration.STATUS.valueOf(read(values[index++]));

          final int clusterBinaryVersion;
          if (version >= 21) {
            clusterBinaryVersion = Integer.parseInt(read(values[index++]));
          } else {
            clusterBinaryVersion = 0;
          }

          currentCluster = new OStoragePaginatedClusterConfiguration(this, clusterId, clusterName, null, cc, bb, aa,
              clusterCompression, clusterEncryption, configuration.getValueAsString(OGlobalConfiguration.STORAGE_ENCRYPTION_KEY),
              clusterConflictStrategy, status, clusterBinaryVersion);

          break;
        case "p":
          // PHYSICAL CLUSTER
          throw new IllegalArgumentException("Cluster of storage 'local' are not supported since 2.0");
        default:
          throw new IllegalArgumentException("Unsupported cluster type: " + clusterType);
        }

        // MAKE ROOMS, EVENTUALLY FILLING EMPTIES ENTRIES
        for (int c = clusters.size(); c <= clusterId; ++c)
          clusters.add(null);

        clusters.set(clusterId, currentCluster);
      }

      size = Integer.parseInt(read(values[index++]));
      clearProperties();
      for (int i = 0; i < size; ++i)
        setProperty(read(values[index++]), read(values[index++]));

      binaryFormatVersion = Integer.parseInt(read(values[index++]));

      clusterSelection = read(values[index++]);

      setMinimumClusters(Integer.parseInt(read(values[index++])));

      autoInitClusters();

      recordSerializer = read(values[index++]);
      recordSerializerVersion = Integer.parseInt(read(values[index++]));

      // READ THE CONFIGURATION
      final int cfgSize = Integer.parseInt(read(values[index++]));
      for (int i = 0; i < cfgSize; ++i) {
        final String key = read(values[index++]);
        final Object value = read(values[index++]);

        final OGlobalConfiguration cfg = OGlobalConfiguration.findByKey(key);
        if (cfg != null) {
          if (value != null)
            configuration.setValue(key, OType.convert(value, cfg.getType()));
        } else
          OLogManager.instance().warn(this, "Ignored storage configuration because not supported: %s=%s", key, value);
      }

      if (version > 15) {
        final int enginesSize = Integer.parseInt(read(values[index++]));

        for (int i = 0; i < enginesSize; i++) {
          final String name = read(values[index++]);
          final String algorithm = read(values[index++]);
          final String indexType;

          if (version > 16)
            indexType = read(values[index++]);
          else
            indexType = "";

          final byte valueSerializerId = Byte.parseByte(read(values[index++]));
          final byte keySerializerId = Byte.parseByte(read(values[index++]));

          final boolean isAutomatic = Boolean.parseBoolean(read((values[index++])));
          final Boolean durableInNonTxMode;

          if (read(values[index]) == null) {
            durableInNonTxMode = null;
            index++;
          } else
            durableInNonTxMode = Boolean.parseBoolean(read(values[index++]));

          final int version = Integer.parseInt(read(values[index++]));
          final boolean nullValuesSupport = Boolean.parseBoolean(read((values[index++])));
          final int keySize = Integer.parseInt(read(values[index++]));

          String encryption = null;
          String encryptionOptions = null;
          if (this.version > 19) {
            encryption = read(values[index++]);
            encryptionOptions = read(values[index++]);
          }

          final int typesLength = Integer.parseInt(read(values[index++]));
          final OType[] types = new OType[typesLength];

          for (int n = 0; n < types.length; n++) {
            final OType type = OType.valueOf(read(values[index++]));
            types[n] = type;
          }

          final int propertiesSize = Integer.parseInt(read(values[index++]));
          final Map<String, String> engineProperties;
          if (propertiesSize == 0)
            engineProperties = null;
          else {
            engineProperties = new HashMap<>(propertiesSize);
            for (int n = 0; n < propertiesSize; n++) {
              final String key = read(values[index++]);
              final String value = read(values[index++]);
              engineProperties.put(key, value);
            }
          }

          int apiVersion;
          boolean mulitvalue;

          if (this.version >= 22) {
            apiVersion = Integer.parseInt(read(values[index++]));
            mulitvalue = Boolean.parseBoolean(read(values[index++]));
          } else {
            apiVersion = 0;
            mulitvalue = false;
          }

          final IndexEngineData indexEngineData = new IndexEngineData(name, algorithm, indexType, durableInNonTxMode, version,
              apiVersion, mulitvalue, valueSerializerId, keySerializerId, isAutomatic, types, nullValuesSupport, keySize,
              encryption, encryptionOptions, engineProperties);

          indexEngines.put(name, indexEngineData);
        }
      }

      if (version > 17) {
        createdAtVersion = read(values[index++]);
      }

      if (version > 18) {
        pageSize = Integer.parseInt(read(values[index++]));
        freeListBoundary = Integer.parseInt(read(values[index++]));
        //noinspection UnusedAssignment
        maxKeySize = Integer.parseInt(read(values[index++]));
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  /**
   * @deprecated because method uses native encoding use {@link #fromStream(byte[], int, int, Charset)} instead.
   */
  @Override
  @Deprecated
  public OSerializableStream fromStream(final byte[] iStream) throws OSerializationException {
    fromStream(iStream, 0, iStream.length, Charset.defaultCharset());
    return this;
  }

  /**
   * @deprecated because method uses native encoding use {@link #toStream(Charset)} instead.
   */
  @Override
  public byte[] toStream() throws OSerializationException {
    return toStream(Integer.MAX_VALUE, Charset.defaultCharset());
  }

  public byte[] toStream(Charset charset) throws OSerializationException {
    return toStream(Integer.MAX_VALUE, charset);
  }

  /**
   * Added version used for managed Network Versioning.
   */
  public byte[] toStream(final int iNetworkVersion, Charset charset) throws OSerializationException {
    lock.acquireReadLock();
    try {
      final StringBuilder buffer = new StringBuilder(8192);

      write(buffer, CURRENT_VERSION);
      write(buffer, name);

      write(buffer, schemaRecordId);
      write(buffer, dictionaryRecordId);
      write(buffer, indexMgrRecordId);

      write(buffer, localeLanguage);
      write(buffer, localeCountry);
      write(buffer, dateFormat);
      write(buffer, dateTimeFormat);

      write(buffer, timeZone.getID());
      write(buffer, charset);
      if (iNetworkVersion > 24)
        write(buffer, conflictStrategy);

      phySegmentToStream(buffer, fileTemplate);

      write(buffer, clusters.size());
      for (OStorageClusterConfiguration c : clusters) {
        if (c == null) {
          write(buffer, -1);
          continue;
        }

        write(buffer, c.getId());
        write(buffer, c.getName());
        write(buffer, c.getDataSegmentId());

        if (c instanceof OStoragePaginatedClusterConfiguration) {
          write(buffer, "d");

          final OStoragePaginatedClusterConfiguration paginatedClusterConfiguration = (OStoragePaginatedClusterConfiguration) c;

          write(buffer, paginatedClusterConfiguration.useWal);
          write(buffer, paginatedClusterConfiguration.recordOverflowGrowFactor);
          write(buffer, paginatedClusterConfiguration.recordGrowFactor);
          write(buffer, paginatedClusterConfiguration.compression);

          if (iNetworkVersion >= 31)
            write(buffer, paginatedClusterConfiguration.encryption);
          if (iNetworkVersion > 24)
            write(buffer, paginatedClusterConfiguration.conflictStrategy);
          if (iNetworkVersion > 25)
            write(buffer, paginatedClusterConfiguration.getStatus().name());

          if (iNetworkVersion >= Integer.MAX_VALUE) {
            write(buffer, paginatedClusterConfiguration.getBinaryVersion());
          }
        }
      }
      if (iNetworkVersion <= 25) {
        // dataSegment array
        write(buffer, 0);
        // tx Segment File
        write(buffer, "");
        write(buffer, "");
        write(buffer, 0);
        // tx segment flags
        write(buffer, false);
        write(buffer, false);
      }
      synchronized (properties) {
        write(buffer, properties.size());
        for (OStorageEntryConfiguration e : properties)
          entryToStream(buffer, e);
      }

      write(buffer, binaryFormatVersion);
      write(buffer, clusterSelection);
      write(buffer, getMinimumClusters());

      if (iNetworkVersion > 24) {
        write(buffer, recordSerializer);
        write(buffer, recordSerializerVersion);

        // WRITE CONFIGURATION
        write(buffer, configuration.getContextSize());
        for (String k : configuration.getContextKeys()) {
          final OGlobalConfiguration cfg = OGlobalConfiguration.findByKey(k);
          write(buffer, k);
          if (cfg != null) {
            write(buffer, cfg.isHidden() ? null : configuration.getValueAsString(cfg));
          } else {
            write(buffer, null);
            OLogManager.instance().warn(this, "Storing configuration for property:'" + k + "' not existing in current version");
          }
        }
      }

      write(buffer, indexEngines.size());
      for (IndexEngineData engineData : indexEngines.values()) {
        write(buffer, engineData.getName());
        write(buffer, engineData.getAlgorithm());
        write(buffer, engineData.getIndexType() == null ? "" : engineData.getIndexType());

        write(buffer, engineData.getValueSerializerId());
        write(buffer, engineData.getKeySerializedId());

        write(buffer, engineData.isAutomatic());
        write(buffer, engineData.getDurableInNonTxMode());

        write(buffer, engineData.getVersion());
        write(buffer, engineData.isNullValuesSupport());
        write(buffer, engineData.getKeySize());
        write(buffer, engineData.getEncryption());
        write(buffer, engineData.getEncryptionOptions());

        if (engineData.getKeyTypes() != null) {
          write(buffer, engineData.getKeyTypes().length);
          for (OType type : engineData.getKeyTypes()) {
            write(buffer, type.name());
          }
        } else {
          write(buffer, 0);
        }

        if (engineData.getEngineProperties() == null) {
          write(buffer, 0);
        } else {
          write(buffer, engineData.getEngineProperties().size());
          for (Map.Entry<String, String> property : engineData.getEngineProperties().entrySet()) {
            write(buffer, property.getKey());
            write(buffer, property.getValue());
          }
        }

        write(buffer, engineData.getApiVersion());
        write(buffer, engineData.isMultivalue());
      }

      write(buffer, createdAtVersion);
      write(buffer, pageSize);
      write(buffer, freeListBoundary);
      write(buffer, maxKeySize);

      // PLAIN: ALLOCATE ENOUGH SPACE TO REUSE IT EVERY TIME
      buffer.append("|");

      return buffer.toString().getBytes(charset);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void create() throws IOException {
    lock.acquireWriteLock();
    try {
      storage.createRecord(CONFIG_RID, new byte[] { 0, 0, 0, 0 }, 0, OBlob.RECORD_TYPE, null);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void delete() throws IOException {
    lock.acquireWriteLock();
    try {
      close();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void close() throws IOException {
    lock.acquireWriteLock();
    try {
      clear();
      initConfiguration(new OContextConfiguration());
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void dropCluster(final int iClusterId) {
    lock.acquireWriteLock();
    try {
      if (iClusterId < clusters.size()) {
        clusters.set(iClusterId, null);
        update();
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void addCluster(OStoragePaginatedClusterConfiguration config) {
    lock.acquireWriteLock();
    try {
      if (clusters.size() <= config.id) {
        final int diff = config.id - clusters.size();
        for (int i = 0; i < diff; i++) {
          clusters.add(null);
        }

        clusters.add(config);
      } else {
        if (clusters.get(config.id) != null) {
          throw new OStorageException(
              "Cluster with id " + config.id + " already exists with name '" + clusters.get(config.id).getName() + "'");
        }

        clusters.set(config.id, config);
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void addIndexEngine(String name, IndexEngineData engineData) {
    lock.acquireWriteLock();
    try {
      final IndexEngineData oldEngine = indexEngines.putIfAbsent(name, engineData);

      if (oldEngine != null)
        OLogManager.instance()
            .warn(this, "Index engine with name '" + engineData.getName() + "' already contained in database configuration");

      update();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void deleteIndexEngine(String name) {
    lock.acquireWriteLock();
    try {
      indexEngines.remove(name);
      update();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public Set<String> indexEngines() {
    lock.acquireReadLock();
    try {
      return Collections.unmodifiableSet(indexEngines.keySet());
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public IndexEngineData getIndexEngine(String name) {
    lock.acquireReadLock();
    try {
      return indexEngines.get(name);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setClusterStatus(final int clusterId, final OStorageClusterConfiguration.STATUS iStatus) {
    lock.acquireWriteLock();
    try {
      final OStorageClusterConfiguration clusterCfg = clusters.get(clusterId);
      if (clusterCfg != null)
        clusterCfg.setStatus(iStatus);
      update();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public TimeZone getTimeZone() {
    lock.acquireReadLock();
    try {
      return timeZone;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setTimeZone(final TimeZone timeZone) {
    lock.acquireWriteLock();
    try {
      this.timeZone = timeZone;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getLocaleLanguage() {
    lock.acquireReadLock();
    try {
      return localeLanguage;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setLocaleLanguage(final String iValue) {
    lock.acquireWriteLock();
    try {
      localeLanguage = iValue;
      localeInstance = null;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getLocaleCountry() {
    lock.acquireReadLock();
    try {
      return localeCountry;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setLocaleCountry(final String iValue) {
    lock.acquireWriteLock();
    try {
      localeCountry = iValue;
      localeInstance = null;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getCharset() {
    lock.acquireReadLock();
    try {
      return charset;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setCharset(String charset) {
    lock.acquireWriteLock();
    try {
      this.charset = charset;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getDateFormat() {
    lock.acquireReadLock();
    try {
      return dateFormat;
    } finally {
      lock.releaseReadLock();
    }
  }

  public String getDateTimeFormat() {
    lock.acquireReadLock();
    try {
      return dateTimeFormat;
    } finally {
      lock.releaseReadLock();
    }
  }

  public String getClusterSelection() {
    lock.acquireReadLock();
    try {
      return clusterSelection;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setClusterSelection(final String clusterSelection) {
    lock.acquireWriteLock();
    try {
      this.clusterSelection = clusterSelection;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public int getMinimumClusters() {
    lock.acquireReadLock();
    try {
      final int mc = getContextConfiguration().getValueAsInteger(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS);
      if (mc == 0) {
        autoInitClusters();
        return (Integer) getContextConfiguration().getValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS);
      }
      return mc;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setMinimumClusters(final int minimumClusters) {
    lock.acquireWriteLock();
    try {
      getContextConfiguration().setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS, minimumClusters);
      autoInitClusters();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getRecordSerializer() {
    lock.acquireReadLock();
    try {
      return recordSerializer;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setRecordSerializer(String recordSerializer) {
    lock.acquireWriteLock();
    try {
      this.recordSerializer = recordSerializer;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public int getRecordSerializerVersion() {
    lock.acquireReadLock();
    try {
      return recordSerializerVersion;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setRecordSerializerVersion(int recordSerializerVersion) {
    lock.acquireWriteLock();
    try {
      this.recordSerializerVersion = recordSerializerVersion;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public boolean isStrictSql() {
    return true;
  }

  public List<OStorageEntryConfiguration> getProperties() {
    lock.acquireReadLock();
    try {
      return Collections.unmodifiableList(properties);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setProperty(final String iName, final String iValue) {
    lock.acquireWriteLock();
    try {

      if ("validation".equalsIgnoreCase(iName))
        validation = "true".equalsIgnoreCase(iValue);

      for (final OStorageEntryConfiguration e : properties) {
        if (e.name.equalsIgnoreCase(iName)) {
          // FOUND: OVERWRITE IT
          e.value = iValue;
          return;
        }
      }

      // NOT FOUND: CREATE IT
      properties.add(new OStorageEntryConfiguration(iName, iValue));
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getProperty(final String iName) {
    lock.acquireReadLock();
    try {
      for (final OStorageEntryConfiguration e : properties) {
        if (e.name.equalsIgnoreCase(iName))
          return e.value;
      }
      return null;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void removeProperty(final String iName) {
    lock.acquireWriteLock();
    try {
      for (Iterator<OStorageEntryConfiguration> it = properties.iterator(); it.hasNext(); ) {
        final OStorageEntryConfiguration e = it.next();
        if (e.name.equalsIgnoreCase(iName)) {
          it.remove();
          break;
        }
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void clearProperties() {
    lock.acquireWriteLock();
    try {
      properties.clear();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public boolean isValidationEnabled() {
    lock.acquireReadLock();
    try {
      return validation;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setValidation(final boolean validation) {
    setProperty("validation", validation ? "true" : "false");
  }

  private int phySegmentFromStream(final String[] values, int index, final OStorageSegmentConfiguration iSegment) {
    iSegment.location = version > 2 ? read(values[index++]) : null;
    iSegment.maxSize = read(values[index++]);
    iSegment.fileType = read(values[index++]);
    iSegment.fileStartSize = read(values[index++]);
    iSegment.fileMaxSize = read(values[index++]);
    iSegment.fileIncrementSize = read(values[index++]);
    iSegment.defrag = read(values[index++]);

    @SuppressWarnings("ConstantConditions")
    final int size = Integer.parseInt(read(values[index++]));
    iSegment.infoFiles = new OStorageFileConfiguration[size];
    String fileName;
    for (int i = 0; i < size; ++i) {
      fileName = read(values[index++]);

      //noinspection ConstantConditions
      if (!fileName.contains("$")) {
        // @COMPATIBILITY 0.9.25
        int pos = fileName.indexOf("/databases");
        if (pos > -1) {
          fileName = "${" + Orient.ORIENTDB_HOME + "}" + fileName.substring(pos);
        }
      }

      iSegment.infoFiles[i] = new OStorageFileConfiguration(iSegment, fileName, read(values[index++]), read(values[index++]),
          iSegment.fileIncrementSize);
    }

    return index;
  }

  private void phySegmentToStream(final StringBuilder iBuffer, final OStorageSegmentConfiguration iSegment) {
    write(iBuffer, iSegment.location);
    write(iBuffer, iSegment.maxSize);
    write(iBuffer, iSegment.fileType);
    write(iBuffer, iSegment.fileStartSize);
    write(iBuffer, iSegment.fileMaxSize);
    write(iBuffer, iSegment.fileIncrementSize);
    write(iBuffer, iSegment.defrag);

    write(iBuffer, iSegment.infoFiles.length);
    for (OStorageFileConfiguration f : iSegment.infoFiles)
      fileToStream(iBuffer, f);
  }

  private void fileToStream(final StringBuilder iBuffer, final OStorageFileConfiguration iFile) {
    write(iBuffer, iFile.path);
    write(iBuffer, iFile.type);
    write(iBuffer, iFile.maxSize);
  }

  private void entryToStream(final StringBuilder iBuffer, final OStorageEntryConfiguration iEntry) {
    write(iBuffer, iEntry.name);
    write(iBuffer, iEntry.value);
  }

  private String read(final String iValue) {
    if (iValue.equals(" "))
      return null;
    return iValue;
  }

  private void write(final StringBuilder iBuffer, final Object iValue) {
    if (iBuffer.length() > 0)
      iBuffer.append('|');
    iBuffer.append(iValue != null ? iValue.toString() : ' ');
  }

  @Override
  public String getSchemaRecordId() {
    lock.acquireReadLock();
    try {
      return schemaRecordId;
    } finally {
      lock.releaseReadLock();
    }

  }

  public void setSchemaRecordId(String schemaRecordId) {
    lock.acquireWriteLock();
    try {
      this.schemaRecordId = schemaRecordId;
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getIndexMgrRecordId() {
    lock.acquireReadLock();
    try {
      return indexMgrRecordId;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setIndexMgrRecordId(String indexMgrRecordId) {
    lock.acquireWriteLock();
    try {
      this.indexMgrRecordId = indexMgrRecordId;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setDateFormat(String dateFormat) {
    lock.acquireWriteLock();
    try {
      this.dateFormat = dateFormat;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setDateTimeFormat(String dateTimeFormat) {
    lock.acquireWriteLock();
    try {
      this.dateTimeFormat = dateTimeFormat;
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public int getBinaryFormatVersion() {
    lock.acquireReadLock();
    try {
      return binaryFormatVersion;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public String getName() {
    lock.acquireReadLock();
    try {
      return name;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public int getVersion() {
    lock.acquireReadLock();
    try {
      return version;
    } finally {
      lock.releaseReadLock();
    }
  }

  public List<OStorageClusterConfiguration> getClusters() {
    lock.acquireReadLock();
    try {
      return Collections.unmodifiableList(clusters);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setConfigurationUpdateListener(OStorageConfigurationUpdateListener updateListener) {
    lock.acquireWriteLock();
    try {
      this.updateListener = updateListener;
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getUuid() {
    throw  new UnsupportedOperationException();
  }

  @Override
  public void setUuid(String uuid) {
    throw new UnsupportedOperationException();
  }
}


/*
  *
  *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
  *  *
  *  *  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.
  *  *
  *  * For more information: http://orientdb.com
  *
  */

package com.orientechnologies.common.util;

import java.lang.reflect.Array;

import com.orientechnologies.common.log.OLogManager;

@SuppressWarnings("unchecked")
public class OArrays {
  public static <T> T[] copyOf(final T[] iSource, final int iNewSize) {
    return (T[]) copyOf(iSource, iNewSize, iSource.getClass());
  }

  public static <T, U> T[] copyOf(final U[] iSource, final int iNewSize, final Class<? extends T[]> iNewType) {
    final T[] copy = ((Object) iNewType == (Object) Object[].class) ? (T[]) new Object[iNewSize] : (T[]) Array.newInstance(
        iNewType.getComponentType(), iNewSize);
    System.arraycopy(iSource, 0, copy, 0, Math.min(iSource.length, iNewSize));
    return copy;
  }

  public static <S> S[] copyOfRange(final S[] iSource, final int iBegin, final int iEnd) {
    return copyOfRange(iSource, iBegin, iEnd, (Class<S[]>) iSource.getClass());
  }

  public static <D, S> D[] copyOfRange(final S[] iSource, final int iBegin, final int iEnd, final Class<? extends D[]> iClass) {
    final int newLength = iEnd - iBegin;
    if (newLength < 0)
      throw new IllegalArgumentException(iBegin + " > " + iEnd);
    final D[] copy = ((Object) iClass == (Object) Object[].class) ? (D[]) new Object[newLength] : (D[]) Array.newInstance(
        iClass.getComponentType(), newLength);
    System.arraycopy(iSource, iBegin, copy, 0, Math.min(iSource.length - iBegin, newLength));
    return copy;
  }

  public static byte[] copyOfRange(final byte[] iSource, final int iBegin, final int iEnd) {
    final int newLength = iEnd - iBegin;
    if (newLength < 0)
      throw new IllegalArgumentException(iBegin + " > " + iEnd);

    try {
      final byte[] copy = new byte[newLength];
      System.arraycopy(iSource, iBegin, copy, 0, Math.min(iSource.length - iBegin, newLength));
      return copy;
    } catch (OutOfMemoryError e) {
      OLogManager.instance().error(null, "Error on copying buffer of size %d bytes", e, newLength);
      throw e;
    }
  }

  public static int[] copyOf(final int[] iSource, final int iNewSize) {
    final int[] copy = new int[iNewSize];
    System.arraycopy(iSource, 0, copy, 0, Math.min(iSource.length, iNewSize));
    return copy;
  }

  /**
   * Returns true if an arrays contains a value, otherwise false
   */
  public static boolean contains(final int[] iArray, final int iToFind) {
    if (iArray == null || iArray.length == 0)
      return false;

    for (int e : iArray)
      if (e == iToFind)
        return true;

    return false;
  }

  /**
   * Returns true if an arrays contains a value, otherwise false
   */
  public static <T> boolean contains(final T[] iArray, final T iToFind) {
    if (iArray == null || iArray.length == 0)
      return false;

    for (T e : iArray)
      if (e != null && e.equals(iToFind))
        return true;

    return false;
  }

  public static int hash(final Object[] iArray) {
    int hash = 0;
    for (Object o : iArray) {
      if (o != null)
        hash += o.hashCode();
    }
    return hash;
  }

}