/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * 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.
 */
package jetbrains.mps.vfs.path;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.StringTokenizer;
import java.util.stream.Collectors;

/**
 * Represents a familiar path to any file or folder on disk.
 * Also might be a path within the archive (from the archive root)
 * As any path a {@code CommonPath} represents a wrapper around path (= string).
 *
 * To work with archives consider using {@link UniPath}.
 *
 * TODO create smth like FileSystem with WinFileSystem and UnixFileSystem, refactor this class
 *
 * Created by apyshkin on 6/17/16.
 */
@Immutable
public final class CommonPath extends AbstractPath {
  private static final Logger LOG = LogManager.getLogger(CommonPath.class);

  private final static String PARENT_DIR_STR = "..";
  private final static String CUR_DIR_STR = ".";
  private final static String DEFAULT_UNIX_ROOT = "";
  private final static String WIN_ROOT_SEP = ":\\";
  private final static String UNIX_ROOT_SEP = UNIX_SEPARATOR;

  private final String myPath; // equals to {@link #joinParts} contains root

  private final String myRoot; // can be drive letter on windows (X) or DEFAULT_UNIX_ROOT on UNIX only!
  private final List<String> myParts; // the min length is zero
  private final boolean myRelativeFlag; // true <=> myRoot != null
  private final String mySeparator;
  private final char mySeparatorChar;
  private final String myRootSeparator;

  private CommonPath(@NotNull String path) {
    boolean isWin = path.contains(WIN_SEPARATOR);
    mySeparatorChar = isWin ? WIN_SEPARATOR_CHAR : UNIX_SEPARATOR_CHAR;
    mySeparator = String.valueOf(mySeparatorChar);
    myRoot = parseRoot(path);
    myRelativeFlag = (myRoot == null);
    myRootSeparator = calcRootSeparator();
    path = trimRootAndSeparators(path);

    myParts = parseRelativeParts(path);
    myPath = joinParts();
  }

  @Nullable
  private String parseRoot(@NotNull String path) {
    boolean relativeFlag;
    String root;
    if (isWindows()) {
      relativeFlag = path.isEmpty() || !path.contains(WIN_ROOT_SEP);
      root = relativeFlag ? null : path.substring(0, path.indexOf(WIN_ROOT_SEP));
    } else {
      relativeFlag = path.isEmpty() || path.charAt(0) != UNIX_SEPARATOR_CHAR;
      root = relativeFlag ? null : DEFAULT_UNIX_ROOT;
    }
    return root;
  }

  @NotNull
  private List<String> parseRelativeParts(@NotNull String path) {
    List<String> result = new ArrayList<>();
    StringTokenizer tokenizer = new StringTokenizer(path, mySeparator);
    while (tokenizer.hasMoreTokens()) {
      String token = tokenizer.nextToken();
      if (!token.isEmpty()) {
        result.add(token);
      }
    }
    return result;
  }

  /**
   * @param parts each of them must not be null.
   */
  private CommonPath(char separator, @Nullable String root, String... parts) {
    if (parts == null || parts.length == 0 || parts[0] == null) {
      parts = new String[0];
    }
    mySeparatorChar = separator;
    mySeparator = String.valueOf(mySeparatorChar);
    myRelativeFlag = root == null;
    myRootSeparator = calcRootSeparator();
    if (!myRelativeFlag && !isWindows() && !root.equals(DEFAULT_UNIX_ROOT)) {
      throw new InvalidPathException(root, "In UNIX root must be always equal to `" + DEFAULT_UNIX_ROOT + "'.");
    }
    myRoot = root;
    myParts = Arrays.asList(parts).stream().map(path -> path + "").collect(Collectors.toList());
    myPath = joinParts();
  }

  private String calcRootSeparator() {
    if (myRelativeFlag) {
      return null;
    }
    return isWindows() ? WIN_ROOT_SEP : UNIX_ROOT_SEP;
  }

  private String joinParts() {
    StringJoiner joiner = new StringJoiner(mySeparator);
    myParts.forEach(joiner::add);
    String partsString = joiner.toString();
    if (isRelative()) {
      return partsString;
    }
    return myRoot + myRootSeparator + partsString;
  }

  private CommonPath(char separator, @Nullable String root, @NotNull List<String> parts) {
    this(separator, root, parts.toArray(new String[parts.size()]));
  }

  public final boolean isWindows() {
    return mySeparatorChar != UNIX_SEPARATOR_CHAR;
  }

  char getSeparatorChar() {
    return mySeparatorChar;
  }

  /**
   * remove all doubling separators specifically in the start and the end of the path
   */
  private String trimRootAndSeparators(@NotNull String path) {
    if (!isRelative()) {
      path = path.substring(myRoot.length() + myRootSeparator.length());
    }
    int lastNonSepSymb = path.length() - 1;
    while (lastNonSepSymb >= 0 && path.charAt(lastNonSepSymb) == mySeparatorChar) {
      --lastNonSepSymb;
    }
    int index = 0;
    char[] result = new char[path.length()];
    for (int i = 0; i <= lastNonSepSymb; ++i) {
      if (index > 0 && result[index - 1] == mySeparatorChar && mySeparatorChar == path.charAt(i)) {
        continue;
      }
      result[index++] = path.charAt(i);
    }
    return String.valueOf(result, 0, index);
  }

