/*
 * Copyright 2013 Google Inc.
 *
 * 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 com.google.common.jimfs;

import java.nio.file.InvalidPathException;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

/**
 * Windows-style path type.
 *
 * @author Colin Decker
 */
final class WindowsPathType extends PathType {

  /** Windows path type. */
  static final WindowsPathType INSTANCE = new WindowsPathType();

  /**
   * Matches the C:foo\bar path format, which has a root (C:) and names (foo\bar) and matches a path
   * relative to the working directory on that drive. Currently can't support that format as it
   * requires behavior that differs completely from Unix.
   */
  // TODO(cgdecker): Can probably support this at some point
  // It would require:
  // - A method like PathType.isAbsolute(Path) or something to that effect; this would allow
  //   WindowsPathType to distinguish between an absolute root path (C:\) and a relative root
  //   path (C:)
  // - Special handling for relative paths that have a root. This handling would determine the
  //   root directory and then determine the working directory from there. The file system would
  //   still have one working directory; for the root that working directory is under, it is the
  //   working directory. For every other root, the root itself is the working directory.
  private static final Pattern WORKING_DIR_WITH_DRIVE = Pattern.compile("^[a-zA-Z]:([^\\\\].*)?$");

  /** Pattern for matching trailing spaces in file names. */
  private static final Pattern TRAILING_SPACES = Pattern.compile("[ ]+(\\\\|$)");

  private WindowsPathType() {
    super(true, '\\', '/');
  }

  @Override
  public ParseResult parsePath(String path) {
    String original = path;
    path = path.replace('/', '\\');

    if (WORKING_DIR_WITH_DRIVE.matcher(path).matches()) {
      throw new InvalidPathException(
          original,
          "Jimfs does not currently support the Windows syntax for a relative path "
              + "on a specific drive (e.g. \"C:foo\\bar\")");
    }

    String root;
    if (path.startsWith("\\\\")) {
      root = parseUncRoot(path, original);
    } else if (path.startsWith("\\")) {
      throw new InvalidPathException(
          original,
          "Jimfs does not currently support the Windows syntax for an absolute path "
              + "on the current drive (e.g. \"\\foo\\bar\")");
    } else {
      root = parseDriveRoot(path);
    }

    // check for root.length() > 3 because only "C:\" type roots are allowed to have :
    int startIndex = root == null || root.length() > 3 ? 0 : root.length();
    for (int i = startIndex; i < path.length(); i++) {
      char c = path.charAt(i);
      if (isReserved(c)) {
        throw new InvalidPathException(original, "Illegal char <" + c + ">", i);
      }
    }

    Matcher trailingSpaceMatcher = TRAILING_SPACES.matcher(path);
    if (trailingSpaceMatcher.find()) {
      throw new InvalidPathException(original, "Trailing char < >", trailingSpaceMatcher.start());
    }

    if (root != null) {
      path = path.substring(root.length());

      if (!root.endsWith("\\")) {
        root = root + "\\";
      }
    }

    return new ParseResult(root, splitter().split(path));
  }

  /** Pattern for matching UNC \\host\share root syntax. */
  private static final Pattern UNC_ROOT = Pattern.compile("^(\\\\\\\\)([^\\\\]+)?(\\\\[^\\\\]+)?");

  /**
   * Parse the root of a UNC-style path, throwing an exception if the path does not start with a
   * valid UNC root.
   */
  private String parseUncRoot(String path, String original) {
    Matcher uncMatcher = UNC_ROOT.matcher(path);
    if (uncMatcher.find()) {
      String host = uncMatcher.group(2);
      if (host == null) {
        throw new InvalidPathException(original, "UNC path is missing hostname");
      }
      String share = uncMatcher.group(3);
      if (share == null) {
        throw new InvalidPathException(original, "UNC path is missing sharename");
      }

      return path.substring(uncMatcher.start(), uncMatcher.end());
    } else {
      // probably shouldn't ever reach this
      throw new InvalidPathException(original, "Invalid UNC path");
    }
  }

  /** Pattern for matching normal C:\ drive letter root syntax. */
  private static final Pattern DRIVE_LETTER_ROOT = Pattern.compile("^[a-zA-Z]:\\\\");

  /** Parses a normal drive-letter root, e.g. "C:\". */
  @NullableDecl
  private String parseDriveRoot(String path) {
    Matcher drivePathMatcher = DRIVE_LETTER_ROOT.matcher(path);
    if (drivePathMatcher.find()) {
      return path.substring(drivePathMatcher.start(), drivePathMatcher.end());
    }
    return null;
  }

  /** Checks if c is one of the reserved characters that aren't allowed in Windows file names. */
  private static boolean isReserved(char c) {
    switch (c) {
      case '<':
      case '>':
      case ':':
      case '"':
      case '|':
      case '?':
      case '*':
        return true;
      default:
        return c <= 31;
    }
  }

  @Override
  public String toString(@NullableDecl String root, Iterable<String> names) {
    StringBuilder builder = new StringBuilder();
    if (root != null) {
      builder.append(root);
    }
    joiner().appendTo(builder, names);
    return builder.toString();
  }

  @Override
  public String toUriPath(String root, Iterable<String> names, boolean directory) {
    if (root.startsWith("\\\\")) {
      root = root.replace('\\', '/');
    } else {
      root = "/" + root.replace('\\', '/');
    }

    StringBuilder builder = new StringBuilder();
    builder.append(root);

    Iterator<String> iter = names.iterator();
    if (iter.hasNext()) {
      builder.append(iter.next());
      while (iter.hasNext()) {
        builder.append('/').append(iter.next());
      }
    }

    if (directory && builder.charAt(builder.length() - 1) != '/') {
      builder.append('/');
    }

    return builder.toString();
  }

  @Override
  public ParseResult parseUriPath(String uriPath) {
    uriPath = uriPath.replace('/', '\\');
    if (uriPath.charAt(0) == '\\' && uriPath.charAt(1) != '\\') {
      // non-UNC path, so the leading / was just there for the URI path format and isn't part
      // of what should be parsed
      uriPath = uriPath.substring(1);
    }
    return parsePath(uriPath);
  }
}