/*
 * Copyright 2000-2014 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 com.intellij.util;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfoRt;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.StandardFileSystems;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.io.URLUtil;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class Urls {
  private static final Logger LOG = Logger.getInstance(Urls.class);

  // about ";" see WEB-100359
  private static final Pattern URI_PATTERN = Pattern.compile("^([^:/?#]+):(//)?([^/?#]*)([^?#;]*)(.*)");

  @NotNull
  public static Url newUri(@NotNull String scheme, @NotNull String path) {
    return new UrlImpl(scheme, null, path);
  }

  @NotNull
  public static Url newLocalFileUrl(@NotNull String path) {
    return new LocalFileUrl(path);
  }

  @NotNull
  public static Url newLocalFileUrl(@NotNull VirtualFile file) {
    return newLocalFileUrl(file.getPath());
  }

  @NotNull
  public static Url newFromEncoded(@NotNull String url) {
    Url result = parseEncoded(url);
    LOG.assertTrue(result != null, url);
    return result;
  }

  @Nullable
  public static Url parseEncoded(@NotNull String url) {
    return parse(url, false);
  }

  @NotNull
  public static Url newHttpUrl(@NotNull String authority, @Nullable String path) {
    return newUrl("http", authority, path);
  }

  @NotNull
  public static Url newUrl(@NotNull String scheme, @NotNull String authority, @Nullable String path) {
    return new UrlImpl(scheme, authority, path);
  }

  @NotNull
  /**
   * Url will not be normalized (see {@link VfsUtilCore#toIdeaUrl(String)}), parsed as is
   */
  public static Url newFromIdea(@NotNull String url) {
    Url result = parseFromIdea(url);
    LOG.assertTrue(result != null, url);
    return result;
  }

  // java.net.URI.create cannot parse "file:///Test Stuff" - but you don't need to worry about it - this method is aware
  @Nullable
  public static Url parseFromIdea(@NotNull String url) {
    return URLUtil.containsScheme(url) ? parseUrl(url) : newLocalFileUrl(url);
  }

  @Nullable
  public static Url parse(@NotNull String url, boolean asLocalIfNoScheme) {
    if (url.isEmpty()) {
      return null;
    }

    if (asLocalIfNoScheme && !URLUtil.containsScheme(url)) {
      // nodejs debug - files only in local filesystem
      return newLocalFileUrl(url);
    }
    return parseUrl(VfsUtilCore.toIdeaUrl(url));
  }

  @Nullable
  public static URI parseAsJavaUriWithoutParameters(@NotNull String url) {
    Url asUrl = parseUrl(url);
    if (asUrl == null) {
      return null;
    }

    try {
      return toUriWithoutParameters(asUrl);
    }
    catch (Exception e) {
      LOG.info("Cannot parse url " + url, e);
      return null;
    }
  }

  @Nullable
  private static Url parseUrl(@NotNull String url) {
    String urlToParse;
    if (url.startsWith("jar:file://")) {
      urlToParse = url.substring("jar:".length());
    }
    else {
      urlToParse = url;
    }

    Matcher matcher = URI_PATTERN.matcher(urlToParse);
    if (!matcher.matches()) {
      return null;
    }
    String scheme = matcher.group(1);
    if (urlToParse != url) {
      scheme = "jar:" + scheme;
    }

    String authority = StringUtil.nullize(matcher.group(3));

    String path = StringUtil.nullize(matcher.group(4));
    if (path != null) {
      path = FileUtil.toCanonicalUriPath(path);
    }

    if (authority != null && (StandardFileSystems.FILE_PROTOCOL.equals(scheme) || StringUtil.isEmpty(matcher.group(2)))) {
      path = path == null ? authority : (authority + path);
      authority = null;
    }
    return new UrlImpl(scheme, authority, path, matcher.group(5));
  }

  @NotNull
  public static Url newFromVirtualFile(@NotNull VirtualFile file) {
    if (file.isInLocalFileSystem()) {
      return newUri(file.getFileSystem().getProtocol(), file.getPath());
    }
    else {
      Url url = parseUrl(file.getUrl());
      return url == null ? new UrlImpl(file.getPath()) : url;
    }
  }

  public static boolean equalsIgnoreParameters(@NotNull Url url, @NotNull Collection<Url> urls) {
    return equalsIgnoreParameters(url, urls, true);
  }

  public static boolean equalsIgnoreParameters(@NotNull Url url, @NotNull Collection<Url> urls, boolean caseSensitive) {
    for (Url otherUrl : urls) {
      if (equals(url, otherUrl, caseSensitive, true)) {
        return true;
      }
    }
    return false;
  }

  public static boolean equalsIgnoreParameters(@NotNull Url url, @NotNull VirtualFile file) {
    if (file.isInLocalFileSystem()) {
      return url.isInLocalFileSystem() && (SystemInfoRt.isFileSystemCaseSensitive
                                           ? url.getPath().equals(file.getPath()) :
                                           url.getPath().equalsIgnoreCase(file.getPath()));
    }
    else if (url.isInLocalFileSystem()) {
      return false;
    }

    Url fileUrl = parseUrl(file.getUrl());
    return fileUrl != null && fileUrl.equalsIgnoreParameters(url);
  }

  public static boolean equals(@Nullable Url url1, @Nullable Url url2, boolean caseSensitive, boolean ignoreParameters) {
    if (url1 == null || url2 == null){
      return url1 == url2;
    }

    Url o1 = ignoreParameters ? url1.trimParameters() : url1;
    Url o2 = ignoreParameters ? url2.trimParameters() : url2;
    return caseSensitive ? o1.equals(o2) : o1.equalsIgnoreCase(o2);
  }

  @NotNull
  public static URI toUriWithoutParameters(@NotNull Url url) {
    try {
      String externalPath = url.getPath();
      boolean inLocalFileSystem = url.isInLocalFileSystem();
      if (inLocalFileSystem && SystemInfoRt.isWindows && externalPath.charAt(0) != '/') {
        externalPath = '/' + externalPath;
      }
      return new URI(inLocalFileSystem ? "file" : url.getScheme(), inLocalFileSystem ? "" : url.getAuthority(), externalPath, null, null);
    }
    catch (URISyntaxException e) {
      throw new RuntimeException(e);
    }
  }

  public static TObjectHashingStrategy<Url> getCaseInsensitiveUrlHashingStrategy() {
    return CaseInsensitiveUrlHashingStrategy.INSTANCE;
  }

  private static final class CaseInsensitiveUrlHashingStrategy implements TObjectHashingStrategy<Url> {
    private static final TObjectHashingStrategy<Url> INSTANCE = new CaseInsensitiveUrlHashingStrategy();

    @Override
    public int computeHashCode(Url url) {
      return url == null ? 0 : url.hashCodeCaseInsensitive();
    }

    @Override
    public boolean equals(Url url1, Url url2) {
      return Urls.equals(url1, url2, false, false);
    }
  }
}