/**
 * 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.hadoop.hdfs.server.federation.resolver;

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.hadoop.hdfs.server.federation.resolver.order.DestinationOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A map of the properties and target destinations (name space + path) for
 * a path in the global/federated name space.
 * This data is generated from the @see MountTable records.
 */
public class PathLocation {

  private static final Logger LOG = LoggerFactory.getLogger(PathLocation.class);


  /** Source path in global namespace. */
  private final String sourcePath;

  /** Remote paths in the target name spaces. */
  private final List<RemoteLocation> destinations;
  /** Order for the destinations. */
  private final DestinationOrder destOrder;


  /**
   * Create a new PathLocation.
   *
   * @param source Source path in the global name space.
   * @param dest Destinations of the mount table entry.
   * @param order Order of the locations.
   */
  public PathLocation(
      String source, List<RemoteLocation> dest, DestinationOrder order) {
    this.sourcePath = source;
    this.destinations = Collections.unmodifiableList(dest);
    this.destOrder = order;
  }

  /**
   * Create a new PathLocation with default HASH order.
   *
   * @param source Source path in the global name space.
   * @param dest Destinations of the mount table entry.
   */
  public PathLocation(String source, List<RemoteLocation> dest) {
    this(source, dest, DestinationOrder.HASH);
  }

  /**
   * Create a path location from another path.
   *
   * @param other Other path location to copy from.
   */
  public PathLocation(final PathLocation other) {
    this.sourcePath = other.sourcePath;
    this.destinations = Collections.unmodifiableList(other.destinations);
    this.destOrder = other.destOrder;
  }

  /**
   * Create a path location from another path with the destinations sorted.
   *
   * @param other Other path location to copy from.
   * @param firstNsId Identifier of the namespace to place first.
   */
  @Deprecated
  public PathLocation(PathLocation other, String firstNsId) {
    this.sourcePath = other.sourcePath;
    this.destOrder = other.destOrder;
    this.destinations = orderedNamespaces(other.destinations, firstNsId);
  }

  /**
   * Return a path location with the prioritized destinations based on
   * the current path location.
   *
   * @param base The base path location we'd like to prioritize on.
   * @param firstNsId Identifier of the namespace to place first.
   * @return path location with the prioritized destinations.
   */
  public static PathLocation prioritizeDestination(
      PathLocation base, String firstNsId) {
    List<RemoteLocation> prioritizedDestinations = orderedNamespaces(
        base.destinations, firstNsId);
    return new PathLocation(base.sourcePath, prioritizedDestinations,
        base.destOrder);
  }

  /**
   * Prioritize a location/destination by its name space/nameserviceId.
   * This destination might be used by other threads, so the source is not
   * modifiable.
   *
   * @param original List of destinations to order.
   * @param nsId The name space/nameserviceID to prioritize.
   * @return Prioritized list of detinations that cannot be modified.
   */
  private static List<RemoteLocation> orderedNamespaces(
      final List<RemoteLocation> original, final String nsId) {
    if (original.size() <= 1) {
      return original;
    }

    LinkedList<RemoteLocation> newDestinations = new LinkedList<>();
    boolean found = false;
    for (RemoteLocation dest : original) {
      if (dest.getNameserviceId().equals(nsId)) {
        found = true;
        newDestinations.addFirst(dest);
      } else {
        newDestinations.add(dest);
      }
    }

    if (!found) {
      LOG.debug("Cannot find location with namespace {} in {}",
          nsId, original);
    }
    return Collections.unmodifiableList(newDestinations);
  }

  /**
   * Get the source path in the global namespace for this path location.
   *
   * @return The path in the global namespace.
   */
  public String getSourcePath() {
    return this.sourcePath;
  }

  /**
   * Get the subclusters defined for the destinations.
   *
   * @return Set containing the subclusters.
   */
  public Set<String> getNamespaces() {
    Set<String> namespaces = new HashSet<>();
    List<RemoteLocation> locations = this.getDestinations();
    for (RemoteLocation location : locations) {
      String nsId = location.getNameserviceId();
      namespaces.add(nsId);
    }
    return namespaces;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (RemoteLocation destination : this.destinations) {
      String nsId = destination.getNameserviceId();
      String path = destination.getDest();
      if (sb.length() > 0) {
        sb.append(",");
      }
      sb.append(nsId + "->" + path);
    }
    if (this.destinations.size() > 1) {
      sb.append(" [")
          .append(this.destOrder.toString())
          .append("]");
    }
    return sb.toString();
  }

  /**
   * Check if this location supports multiple clusters/paths.
   *
   * @return If it has multiple destinations.
   */
  public boolean hasMultipleDestinations() {
    return this.destinations.size() > 1;
  }

  /**
   * Get the list of locations found in the mount table.
   * The first result is the highest priority path.
   *
   * @return List of remote locations.
   */
  public List<RemoteLocation> getDestinations() {
    return Collections.unmodifiableList(this.destinations);
  }

  /**
   * Get the order for the destinations.
   *
   * @return Order for the destinations.
   */
  public DestinationOrder getDestinationOrder() {
    return this.destOrder;
  }

  /**
   * Get the default or highest priority location.
   *
   * @return The default location.
   */
  public RemoteLocation getDefaultLocation() {
    if (destinations.isEmpty() || destinations.get(0).getDest() == null) {
      throw new UnsupportedOperationException(
          "Unsupported path " + sourcePath + " please check mount table");
    }
    return destinations.get(0);
  }
}