/*******************************************************************************
 * Copyright (c) Intel Corporation
 * Copyright (c) 2017
 *
 * 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 org.osc.core.server.resolver.impl;

import java.net.URI;

public final class URIUtils {

	private static final String JAR_URI_SCHEME = "jar";

	private URIUtils() {}

	/**
	 * Resolve a URI relative to a base URI.
	 *
	 * @param baseUri
	 *            The base URI which must be absolute.
	 * @param uri
	 *            The relative URI to resolve against the specified base. Note
	 *            that if this URI is absolute, it will be simply returned
	 *            unchanged.
	 * @return An absolute, resolved URI.
	 * @throws {@link IllegalArgumentException} If the {@code baseUri} parameter
	 *         is null/relative/opaque or if the URI otherwise cannot be
	 *         resolved.
	 */
	public static URI resolve(URI baseUri, URI uri) throws IllegalArgumentException {
		URI resolved;
		if (uri.isAbsolute()) {
            resolved = uri;
        } else {
			URI relative;
			String innerPath;

			// Work out if the relative URI contains a bang (!), meaning to navigate into a JAR
			String uriPath = uri.getPath();
			int bangIndex = uriPath.indexOf('!');
			if (bangIndex >= 0) {
				relative = URI.create(uriPath.substring(0, bangIndex));
				innerPath = uriPath.substring(bangIndex + 1);
			} else {
				relative = uri;
				innerPath = null;
			}

			// Do the normal resolution
			if (baseUri == null) {
                throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is null/unavailable.", uri));
            }
			if (!baseUri.isAbsolute()) {
                throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is also relative (%s).", uri, baseUri));
            }
			if (baseUri.isOpaque()) {
				// Handle "jar:" URIs as a special case
				if (JAR_URI_SCHEME.equals(baseUri.getScheme())) {
                    resolved = resolveJarUri(baseUri, relative);
                } else {
                    throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is opaque (%s).", uri, baseUri));
                }
			} else {
				resolved = baseUri.resolve(relative);
			}

			// If an inner path was indicated, create a jar: URI
			if (innerPath != null) {
				resolved = URI.create("jar:" + resolved.toString() + "!" + innerPath);
			}
		}
		return resolved;
	}

	private static URI resolveJarUri(URI baseUri, URI uri) throws IllegalArgumentException {
		assert JAR_URI_SCHEME.equals(baseUri.getScheme()) : "not a jar: URI";

		String ssp = baseUri.getSchemeSpecificPart();
		int bangIndex = ssp.lastIndexOf('!');
		if (bangIndex == -1) {
            throw new IllegalArgumentException("Invalid URI in the jar: scheme (missing ! separator): " + baseUri);
        }
		URI jarUri = URI.create(ssp.substring(0, bangIndex));
		String pathStr = ssp.substring(bangIndex + 1);

		String query = baseUri.getQuery();
		if (query == null) {
			int qmIndex = pathStr.lastIndexOf('?');
			if (qmIndex >= 0) {
				query = pathStr.substring(qmIndex + 1);
				pathStr = pathStr.substring(0, qmIndex);
			} else {
				query = "";
			}
		}
		boolean nonavigate = query.indexOf("navigate=false") >= 0;

		Path basePath = Path.parse(pathStr);
		Path baseDir = basePath.parent();
		Path resolvedPath = baseDir.append(uri.getPath());

		URI result;
		if (resolvedPath.isEmpty()) {
			result = jarUri;
		} else if (Path.GO_UP.equals(resolvedPath.head())) {
			if (nonavigate) {
                throw new IllegalArgumentException("Cannot navigate outside JAR contents.");
            }
			result = jarUri.resolve(resolvedPath.tail().toString());
		} else {
			result = URI.create(JAR_URI_SCHEME + ":" + jarUri + "!/" + resolvedPath);
		}

		return result;
	}

	/**
	 * Returns the file name of the resource references by the URI. Works with jar: URIs, which normally return
	 * {@code null} if the {@link URI#getPath()} call is used.
	 * @param uri
	 * @return The filename, if it can be calculated.
	 * @throws IllegalArgumentException if the filename cannot be calculated (e.g. if the scheme is not understood).
	 */
	public static String getFileName(URI uri) throws IllegalArgumentException {
		String filename;
		if (JAR_URI_SCHEME.equals(uri.getScheme())) {
			String ssp = uri.getRawSchemeSpecificPart();
			String innerPath;
			int bang = ssp.indexOf('!');
			if (bang < 0) {
                throw new IllegalArgumentException("Invalid jar content URI; missing '!/' path separator: " + uri);
            } else {
                innerPath = ssp.substring(bang + 1);
            }
			filename = getPathLastSegment(innerPath);
		} else {
			String path = uri.getPath();
			if (path == null) {
                throw new IllegalArgumentException("Cannot calculate filename for unknown opaque URI scheme: " + uri);
            }
			filename = getPathLastSegment(path);
		}
		return filename;
	}

	private static String getPathLastSegment(String path) {
		int slash = path.lastIndexOf('/');
		if (slash < 0) {
            return path;
        }

		return path.substring(slash + 1);
	}

}