/*
 * 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 org.apache.geode.distributed.internal.membership;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.geode.DataSerializer;
import org.apache.geode.GemFireConfigException;
import org.apache.geode.InternalGemFireError;
import org.apache.geode.cache.UnsupportedVersionException;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DurableClientAttributes;
import org.apache.geode.distributed.Role;
import org.apache.geode.distributed.internal.ClusterDistributionManager;
import org.apache.geode.distributed.internal.DistributionAdvisor.ProfileId;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.ServerLocation;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.DataSerializableFixedID;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.Version;
import org.apache.geode.internal.cache.versions.VersionSource;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.net.SocketCreator;

/**
 * This is the fundamental representation of a member of a GemFire distributed system.
 */
public class InternalDistributedMember implements DistributedMember, Externalizable,
    DataSerializableFixedID, ProfileId, VersionSource<DistributedMember> {
  private static final long serialVersionUID = -2785249969777296507L;

  // whether to show NetMember components in toString()
  private final boolean SHOW_NETMEMBER =
      Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "show_netmembers");

  protected NetMember netMbr; // the underlying member object, e.g. from JGroups

  /**
   * whether this is a partial member ID (without roles, durable attributes). We use partial IDs in
   * EventID objects to reduce their size. It would be better to use canonical IDs but there is
   * currently no central mechanism that would allow that for both server and client identifiers
   */
  private boolean isPartial;

  /**
   * The roles, if any, of this member. Lazily created first time getRoles() is called.
   */
  private volatile Set<Role> rolesSet = null;

  /** lock object used when getting/setting roles/rolesSet fields */
  private final Object rolesLock = new Object();

  /**
   * Unique tag (such as randomly generated bytes) to help enforce uniqueness. Note: this should be
   * displayable.
   */
  private String uniqueTag = null;

  /** serialization bit flag */
  private static final int NPD_ENABLED_BIT = 0x1;

  /** serialization bit flag */
  private static final int COORD_ENABLED_BIT = 0x2;

  /** partial ID bit flag */
  private static final int PARTIAL_ID_BIT = 0x4;

  /** product version bit flag */
  private static final int VERSION_BIT = 0x8;

  /**
   * Representing the host name of this member.
   */
  private String hostName = null;

  private transient Version versionObj = Version.CURRENT;

  /** The versions in which this message was modified */
  private static final Version[] dsfidVersions = new Version[] {Version.GFE_71, Version.GFE_90};

  private void defaultToCurrentHost() {
    netMbr.setProcessId(OSProcess.getId());
    try {
      if (SocketCreator.resolve_dns) {
        this.hostName = SocketCreator.getHostName(SocketCreator.getLocalHost());
      } else {
        this.hostName = SocketCreator.getLocalHost().getHostAddress();
      }
    } catch (UnknownHostException ee) {
      throw new InternalGemFireError(ee);
    }
  }


  // Used only by Externalization
  public InternalDistributedMember() {}

  /**
   * Construct a InternalDistributedMember. All fields are specified.
   * <p>
   *
   * This, and the following constructor are the only valid ways to create an ID for a distributed
   * member for use in the P2P cache. Use of other constructors can break
   * network-partition-detection.
   *
   * @param i the inet address
   * @param p the membership port
   * @param splitBrainEnabled whether this feature is enabled for the member
   * @param canBeCoordinator whether the member is eligible to be the membership coordinator
   * @param attr the member's attributes
   */
  public InternalDistributedMember(InetAddress i, int p, boolean splitBrainEnabled,
      boolean canBeCoordinator, MemberAttributes attr) {

    this.netMbr = MemberFactory.newNetMember(i, p, splitBrainEnabled, canBeCoordinator,
        Version.CURRENT_ORDINAL, attr);

    this.hostName = SocketCreator.resolve_dns ? SocketCreator.getHostName(i) : i.getHostAddress();

    short version = netMbr.getVersionOrdinal();
    try {
      this.versionObj = Version.fromOrdinal(version, false);
    } catch (UnsupportedVersionException e) {
      this.versionObj = Version.CURRENT;
    }
    // checkHostName();
  }


  /**
   * Construct a InternalDistributedMember based on the given NetMember.
   * <p>
   * This is not the preferred way of creating an instance since the NetMember may not have all
   * required information (e.g., a JGroups address without direct-port and other information).
   *
   */
  public InternalDistributedMember(NetMember m) {
    netMbr = m;

    this.hostName = SocketCreator.resolve_dns ? SocketCreator.getHostName(m.getInetAddress())
        : m.getInetAddress().getHostAddress();

    short version = m.getVersionOrdinal();
    try {
      this.versionObj = Version.fromOrdinal(version, false);
    } catch (UnsupportedVersionException e) {
      this.versionObj = Version.CURRENT;
    }
    cachedToString = null;
    this.isPartial = true;
  }

  /**
   * Replace the current NetMember with the given member. This can be used to fill out an
   * InternalDistributedMember that was created from a partial NetMember created by
   * readEssentialData.
   *
   * @param m the replacement NetMember
   */
  public void setNetMember(NetMember m) {
    this.netMbr = m;
  }

  /**
   * Create a InternalDistributedMember referring to the current host (as defined by the given
   * string).
   * <p>
   *
   * <b> THIS METHOD IS FOR TESTING ONLY. DO NOT USE IT TO CREATE IDs FOR USE IN THE PRODUCT. IT
   * DOES NOT PROPERLY INITIALIZE ATTRIBUTES NEEDED FOR P2P FUNCTIONALITY. </b>
   *
   *
   * @param i the hostname, must be for the current host
   * @param p the membership listening port
   * @throws UnknownHostException if the given hostname cannot be resolved
   */
  public InternalDistributedMember(String i, int p) {
    this(i, p, Version.CURRENT);
  }

  /**
   * Creates a new InternalDistributedMember for use in notifying membership listeners. The version
   * information in the ID is set to Version.CURRENT.
   *
   * @param location the coordinates of the server
   */

  public InternalDistributedMember(ServerLocation location) {
    this.hostName = location.getHostName();
    InetAddress addr = null;
    try {
      addr = InetAddress.getByName(this.hostName);
    } catch (UnknownHostException e) {
      throw new GemFireConfigException("Unable to resolve server location " + location, e);
    }
    netMbr = MemberFactory.newNetMember(addr, location.getPort());
    netMbr.setVmKind(ClusterDistributionManager.NORMAL_DM_TYPE);
    versionObj = Version.CURRENT;
    netMbr.setVersion(versionObj);
  }

  /**
   * Create a InternalDistributedMember referring to the current host (as defined by the given
   * string).
   * <p>
   *
   * <b> THIS METHOD IS FOR TESTING ONLY. DO NOT USE IT TO CREATE IDs FOR USE IN THE PRODUCT. IT
   * DOES NOT PROPERLY INITIALIZE ATTRIBUTES NEEDED FOR P2P FUNCTIONALITY. </b>
   *
   *
   * @param i the hostname, must be for the current host
   * @param p the membership listening port
   * @param version the version of this member
   * @throws UnknownHostException if the given hostname cannot be resolved
   */
  public InternalDistributedMember(String i, int p, Version version) {
    this(i, p, version, MemberFactory.newNetMember(i, p));
  }

  /**
   * Create a InternalDistributedMember referring to the current host (as defined by the given
   * string).
   * <p>
   *
   * <b> THIS METHOD IS FOR TESTING ONLY. DO NOT USE IT TO CREATE IDs FOR USE IN THE PRODUCT. IT
   * DOES NOT PROPERLY INITIALIZE ATTRIBUTES NEEDED FOR P2P FUNCTIONALITY. </b>
   **/
  public InternalDistributedMember(String i, int p, Version version, NetMember netMember) {
    netMbr = netMember;
    defaultToCurrentHost();
    netMember.setVmKind(ClusterDistributionManager.NORMAL_DM_TYPE);
    this.versionObj = version;
    netMember.setVersion(version);
  }

  /**
   * Create a InternalDistributedMember referring to the current host (as defined by the given
   * string) with additional info including optional connection name and an optional unique string.
   * Currently these two optional fields (and this constructor) are only used by the
   * LonerDistributionManager.
   * <p>
   *
   * < b> DO NOT USE THIS METHOD TO CREATE ANYTHING OTHER THAN A LONER ID. IT DOES NOT PROPERLY
   * INITIALIZE THE ID. </b>
   *
   * @param host the hostname, must be for the current host
   * @param p the membership listening port
   * @param n gemfire properties connection name
   * @param u unique string used make the member more unique
   * @param vmKind the dmType
   * @param groups the server groups / roles
   * @param attr durable client attributes, if any
   *
   * @throws UnknownHostException if the given hostname cannot be resolved
   */
  public InternalDistributedMember(String host, int p, String n, String u, int vmKind,
      String[] groups, DurableClientAttributes attr) throws UnknownHostException {
    MemberAttributes mattr = new MemberAttributes(p, org.apache.geode.internal.OSProcess.getId(),
        vmKind, -1, n, groups, attr);
    InetAddress addr = SocketCreator.toInetAddress(host);
    netMbr = MemberFactory.newNetMember(addr, p, false, true, Version.CURRENT_ORDINAL, mattr);
    defaultToCurrentHost();
    netMbr.setName(n);
    this.uniqueTag = u;
    netMbr.setVmKind(vmKind);
    netMbr.setDirectPort(p);
    netMbr.setDurableClientAttributes(attr);
    this.hostName = host;
    netMbr.setGroups(groups);
  }

  /**
   * Create a InternalDistributedMember referring to the current host (as defined by the given
   * address).
   * <p>
   *
   * <b> THIS METHOD IS FOR TESTING ONLY. DO NOT USE IT TO CREATE IDs FOR USE IN THE PRODUCT. IT
   * DOES NOT PROPERLY INITIALIZE ATTRIBUTES NEEDED FOR P2P FUNCTIONALITY. </b>
   *
   *
   * @param i the hostname, must be for the current host
   * @param p the membership listening port
   */
  public InternalDistributedMember(InetAddress i, int p) {
    netMbr = MemberFactory.newNetMember(i, p);
    defaultToCurrentHost();
  }

  /**
   * Create a InternalDistributedMember as defined by the given address.
   * <p>
   *
   * <b> THIS METHOD IS FOR TESTING ONLY. DO NOT USE IT TO CREATE IDs FOR USE IN THE PRODUCT. IT
   * DOES NOT PROPERLY INITIALIZE ATTRIBUTES NEEDED FOR P2P FUNCTIONALITY. </b>
   *
   * @param addr address of the server
   * @param p the listening port of the server
   * @param isCurrentHost true if the given host refers to the current host (bridge and gateway use
   *        false to create a temporary id for the OTHER side of a connection)
   */
  public InternalDistributedMember(InetAddress addr, int p, boolean isCurrentHost) {
    netMbr = MemberFactory.newNetMember(addr, p);
    if (isCurrentHost) {
      defaultToCurrentHost();
    }
  }

  /**
   * Return the underlying host address
   *
   * @return the underlying host address
   */
  public InetAddress getInetAddress() {
    return netMbr.getInetAddress();
  }

  public NetMember getNetMember() {
    return netMbr;
  }

  /**
   * Return the underlying port (membership port)
   *
   * @return the underlying membership port
   */
  public int getPort() {
    return netMbr.getPort();
  }


  /**
   * Returns the port on which the direct channel runs
   */
  public int getDirectChannelPort() {
    assert !this.isPartial;
    return netMbr.getDirectPort();
  }

  /**
   * [GemStone] Returns the kind of VM that hosts the distribution manager with this address.
   *
   * @see ClusterDistributionManager#getDMType()
   * @see ClusterDistributionManager#NORMAL_DM_TYPE
   */
  public int getVmKind() {
    return netMbr.getVmKind();
  }

  /**
   * Returns the membership view ID that this member was born in. For backward compatibility reasons
   * this is limited to 16 bits.
   */
  public int getVmViewId() {
    return netMbr.getVmViewId();
  }

  /**
   * Returns an unmodifiable Set of this member's Roles.
   */
  public Set<Role> getRoles() {
    Set<Role> tmpRolesSet = this.rolesSet;
    if (tmpRolesSet != null) {
      return tmpRolesSet;
    }
    assert !this.isPartial;
    synchronized (this.rolesLock) {
      tmpRolesSet = this.rolesSet;
      if (tmpRolesSet == null) {
        final String[] tmpRoles = netMbr.getGroups();
        // convert array of string role names to array of Roles...
        if (tmpRoles == null || tmpRoles.length == 0) {
          tmpRolesSet = Collections.emptySet();
        } else {
          tmpRolesSet = new HashSet<Role>(tmpRoles.length);
          for (int i = 0; i < tmpRoles.length; i++) {
            tmpRolesSet.add(InternalRole.getRole(tmpRoles[i]));
          }
          tmpRolesSet = Collections.unmodifiableSet(tmpRolesSet);
        }
        this.rolesSet = tmpRolesSet;
      }
    }
    Assert.assertTrue(tmpRolesSet != null);
    return tmpRolesSet;
  }

  public List<String> getGroups() {
    return Collections.unmodifiableList(Arrays.asList(netMbr.getGroups()));
  }

  public void setGroups(String[] newGroups) {
    assert !this.isPartial;
    assert newGroups != null;
    synchronized (this.rolesLock) {
      netMbr.setGroups(newGroups);
      this.rolesSet = null;
      this.cachedToString = null;
    }
  }

  public void setVmKind(int p) {
    netMbr.setVmKind(p);
    cachedToString = null;
  }

  public void setVmViewId(int p) {
    netMbr.setVmViewId(p);
    cachedToString = null;
  }

  /**
   * [GemStone] Returns the process id of the VM that hosts the distribution manager with this
   * address.
   *
   * @since GemFire 4.0
   */
  public int getVmPid() {
    return netMbr.getProcessId();
  }

  /**
   * [GemStone] Sets the process id of the VM that hosts the distribution manager with this address.
   *
   * @since GemFire 4.0
   */
  public void setVmPid(int p) {
    netMbr.setProcessId(p);
    cachedToString = null;
  }

  /**
   * Returns the name of this member's distributed system connection or null if no name was
   * specified.
   *
   * @see org.apache.geode.distributed.DistributedSystem#getName
   */
  public String getName() {
    String result = netMbr.getName();
    if (result == null) {
      result = "";
    }
    return result;
  }

  /**
   * Returns this client member's durable attributes or null if no durable attributes were created.
   */
  public DurableClientAttributes getDurableClientAttributes() {
    assert !this.isPartial;
    DurableClientAttributes attributes = netMbr.getDurableClientAttributes();
    if (attributes == null) {
      attributes = new DurableClientAttributes("", 300);
      netMbr.setDurableClientAttributes(attributes);
    }
    return netMbr.getDurableClientAttributes();
  }

  /**
   * implements the java.lang.Comparable interface
   *
   * @see java.lang.Comparable
   * @param o - the Object to be compared
   * @return a negative integer, zero, or a positive integer as this object is less than, equal to,
   *         or greater than the specified object.
   * @exception java.lang.ClassCastException - if the specified object's type prevents it from being
   *            compared to this Object.
   */
  public int compareTo(DistributedMember o) {
    return compareTo(o, true);
  }

  public int compareTo(DistributedMember o, boolean checkNetMembersIfEqual) {
    return compareTo(o, checkNetMembersIfEqual, true);
  }

  public int compareTo(DistributedMember o, boolean checkNetMembersIfEqual, boolean verifyViewId) {
    if (this == o) {
      return 0;
    }
    // obligatory type check
    if ((o == null) || !(o instanceof InternalDistributedMember))
      throw new ClassCastException(
          LocalizedStrings.InternalDistributedMember_INTERNALDISTRIBUTEDMEMBERCOMPARETO_COMPARISON_BETWEEN_DIFFERENT_CLASSES
              .toLocalizedString());
    InternalDistributedMember other = (InternalDistributedMember) o;

    int myPort = getPort();
    int otherPort = other.getPort();
    if (myPort < otherPort)
      return -1;
    if (myPort > otherPort)
      return 1;


    InetAddress myAddr = getInetAddress();
    InetAddress otherAddr = other.getInetAddress();

    // Discard null cases
    if (myAddr == null && otherAddr == null) {
      if (myPort < otherPort)
        return -1;
      else if (myPort > otherPort)
        return 1;
      else
        return 0;
    } else if (myAddr == null) {
      return -1;
    } else if (otherAddr == null)
      return 1;

    byte[] myBytes = myAddr.getAddress();
    byte[] otherBytes = otherAddr.getAddress();

    if (myBytes != otherBytes) {
      for (int i = 0; i < myBytes.length; i++) {
        if (i >= otherBytes.length)
          return -1; // same as far as they go, but shorter...
        if (myBytes[i] < otherBytes[i])
          return -1;
        if (myBytes[i] > otherBytes[i])
          return 1;
      }
      if (myBytes.length > otherBytes.length)
        return 1; // same as far as they go, but longer...
    }

    String myName = getName();
    String otherName = other.getName();
    if (!(other.isPartial || this.isPartial)) {
      if (myName == null && otherName == null) {
        // do nothing
      } else if (myName == null) {
        return -1;
      } else if (otherName == null) {
        return 1;
      } else {
        int i = myName.compareTo(otherName);
        if (i != 0) {
          return i;
        }
      }
    }

    if (this.uniqueTag == null && other.uniqueTag == null) {
      if (verifyViewId) {
        // not loners, so look at P2P view ID
        int thisViewId = getVmViewId();
        int otherViewId = other.getVmViewId();
        if (thisViewId >= 0 && otherViewId >= 0) {
          if (thisViewId < otherViewId) {
            return -1;
          } else if (thisViewId > otherViewId) {
            return 1;
          } // else they're the same, so continue
        }
      }
    } else if (this.uniqueTag == null) {
      return -1;
    } else if (other.uniqueTag == null) {
      return 1;
    } else {
      int i = this.uniqueTag.compareTo(other.uniqueTag);
      if (i != 0) {
        return i;
      }
    }

    if (checkNetMembersIfEqual && this.netMbr != null && other.netMbr != null) {
      return this.netMbr.compareAdditionalData(other.netMbr);
    } else {
      return 0;
    }

    // purposely avoid comparing roles
    // @todo Add durableClientAttributes to compare
  }

  /**
   * An InternalDistributedMember created for a test or via readEssentialData will be a Partial ID,
   * possibly not having ancillary info like "name".
   *
   * @return true if this is a partial ID
   */
  public boolean isPartial() {
    return isPartial;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    // GemStone fix for 29125
    if ((obj == null) || !(obj instanceof InternalDistributedMember)) {
      return false;
    }
    return compareTo((InternalDistributedMember) obj) == 0;
  }

  @Override
  public int hashCode() {
    int result = 0;
    result = result + netMbr.getInetAddress().hashCode();
    result = result + getPort();
    return result;
  }

  private String shortName(String hostname) {
    if (hostname == null)
      return "<null inet_addr hostname>";
    int index = hostname.indexOf('.');

    if (index > 0 && !Character.isDigit(hostname.charAt(0)))
      return hostname.substring(0, index);
    else
      return hostname;
  }


  /** the cached string description of this object */
  private transient String cachedToString;

  @Override
  public String toString() {
    String result = cachedToString;
    if (result == null) {
      final StringBuilder sb = new StringBuilder();
      addFixedToString(sb);

      // add version if not current
      short version = netMbr.getVersionOrdinal();
      if (version != Version.CURRENT.ordinal()) {
        sb.append("(version:").append(Version.toString(version)).append(')');
      }

      if (SHOW_NETMEMBER) {
        sb.append("[[").append(this.netMbr).append("]]");
      }

      // leave out Roles on purpose

      // if (netMbr instanceof GMSMember) {
      // sb.append("(UUID=").append(((GMSMember)netMbr).getUUID()).append(")");
      // }

      result = sb.toString();
      cachedToString = result;
    }
    return result;
  }

  public void addFixedToString(StringBuilder sb) {
    // Note: This method is used to generate the HARegion name. If it is changed, memory and GII
    // issues will occur in the case of clients with subscriptions during rolling upgrade.
    String host;

    InetAddress add = getInetAddress();
    if (add.isMulticastAddress())
      host = add.getHostAddress();
    else {
      // host = shortName(add.getHostName());
      host = SocketCreator.resolve_dns ? shortName(this.hostName) : this.hostName;
    }

    sb.append(host);

    String myName = getName();
    int vmPid = netMbr.getProcessId();
    int vmKind = netMbr.getVmKind();
    if (vmPid > 0 || vmKind != ClusterDistributionManager.NORMAL_DM_TYPE || !"".equals(myName)) {
      sb.append("(");

      if (!"".equals(myName)) {
        sb.append(myName);
        if (vmPid > 0) {
          sb.append(':');
        }
      }

      if (vmPid > 0)
        sb.append(Integer.toString(vmPid));

      String vmStr = "";
      switch (vmKind) {
        case ClusterDistributionManager.NORMAL_DM_TYPE:
          // vmStr = ":local"; // let this be silent
          break;
        case ClusterDistributionManager.LOCATOR_DM_TYPE:
          vmStr = ":locator";
          break;
        case ClusterDistributionManager.ADMIN_ONLY_DM_TYPE:
          vmStr = ":admin";
          break;
        case ClusterDistributionManager.LONER_DM_TYPE:
          vmStr = ":loner";
          break;
        default:
          vmStr = ":<unknown:" + vmKind + ">";
          break;
      }
      sb.append(vmStr);
      sb.append(")");
    }
    if (vmKind != ClusterDistributionManager.LONER_DM_TYPE && netMbr.preferredForCoordinator()) {
      sb.append("<ec>");
    }
    int vmViewId = getVmViewId();
    if (vmViewId >= 0) {
      sb.append("<v" + vmViewId + ">");
    }
    sb.append(":");
    sb.append(getPort());

    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      // add some more info that was added in 4.2.1 for loner bridge clients
      // impact on non-bridge loners is ok
      if (this.uniqueTag != null && this.uniqueTag.length() != 0) {
        sb.append(":").append(this.uniqueTag);
      }
      String name = getName();
      if (name.length() != 0) {
        sb.append(":").append(name);
      }
    }
  }

  private short readVersion(int flags, DataInput in) throws IOException {
    if ((flags & VERSION_BIT) != 0) {
      short version = Version.readOrdinal(in);
      this.versionObj = Version.fromOrdinalNoThrow(version, false);
      return version;
    } else {
      // prior to 7.1 member IDs did not serialize their version information
      Version v = InternalDataSerializer.getVersionForDataStreamOrNull(in);
      if (v != null) {
        this.versionObj = v;
        return v.ordinal();
      }
      return Version.CURRENT_ORDINAL;
    }
  }

  /**
   * For Externalizable
   *
   * @see Externalizable
   */
  public void writeExternal(ObjectOutput out) throws IOException {
    Assert.assertTrue(netMbr.getVmKind() > 0);

    // do it the way we like
    byte[] address = getInetAddress().getAddress();

    out.writeInt(address.length); // IPv6 compatible
    out.write(address);
    out.writeInt(getPort());

    DataSerializer.writeString(this.hostName, out);

    int flags = 0;
    if (netMbr.isNetworkPartitionDetectionEnabled())
      flags |= NPD_ENABLED_BIT;
    if (netMbr.preferredForCoordinator())
      flags |= COORD_ENABLED_BIT;
    if (this.isPartial)
      flags |= PARTIAL_ID_BIT;
    // always write product version but enable reading from older versions
    // that do not have it
    flags |= VERSION_BIT;
    out.writeByte((byte) (flags & 0xff));

    out.writeInt(netMbr.getDirectPort());
    out.writeInt(netMbr.getProcessId());
    out.writeInt(netMbr.getVmKind());
    out.writeInt(netMbr.getVmViewId());
    DataSerializer.writeStringArray(netMbr.getGroups(), out);

    DataSerializer.writeString(netMbr.getName(), out);
    DataSerializer.writeString(this.uniqueTag, out);
    DurableClientAttributes attributes = netMbr.getDurableClientAttributes();
    DataSerializer.writeString(attributes == null ? "" : attributes.getId(), out);
    DataSerializer.writeInteger(Integer.valueOf(attributes == null ? 300 : attributes.getTimeout()),
        out);
    Version.writeOrdinal(out, netMbr.getVersionOrdinal(), true);
    netMbr.writeAdditionalData(out);
  }

  /**
   * For Externalizable
   *
   * @see Externalizable
   */
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    int len = in.readInt(); // IPv6 compatible
    byte addr[] = new byte[len];
    in.readFully(addr);
    InetAddress inetAddr = InetAddress.getByAddress(addr);
    int port = in.readInt();

    this.hostName = DataSerializer.readString(in);

    int flags = in.readUnsignedByte();
    boolean sbEnabled = (flags & NPD_ENABLED_BIT) != 0;
    boolean elCoord = (flags & COORD_ENABLED_BIT) != 0;
    this.isPartial = (flags & PARTIAL_ID_BIT) != 0;

    int dcPort = in.readInt();
    int vmPid = in.readInt();
    int vmKind = in.readInt();
    int vmViewId = in.readInt();
    String[] groups = DataSerializer.readStringArray(in);

    String name = DataSerializer.readString(in);
    this.uniqueTag = DataSerializer.readString(in);
    String durableId = DataSerializer.readString(in);
    int durableTimeout = DataSerializer.readInteger(in).intValue();
    DurableClientAttributes durableClientAttributes =
        new DurableClientAttributes(durableId, durableTimeout);

    short version = readVersion(flags, in);

    netMbr = MemberFactory.newNetMember(inetAddr, port, sbEnabled, elCoord, version,
        new MemberAttributes(dcPort, vmPid, vmKind, vmViewId, name, groups,
            durableClientAttributes));
    if (version >= Version.GFE_90.ordinal()) {
      try {
        netMbr.readAdditionalData(in);
      } catch (java.io.EOFException e) {
        // old version
      }
    }

    Assert.assertTrue(netMbr.getVmKind() > 0);
  }

  public int getDSFID() {
    return DISTRIBUTED_MEMBER;
  }

  public void toData(DataOutput out) throws IOException {
    toDataPre_GFE_9_0_0_0(out);
    if (netMbr.getVersionOrdinal() >= Version.GFE_90.ordinal()) {
      getNetMember().writeAdditionalData(out);
    }
  }


  public void toDataPre_GFE_9_0_0_0(DataOutput out) throws IOException {
    // Assert.assertTrue(vmKind > 0);
    // NOTE: If you change the serialized format of this class
    // then bump Connection.HANDSHAKE_VERSION since an
    // instance of this class is sent during Connection handshake.
    DataSerializer.writeInetAddress(getInetAddress(), out);
    out.writeInt(getPort());

    DataSerializer.writeString(this.hostName, out);

    int flags = 0;
    if (netMbr.isNetworkPartitionDetectionEnabled())
      flags |= NPD_ENABLED_BIT;
    if (netMbr.preferredForCoordinator())
      flags |= COORD_ENABLED_BIT;
    if (this.isPartial)
      flags |= PARTIAL_ID_BIT;
    // always write product version but enable reading from older versions
    // that do not have it
    flags |= VERSION_BIT;

    out.writeByte((byte) (flags & 0xff));

    out.writeInt(netMbr.getDirectPort());
    out.writeInt(netMbr.getProcessId());
    int vmKind = netMbr.getVmKind();
    out.writeByte(vmKind);
    DataSerializer.writeStringArray(netMbr.getGroups(), out);

    DataSerializer.writeString(netMbr.getName(), out);
    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      DataSerializer.writeString(this.uniqueTag, out);
    } else { // added in 6.5 for unique identifiers in P2P
      DataSerializer.writeString(String.valueOf(netMbr.getVmViewId()), out);
    }
    DurableClientAttributes durableClientAttributes = netMbr.getDurableClientAttributes();
    DataSerializer
        .writeString(durableClientAttributes == null ? "" : durableClientAttributes.getId(), out);
    DataSerializer.writeInteger(Integer.valueOf(
        durableClientAttributes == null ? 300 : durableClientAttributes.getTimeout()), out);

    short version = netMbr.getVersionOrdinal();
    Version.writeOrdinal(out, version, true);
  }

  public void toDataPre_GFE_7_1_0_0(DataOutput out) throws IOException {
    Assert.assertTrue(netMbr.getVmKind() > 0);
    // disabled to allow post-connect setting of the port for loner systems
    // Assert.assertTrue(getPort() > 0);
    // if (this.getPort() == 0) {
    // InternalDistributedSystem.getLoggerI18n().warning(LocalizedStrings.DEBUG,
    // "Serializing ID with zero port", new Exception("Stack trace"));
    // }

    // NOTE: If you change the serialized format of this class
    // then bump Connection.HANDSHAKE_VERSION since an
    // instance of this class is sent during Connection handshake.
    DataSerializer.writeInetAddress(getInetAddress(), out);
    out.writeInt(getPort());

    DataSerializer.writeString(this.hostName, out);

    int flags = 0;
    if (netMbr.isNetworkPartitionDetectionEnabled())
      flags |= NPD_ENABLED_BIT;
    if (netMbr.preferredForCoordinator())
      flags |= COORD_ENABLED_BIT;
    if (this.isPartial)
      flags |= PARTIAL_ID_BIT;
    out.writeByte((byte) (flags & 0xff));

    out.writeInt(netMbr.getDirectPort());
    out.writeInt(netMbr.getProcessId());
    out.writeByte(netMbr.getVmKind());
    DataSerializer.writeStringArray(netMbr.getGroups(), out);

    DataSerializer.writeString(netMbr.getName(), out);
    int vmKind = netMbr.getVmKind();
    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      DataSerializer.writeString(this.uniqueTag, out);
    } else { // added in 6.5 for unique identifiers in P2P
      DataSerializer.writeString(String.valueOf(netMbr.getVmViewId()), out);
    }
    DurableClientAttributes durableClientAttributes = netMbr.getDurableClientAttributes();
    DataSerializer
        .writeString(durableClientAttributes == null ? "" : durableClientAttributes.getId(), out);
    DataSerializer.writeInteger(Integer.valueOf(
        durableClientAttributes == null ? 300 : durableClientAttributes.getTimeout()), out);
  }


  public void fromData(DataInput in) throws IOException, ClassNotFoundException {
    fromDataPre_GFE_9_0_0_0(in);
    // just in case this is just a non-versioned read
    // from a file we ought to check the version
    if (getNetMember().getVersionOrdinal() >= Version.GFE_90.ordinal()) {
      try {
        netMbr.readAdditionalData(in);
      } catch (EOFException e) {
        // nope - it's from a pre-GEODE client or WAN site
      }
    }
  }

  public void fromDataPre_GFE_9_0_0_0(DataInput in) throws IOException, ClassNotFoundException {
    InetAddress inetAddr = DataSerializer.readInetAddress(in);
    int port = in.readInt();

    this.hostName = DataSerializer.readString(in);

    this.hostName = SocketCreator.resolve_dns
        ? SocketCreator.getCanonicalHostName(inetAddr, hostName) : inetAddr.getHostAddress();

    int flags = in.readUnsignedByte();
    boolean sbEnabled = (flags & NPD_ENABLED_BIT) != 0;
    boolean elCoord = (flags & COORD_ENABLED_BIT) != 0;
    this.isPartial = (flags & PARTIAL_ID_BIT) != 0;

    int dcPort = in.readInt();
    int vmPid = in.readInt();
    int vmKind = in.readUnsignedByte();
    String[] groups = DataSerializer.readStringArray(in);
    int vmViewId = -1;

    String name = DataSerializer.readString(in);
    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      this.uniqueTag = DataSerializer.readString(in);
    } else {
      String str = DataSerializer.readString(in);
      if (str != null) { // backward compatibility from earlier than 6.5
        vmViewId = Integer.parseInt(str);
      }
    }

    String durableId = DataSerializer.readString(in);
    int durableTimeout = DataSerializer.readInteger(in).intValue();
    DurableClientAttributes durableClientAttributes =
        durableId.length() > 0 ? new DurableClientAttributes(durableId, durableTimeout) : null;

    short version = readVersion(flags, in);

    MemberAttributes attr = new MemberAttributes(dcPort, vmPid, vmKind, vmViewId, name, groups,
        durableClientAttributes);
    netMbr = MemberFactory.newNetMember(inetAddr, port, sbEnabled, elCoord, version, attr);

    Assert.assertTrue(netMbr.getVmKind() > 0);
    // Assert.assertTrue(getPort() > 0);
  }

  public void fromDataPre_GFE_7_1_0_0(DataInput in) throws IOException, ClassNotFoundException {
    InetAddress inetAddr = DataSerializer.readInetAddress(in);
    int port = in.readInt();

    this.hostName = DataSerializer.readString(in);

    this.hostName = SocketCreator.resolve_dns
        ? SocketCreator.getCanonicalHostName(inetAddr, hostName) : inetAddr.getHostAddress();

    int flags = in.readUnsignedByte();
    boolean sbEnabled = (flags & NPD_ENABLED_BIT) != 0;
    boolean elCoord = (flags & COORD_ENABLED_BIT) != 0;
    this.isPartial = (flags & PARTIAL_ID_BIT) != 0;

    int dcPort = in.readInt();
    int vmPid = in.readInt();
    int vmKind = in.readUnsignedByte();
    String[] groups = DataSerializer.readStringArray(in);
    int vmViewId = -1;

    String name = DataSerializer.readString(in);
    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      this.uniqueTag = DataSerializer.readString(in);
    } else {
      String str = DataSerializer.readString(in);
      if (str != null) { // backward compatibility from earlier than 6.5
        vmViewId = Integer.parseInt(str);
      }
    }

    String durableId = DataSerializer.readString(in);
    int durableTimeout = DataSerializer.readInteger(in).intValue();
    DurableClientAttributes durableClientAttributes =
        durableId.length() > 0 ? new DurableClientAttributes(durableId, durableTimeout) : null;

    short version = readVersion(flags, in);

    MemberAttributes attr = new MemberAttributes(dcPort, vmPid, vmKind, vmViewId, name, groups,
        durableClientAttributes);
    netMbr = MemberFactory.newNetMember(inetAddr, port, sbEnabled, elCoord, version, attr);

    Assert.assertTrue(netMbr.getVmKind() > 0);
  }

  /** this reads an ID written with writeEssentialData */
  public static InternalDistributedMember readEssentialData(DataInput in)
      throws IOException, ClassNotFoundException {
    final InternalDistributedMember mbr = new InternalDistributedMember();
    mbr._readEssentialData(in);
    return mbr;
  }

  private void _readEssentialData(DataInput in) throws IOException, ClassNotFoundException {
    this.isPartial = true;
    InetAddress inetAddr = DataSerializer.readInetAddress(in);
    int port = in.readInt();

    this.hostName =
        SocketCreator.resolve_dns ? SocketCreator.getHostName(inetAddr) : inetAddr.getHostAddress();

    int flags = in.readUnsignedByte();
    boolean sbEnabled = (flags & NPD_ENABLED_BIT) != 0;
    boolean elCoord = (flags & COORD_ENABLED_BIT) != 0;

    int vmKind = in.readUnsignedByte();
    int vmViewId = -1;

    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      this.uniqueTag = DataSerializer.readString(in);
    } else {
      String str = DataSerializer.readString(in);
      if (str != null) { // backward compatibility from earlier than 6.5
        vmViewId = Integer.parseInt(str);
      }
    }

    String name = DataSerializer.readString(in);

    MemberAttributes attr = new MemberAttributes(-1, -1, vmKind, vmViewId, name, null, null);
    netMbr = MemberFactory.newNetMember(inetAddr, port, sbEnabled, elCoord,
        InternalDataSerializer.getVersionForDataStream(in).ordinal(), attr);

    if (InternalDataSerializer.getVersionForDataStream(in).compareTo(Version.GFE_90) == 0) {
      netMbr.readAdditionalData(in);
    }
  }


  public void writeEssentialData(DataOutput out) throws IOException {
    Assert.assertTrue(netMbr.getVmKind() > 0);
    DataSerializer.writeInetAddress(getInetAddress(), out);
    out.writeInt(getPort());

    int flags = 0;
    if (netMbr.isNetworkPartitionDetectionEnabled())
      flags |= NPD_ENABLED_BIT;
    if (netMbr.preferredForCoordinator())
      flags |= COORD_ENABLED_BIT;
    flags |= PARTIAL_ID_BIT;
    out.writeByte((byte) (flags & 0xff));

    // out.writeInt(dcPort);
    byte vmKind = netMbr.getVmKind();
    out.writeByte(vmKind);

    if (vmKind == ClusterDistributionManager.LONER_DM_TYPE) {
      DataSerializer.writeString(this.uniqueTag, out);
    } else { // added in 6.5 for unique identifiers in P2P
      DataSerializer.writeString(String.valueOf(netMbr.getVmViewId()), out);
    }
    // write name last to fix bug 45160
    DataSerializer.writeString(netMbr.getName(), out);

    Version outputVersion = InternalDataSerializer.getVersionForDataStream(out);
    if (0 <= outputVersion.compareTo(Version.GFE_90)
        && outputVersion.compareTo(Version.GEODE_110) < 0) {
      netMbr.writeAdditionalData(out);
    }
  }

  /**
   * [GemStone] Set the direct channel port
   */
  public void setDirectChannelPort(int p) {
    netMbr.setDirectPort(p);
  }

  /**
   * Set the membership port. This is done in loner systems using client/server connection
   * information to help form a unique ID
   */
  public void setPort(int p) {
    assert netMbr.getVmKind() == ClusterDistributionManager.LONER_DM_TYPE;
    this.netMbr.setPort(p);
    cachedToString = null;
  }

  public String getHost() {
    return this.netMbr.getInetAddress().getCanonicalHostName();
  }

  public int getProcessId() {
    return netMbr.getProcessId();
  }

  public String getId() {
    return toString();
  }

  public void setVersionObjectForTest(Version v) {
    this.versionObj = v;
    netMbr.setVersion(v);
  }

  public Version getVersionObject() {
    return this.versionObj;
  }

  @Override
  public Version[] getSerializationVersions() {
    return dsfidVersions;
  }


  public static class InternalDistributedMemberWrapper {
    InternalDistributedMember mbr;

    public InternalDistributedMemberWrapper(InternalDistributedMember m) {
      this.mbr = m;
    }

    public InternalDistributedMember getMbr() {
      return mbr;
    }

    @Override
    public int hashCode() {
      return mbr.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
      InternalDistributedMember other = ((InternalDistributedMemberWrapper) obj).mbr;
      return mbr.compareTo(other, false, false) == 0;
    }

    @Override
    public String toString() {
      return "InternalDistributedMemberWrapper [mbr=" + mbr + "]";
    }
  }
}
