/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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.gemstone.gemfire;

import static org.apache.geode.internal.cache.tier.sockets.ServerConnection.USER_NOT_FOUND;

import java.io.DataInput;
import java.io.DataOutput;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.geode.cache.Cache;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.CacheService;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.tier.sockets.ClientReAuthenticateMessage;
import org.apache.geode.internal.cache.tier.sockets.OldClientSupportService;
import org.apache.geode.internal.serialization.KnownVersion;
import org.apache.geode.internal.serialization.VersionedDataOutputStream;
import org.apache.geode.management.internal.beans.CacheServiceMBeanBase;
import org.apache.geode.security.AuthenticationExpiredException;
import org.apache.geode.security.AuthenticationRequiredException;
import org.apache.geode.util.internal.GeodeGlossary;

import com.gemstone.gemfire.cache.execute.EmtpyRegionFunctionException;

/**
 * Support for old GemFire clients
 */
public class OldClientSupportProvider implements OldClientSupportService {
  static final String GEODE = "org.apache.geode";
  static final String GEMFIRE = "com.gemstone.gemfire";

  static final String ALWAYS_CONVERT_CLASSES_NAME =
      GeodeGlossary.GEMFIRE_PREFIX + "old-client-support.convert-all";

  /** whether to always convert new package names to old on outgoing serialization */
  static final boolean ALWAYS_CONVERT_CLASSES = Boolean.getBoolean(ALWAYS_CONVERT_CLASSES_NAME);

  private final Map<String, String> oldClassNamesToNew = new ConcurrentHashMap<>();
  private final Map<String, String> newClassNamesToOld = new ConcurrentHashMap<>();

  /* returns the cache's OldClientSupportService */
  public static OldClientSupportService getService(Cache cache) {
    return ((InternalCache) cache)
        .getService(OldClientSupportService.class);
  }

  @Override
  public boolean init(final Cache cache) {
    InternalDataSerializer.setOldClientSupportService(this);
    return true;
  }

  @Override
  public Class<? extends CacheService> getInterface() {
    return OldClientSupportService.class;
  }

  @Override
  public CacheServiceMBeanBase getMBean() {
    // no mbean services provided
    return null;
  }

  @Override
  public String processIncomingClassName(String name) {
    // tcpserver was moved to a different package in Geode.
    String oldPackage = "com.gemstone.org.jgroups.stack.tcpserver";
    String newPackage = "org.apache.geode.distributed.internal.tcpserver";
    if (name.startsWith(oldPackage)) {
      String cached = oldClassNamesToNew.get(name);
      if (cached == null) {
        cached = newPackage + name.substring(oldPackage.length());
        oldClassNamesToNew.put(name, cached);
      }
      return cached;
    }
    return processClassName(name, GEMFIRE, GEODE, oldClassNamesToNew);
  }


  @Override
  public String processIncomingClassName(String name, DataInput in) {
    return processIncomingClassName(name);
  }


  @Override
  public String processOutgoingClassName(String name, DataOutput out) {
    // tcpserver was moved to a different package
    String oldPackage = "com.gemstone.org.jgroups.stack.tcpserver";
    String newPackage = "org.apache.geode.distributed.internal.tcpserver";
    if (name.startsWith(newPackage)) {
      return oldPackage + name.substring(newPackage.length());
    }
    if (ALWAYS_CONVERT_CLASSES) {
      return processClassName(name, GEODE, GEMFIRE, newClassNamesToOld);
    }
    // if the client is old then it needs com.gemstone.gemfire package names
    if (out instanceof VersionedDataOutputStream) {
      VersionedDataOutputStream vout = (VersionedDataOutputStream) out;
      KnownVersion version = vout.getVersion();
      if (version != null && version.isOlderThan(KnownVersion.GFE_90)) {
        return processClassName(name, GEODE, GEMFIRE, newClassNamesToOld);
      }
    }
    return name;
  }

  @Override
  public Throwable getThrowable(Throwable theThrowable, KnownVersion clientVersion) {

    if (theThrowable == null) {
      return theThrowable;
    }

    // backward compatibility for authentication expiration
    if (clientVersion.isOlderThan(ClientReAuthenticateMessage.RE_AUTHENTICATION_START_VERSION)) {
      if (theThrowable instanceof AuthenticationExpiredException) {
        return new AuthenticationRequiredException(USER_NOT_FOUND);
      }
      Throwable cause = theThrowable.getCause();
      if (cause instanceof AuthenticationExpiredException) {
        return new AuthenticationRequiredException(USER_NOT_FOUND);
      }
    }

    if (clientVersion.isNotOlderThan(KnownVersion.GFE_90)) {
      return theThrowable;
    }

    String className = theThrowable.getClass().getName();
    // this class has been renamed, so it cannot be automatically translated
    // during java deserialization
    if (className.equals("org.apache.geode.cache.execute.EmptyRegionFunctionException")) {
      return new EmtpyRegionFunctionException(theThrowable.getMessage(), theThrowable.getCause());
    }

    // other exceptions will be translated automatically by receivers
    return theThrowable;
  }


  private String processClassName(String p_className, String oldPackage, String newPackage,
      Map<String, String> cache) {
    String cached = cache.get(p_className);
    if (cached != null) {
      return cached;
    }

    String className = p_className;

    if (className.startsWith(oldPackage)) {
      className = newPackage + className.substring(oldPackage.length());

    } else if (className.startsWith("[") && className.contains("[L" + oldPackage)) {
      int idx = className.indexOf("[L") + 2;
      className =
          className.substring(0, idx) + newPackage + className.substring(idx, oldPackage.length());
    }

    if (className != p_className) {
      cache.put(p_className, className);
    }

    return className;
  }


}
