/*
 * Copyright 2018 Google
 *
 * 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.
 */

#ifndef FIRESTORE_CORE_SRC_MODEL_BASE_PATH_H_
#define FIRESTORE_CORE_SRC_MODEL_BASE_PATH_H_

#include <algorithm>
#include <cctype>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>

#include "Firestore/core/src/util/comparison.h"
#include "Firestore/core/src/util/hard_assert.h"
#include "Firestore/core/src/util/hashing.h"

namespace firebase {
namespace firestore {
namespace model {
namespace impl {

/**
 * BasePath represents a path sequence in the Firestore database. It is composed
 * of an ordered sequence of string segments.
 *
 * BasePath is reassignable and movable. Apart from those, all other mutating
 * operations return new independent instances.
 *
 * ## Subclassing Notes
 *
 * BasePath is strictly meant as a base class for concrete implementations. It
 * doesn't contain a single virtual method, can't be instantiated, and should
 * never be used in any polymorphic way. BasePath is templated to allow static
 * factory methods to return objects of the derived class (the expected
 * inheritance would use CRTP: struct Derived : BasePath<Derived>).
 */
template <typename T>
class BasePath {
 protected:
  using SegmentsT = std::vector<std::string>;

 public:
  using const_iterator = SegmentsT::const_iterator;

  /** Returns i-th segment of the path. */
  const std::string& operator[](const size_t i) const {
    HARD_ASSERT(i < segments_.size(), "index %s out of range", i);
    return segments_[i];
  }

  /** Returns the first segment of the path. */
  const std::string& first_segment() const {
    HARD_ASSERT(!empty(), "Cannot call first_segment on empty path");
    return segments_[0];
  }
  /** Returns the last segment of the path. */
  const std::string& last_segment() const {
    HARD_ASSERT(!empty(), "Cannot call last_segment on empty path");
    return segments_[size() - 1];
  }

  size_t size() const {
    return segments_.size();
  }
  bool empty() const {
    return segments_.empty();
  }

  const_iterator begin() const {
    return segments_.begin();
  }
  const_iterator end() const {
    return segments_.end();
  }

  /**
   * Returns a new path which is the result of concatenating this path with an
   * additional segment.
   */
  T Append(const std::string& segment) const {
    auto appended = segments_;
    appended.push_back(segment);
    return T{std::move(appended)};
  }
  T Append(std::string&& segment) const {
    auto appended = segments_;
    appended.push_back(std::move(segment));
    return T{std::move(appended)};
  }

  /**
   * Returns a new path which is the result of concatenating this path with an
   * another path.
   */
  T Append(const T& path) const {
    auto appended = segments_;
    appended.insert(appended.end(), path.begin(), path.end());
    return T{std::move(appended)};
  }

  /**
   * Returns a new path which is the result of omitting the first n segments of
   * this path.
   */
  T PopFirst(const size_t n = 1) const {
    HARD_ASSERT(n <= size(), "Cannot call PopFirst(%s) on path of length %s", n,
                size());
    return T{begin() + n, end()};
  }

  /**
   * Returns a new path which is the result of omitting the last segment of
   * this path.
   */
  T PopLast() const {
    HARD_ASSERT(!empty(), "Cannot call PopLast() on empty path");
    return T{begin(), end() - 1};
  }

  /**
   * Returns true if this path is a prefix of the given path.
   *
   * Empty path is a prefix of any path. Any path is a prefix of itself.
   */
  bool IsPrefixOf(const T& rhs) const {
    return size() <= rhs.size() && std::equal(begin(), end(), rhs.begin());
  }

  /**
   * Returns true if the given argument is a direct child of this path.
   *
   * Empty path is a parent of any path that consists of a single segment.
   */
  bool IsImmediateParentOf(const T& potential_child) const {
    return size() + 1 == potential_child.size() &&
           std::equal(begin(), end(), potential_child.begin());
  }

  /**
   * Compares the current path against another Path object. Paths are compared
   * segment by segment, prioritizing numeric IDs (e.g., "__id123__") in numeric
   * ascending order, followed by string segments in lexicographical order.
   */
  util::ComparisonResult CompareTo(const T& rhs) const {
    size_t min_size = std::min(size(), rhs.size());
    for (size_t i = 0; i < min_size; ++i) {
      auto cmp = CompareSegments(segments_[i], rhs.segments_[i]);
      if (!util::Same(cmp)) return cmp;
    }
    return util::Compare(size(), rhs.size());
  }

  friend bool operator==(const BasePath& lhs, const BasePath& rhs) {
    return lhs.segments_ == rhs.segments_;
  }

  size_t Hash() const {
    return util::Hash(segments_);
  }

 protected:
  BasePath() = default;
  template <typename IterT>
  BasePath(const IterT begin, const IterT end) : segments_{begin, end} {
  }
  BasePath(std::initializer_list<std::string> list) : segments_{list} {
  }
  explicit BasePath(SegmentsT&& segments) : segments_{std::move(segments)} {
  }

 private:
  SegmentsT segments_;

  static const size_t kNumericIdPrefixLength = 4;
  static const size_t kNumericIdSuffixLength = 2;
  static const size_t kNumericIdTotalOverhead =
      kNumericIdPrefixLength + kNumericIdSuffixLength;

  static util::ComparisonResult CompareSegments(const std::string& lhs,
                                                const std::string& rhs) {
    bool isLhsNumeric = IsNumericId(lhs);
    bool isRhsNumeric = IsNumericId(rhs);

    if (isLhsNumeric && !isRhsNumeric) {
      return util::ComparisonResult::Ascending;
    } else if (!isLhsNumeric && isRhsNumeric) {
      return util::ComparisonResult::Descending;
    } else if (isLhsNumeric && isRhsNumeric) {
      return util::Compare(ExtractNumericId(lhs), ExtractNumericId(rhs));
    } else {
      return util::Compare(lhs, rhs);
    }
  }

  static bool IsNumericId(const std::string& segment) {
    return segment.size() > kNumericIdTotalOverhead &&
           segment.substr(0, kNumericIdPrefixLength) == "__id" &&
           segment.substr(segment.size() - kNumericIdSuffixLength) == "__";
  }

  static int64_t ExtractNumericId(const std::string& segment) {
    return std::stol(segment.substr(kNumericIdPrefixLength,
                                    segment.size() - kNumericIdSuffixLength));
  }
};

}  // namespace impl
}  // namespace model
}  // namespace firestore
}  // namespace firebase

#endif  // FIRESTORE_CORE_SRC_MODEL_BASE_PATH_H_
