/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.resource;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Strings;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;

import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.problem.ProblemPlaceholder;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.problem.StandardCodes;
import nz.org.riskscape.rl.TokenTypes;

/**
 * Helper for resolving a resource's URI, e.g. for {@link Bookmark} locations.
 * This centralizes the logic for dealing with string that could be either a
 * Windows or Linux filepath, or a URI like http://foo.bar.com.
 */
public class UriHelper {

  /**
   * Pattern for locations to "sniff" them and see if they look like they start with the protocol of a URI.
   *
   * To match we require a protocol/scheme with two or more alpha characters then a colon. We require
   * two of more to prevent false positives on absolute Windows paths like 'C:\\path\to\file
   */
  public static final Pattern PROTO_ISH = Pattern.compile("^[a-z]{2,}:");
  public static final String FILE_SCHEME = "file";

  /**
   * Escaper that can be used to escape characters in URIs.
   *
   * Encodes spaces to `%20` whilst leaving / path separators alone.
   */
  private static final Escaper URI_ESCAPER = UrlEscapers.urlFragmentEscaper();

  /**
   * Converts a resource location from a String to a URI. This handles:
   * - locations that can be protocol-based (e.g. "https://") as well as filepath-based.
   * - both relative and absolute filepaths.
   * - Windows-based or linux-based filepaths, depending on the system we're running on.
   * - file locations that are relative to some other specified file (as opposed to the PWD).
   */
  public static ResultOrProblems<URI> uriFromLocation(String location, URI relativeTo) {
    relativeTo = removeFragment(relativeTo);

    if (hasUriScheme(location)) {
      try {
        // we've already got a full URI, but it may contain spacecharacters that need escaping.
        // we only escape spaces because we know they must be escaped and doing so is relativey harmless.
        return ResultOrProblems.of(new URI(location.replace(" ", "%20")));
      } catch (URISyntaxException e) {
        return uriProblem(location, e);
      }
    } else if (!isFile(relativeTo)) {
      try {
        // if we are resolving a filepath relativate to a HTTP URI we will need to map
        // any windows style file separators to forward slashes
        location = location.replaceAll("\\\\", "/");
        // URIs need escaping. This is most often needed when file names contain spaces.
        return ResultOrProblems.of(relativeTo.resolve(URI_ESCAPER.escape(location)));
      } catch (IllegalArgumentException ex) {
        return uriProblem(location, ex.getCause());
      }
    } else {
      // we can assume we're dealing with a local file
      Path relative = Paths.get(relativeTo);
      if (!relative.toFile().isDirectory()) {
        relative = relative.getParent();
      }
      // this allows sharing of INI files between windows/linux
      if (File.separator.equals("/")) {
        // we on a *nix type system. map any \ to /
        location = location.replaceAll("\\\\", Matcher.quoteReplacement(File.separator));
      } else {
        // we are on a windows system. replace forward slashes with back slashes
        location = location.replaceAll("/", Matcher.quoteReplacement(File.separator));
      }

      URI locationURI = relative.resolve(location).toUri().normalize();

      // if it came in with a trailing '/', then it goes out with a trailing '/'. But we actually check
      // for trailing File.separator because of the replaceAll two lines up.
      // Sometimes Path.toUri can strip off the trailing slash (i.e. when the location is a non-existent directory)
      if (location.endsWith(File.separator) && !locationURI.toString().endsWith("/")) {
        try {
          locationURI = new URI(locationURI.toString() + "/");
        } catch (URISyntaxException ex) {
          // we don't expect this exception as adding the slash shouldn't create bad URI syntax
          return uriProblem(locationURI.toString() + "/", ex);
        }
      }
      return ResultOrProblems.of(locationURI);
    }
  }

  /**
   * Removes url fragment from file uris so that they're safe to use for converting in to paths
   */
  private static URI removeFragment(URI relativeTo) {
    if (!relativeTo.getScheme().equals(FILE_SCHEME)) {
      return relativeTo;
    }

    if (Strings.isNullOrEmpty(relativeTo.getFragment())) {
      return relativeTo;
    } else {
      String uriString = relativeTo.toString();
      int lastIndexOf = uriString.lastIndexOf('#');

      if (lastIndexOf > 0) {
        return URI.create(uriString.substring(0, lastIndexOf));
      } else {
        return relativeTo;
      }
    }
  }

  /**
   * Similar to {@link #uriFromLocation(java.lang.String, java.net.URI) } except that the resulting URI
   * is forced to be a directory URI by adding a trailing "/" if necessary.
   */
  public static ResultOrProblems<URI> directoryUriFromLocation(String location, URI relativeTo) {
    // make sure the input location has a trailing slash (or maybe a backslash), indicating it's a directory.
    // uriFromLocation should always honour/preserve the trailing slash in the resulting URI
    return uriFromLocation(location.endsWith("/") || location.endsWith(File.separator)
        ? location : location + "/", relativeTo);
  }

  /**
   * If the location passed to it begins with "./", return a URI for that location.
   * Strips quotes from the location before checking for "./". If the location does not begin with "./",
   * returns the argument as passed.
   */
  public static String makeRelativeToIfDotSlash(String location, URI relativeTo) {
    String unquoted = TokenTypes.stripQuotes(location);

    if (unquoted.startsWith("./")) {
      ResultOrProblems<URI> uri = uriFromLocation(unquoted, relativeTo);
      if (!uri.hasProblems()) {
        return uri.get().toString();
      }
    }
    return location;
  }

  private static ResultOrProblems<URI> uriProblem(String location, Throwable uriException) {
    ProblemPlaceholder uriContext = ProblemPlaceholder.of(URI.class, location);
    return ResultOrProblems.failed(Problem.error(StandardCodes.INVALID_BECAUSE,
        uriContext, uriException.getMessage()).withException(uriException).affecting(uriContext));
  }

  /**
   * @return true if the URI specified is a local file, i.e. "file://...".
   */
  public static boolean isFile(URI uri) {
    return FILE_SCHEME.equals(uri.getScheme());
  }

  /**
   * @return true if the location string has a URI scheme specified,
   * i.e. "file://", "http://", etc.
   */
  public static boolean hasUriScheme(String location) {
    return PROTO_ISH.matcher(location).find();
  }

}