  private static void validate(@NotNull String path) {
    if (path.contains(Path.UNIX_SEPARATOR) && path.contains(Path.WIN_SEPARATOR)) {
      LOG.warn("Path " + path + " contains both unix and windows separators.");
    }
    if (path.contains(Path.ARCHIVE_SEPARATOR)) {
      throw new InvalidPathException(path, "CommonPath is not allowed to include archive separators. One would expect UniPath to be used here.");
    }
  }

  /**
   * parses the path string and creates a common path from it
   */
  public static CommonPath fromString(@NotNull String path) {
    validate(path);
    return new CommonPath(path);
  }

  /**
   * creates an instance
   * @param separator default separator for the path
   * @param root might be drive letter on windows or {@link #DEFAULT_UNIX_ROOT} on UNIX. NB: in the case of null the path will be relative!
   * @param parts each of the parts must not be null
   */
  public static CommonPath fromParts(char separator, @Nullable String root, @Nullable String... parts) {
    root = validateRoot(root, separator);
    validateParts(parts);
    return new CommonPath(separator, root, parts);
  }

  private static String validateRoot(String root, char separator) {
    if (separator == WIN_SEPARATOR_CHAR) {
      if (root != null && root.contains(String.valueOf(separator))) {
        throw new InvalidPathException(root, "The root is not allowed to contain separator " + separator);
      }
    } else if (separator == UNIX_SEPARATOR_CHAR) {
      if (root != null && (!root.equals(DEFAULT_UNIX_ROOT) && !root.equals(UNIX_ROOT_SEP))) {
        throw new InvalidPathException(root, "The UNIX root is allowed to be `" + DEFAULT_UNIX_ROOT + "' or `" + UNIX_ROOT_SEP + "'.");
      }
      if (root != null && root.equals(UNIX_ROOT_SEP)) { // hack to improve user experience
        root = DEFAULT_UNIX_ROOT;
      }
    }
    return root;
  }

  private static void validateParts(String[] parts) {
    if (parts != null) {
      for (String part : parts) {
        if (part == null) {
          throw new InvalidPathException(Arrays.toString(parts), "The null parts are not allowed");
        }
      }
    }
  }

  @Override
  public boolean isRelative() {
    assert myRelativeFlag == (myRoot == null);
    return myRelativeFlag;
  }

  @Override
  public char getSeparator() {
    return mySeparatorChar;
  }

  @NotNull
  @Override
  public List<String> getNames() {
    if (myRelativeFlag) {
      return Collections.unmodifiableList(myParts);
    } else {
      List<String> res = new ArrayList<>();
      assert myRoot != null;
      res.add(myRoot);
      res.addAll(myParts);
      return Collections.unmodifiableList(res);
    }
  }

  @Nullable
  @Override
  public Path getRoot() {
    return CommonPath.fromParts(mySeparatorChar, myRoot);
  }

  @Override
  @Nullable
  public CommonPath getParent() {
    if (myParts.isEmpty()) {
      return null;
    }
    List<String> parentParts = myParts.subList(0, myParts.size() - 1);
    return new CommonPath(mySeparatorChar, myRoot, parentParts);
  }

  @Override
  @NotNull
  public CommonPath toIndependentPath() {
    if (mySeparatorChar == UNIX_SEPARATOR_CHAR) {
      return copy();
    }
    if (!isRelative()) {
      throw new InvalidPathException(myPath, "Cannot convert absolute windows path to unix path");
    }
    assert myRoot == null;
    return new CommonPath(UNIX_SEPARATOR_CHAR, null, myParts);
  }

  @Override
  @NotNull
  public CommonPath toSystemPath() {
    if (mySeparatorChar == SYSTEM_SEPARATOR_CHAR) {
      return copy();
    }
    if (!isRelative()) {
      throw new InvalidPathException(myPath, "Cannot convert absolute windows path to unix path and vice versa");
    }
    assert myRoot == null;
    return new CommonPath(SYSTEM_SEPARATOR_CHAR, null, myParts);
  }

  @Override
  public boolean endsWith(@NotNull String other) {
    return endsWith(CommonPath.fromString(other));
  }

  @Override
  public boolean startsWith(@NotNull String other) {
    return startsWith(CommonPath.fromString(other));
  }

  @NotNull
  @Override
  public CommonPath relativize(@NotNull Path other) {
    return null;
  }

  @NotNull
  @Override
  public CommonPath toAbsolute() {
    return CommonPath.fromString(new File(myPath).getAbsolutePath());
  }

  @NotNull
  @Override
  public final CommonPath toNormal() {
    List<String> newParts = new ArrayList<>();
    for (String part : myParts) {
      if (part.equals(PARENT_DIR_STR)) {
        if (!newParts.isEmpty() && !newParts.get(newParts.size() - 1).equals(PARENT_DIR_STR)) {
          newParts.remove(newParts.size() - 1);
          continue;
        }
      } else if (part.equals(CUR_DIR_STR)) {
        continue;
      }
      newParts.add(part);
    }
    return new CommonPath(mySeparatorChar, myRoot, newParts);
  }

  @NotNull
  @Override
  public CommonPath toCanonical() throws IOException {
    return new CommonPath(new File(myPath).getCanonicalPath());
  }

  @NotNull
  @Override
  public Path resolve(@NotNull Path other) {
    return null;
  }

  @NotNull
  @Override
  public CommonPath resolve(@NotNull String other) {
    return null;
  }

  @Override
  public int compareTo(@NotNull Path path) {
    return toString().compareTo(path.toString()); //FIXME
  }

  @Override
  public String toString() {
    return myPath;
  }

  @NotNull
  @Override
  public CommonPath copy() {
    return new CommonPath(mySeparatorChar, myRoot, myParts);
  }
}
