/*******************************************************************************
 * Copyright (c) 2012, 2017 Red Hat Inc. and others
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Mickael Istria (Red Hat) - Initial API & implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.updatesite;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

public class SiteBundle {

	private String arch;
	// performance
	private URL base;
	private List<String> categoryNames;
	private String bundleId;
	private String bundleVersion;
	private String label;
	private String nl;

	private String os;
	private String patch;
	private static final boolean resolved = false;
	private SiteModel site;
	private String type;
	private URL url;
	private String urlString;
	private String ws;

	/*
	 * Compares two URL for equality
	 */
	public static boolean sameURL(URL url1, URL url2) {
		if (url1 == url2)
			return true;
		if (url1 == null ^ url2 == null)
			return false;

		// check if URL are file: URL as we may
		// have 2 URL pointing to the same bundleReference
		// but with different representation
		// (i.e. file:/C;/ and file:C:/)
		final boolean isFile1 = "file".equalsIgnoreCase(url1.getProtocol());//$NON-NLS-1$
		final boolean isFile2 = "file".equalsIgnoreCase(url2.getProtocol());//$NON-NLS-1$
		if (isFile1 && isFile2) {
			File file1 = new File(url1.getFile());
			File file2 = new File(url2.getFile());
			return file1.equals(file2);
		}
		// URL1 xor URL2 is a file, return false. (They either both need to be files, or
		// neither)
		if (isFile1 ^ isFile2)
			return false;
		return getExternalForm(url1).equals(getExternalForm(url2));
	}

	/**
	 * Gets the external form of this URL. In particular, it trims any white space,
	 * removes a trailing slash and creates a lower case string.
	 */
	private static String getExternalForm(URL url) {
		String externalForm = url.toExternalForm();
		if (externalForm == null)
			return ""; //$NON-NLS-1$
		externalForm = externalForm.trim();
		if (externalForm.endsWith("/")) { //$NON-NLS-1$
			// Remove the trailing slash
			externalForm = externalForm.substring(0, externalForm.length() - 1);
		}
		return externalForm.toLowerCase();

	}

	/**
	 * Creates an uninitialized bundle reference model object.
	 */
	public SiteBundle() {
		super();
	}

	/**
	 * Adds the name of a category this bundle belongs to. Throws a runtime
	 * exception if this object is marked read-only.
	 * 
	 * @param categoryName category name
	 */
	public void addCategoryName(String categoryName) {
		if (this.categoryNames == null)
			this.categoryNames = new ArrayList<>();
		if (!this.categoryNames.contains(categoryName))
			this.categoryNames.add(categoryName);
	}

	private void delayedResolve() {

		// PERF: delay resolution
		if (resolved)
			return;

		// resolve local elements
		try {
			url = new URL(base, urlString);
		} catch (MalformedURLException e) {
			// UpdateCore.warn("", e); //$NON-NLS-1$
		}
	}

	/**
	 * Compares 2 bundle reference models for equality
	 * 
	 * @param object bundle reference model to compare with
	 * @return <code>true</code> if the two models are equal, <code>false</code>
	 *         otherwise
	 */
	@Override
	public boolean equals(Object object) {
		if (object == null)
			return false;
		if (!(object instanceof SiteBundle))
			return false;
		SiteBundle that = (SiteBundle) object;
		if (this.bundleId == null) {
			if (that.bundleId != null)
				return false;
		} else if (!this.bundleId.equals(that.bundleId))
			return false;
		if (this.bundleVersion == null) {
			if (that.bundleVersion != null)
				return false;
		} else if (!this.bundleVersion.equals(that.bundleVersion))
			return false;
		if (this.label == null) {
			if (that.label != null)
				return false;
		} else if (!this.label.equals(that.label))
			return false;
		return sameURL(this.getURL(), that.getURL());
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (bundleId == null ? 0 : bundleId.hashCode());
		result = prime * result + (bundleVersion == null ? 0 : bundleVersion.hashCode());
		if (this.getURL() == null)
			return result;

		if ("file".equalsIgnoreCase(getURL().getProtocol())) {//$NON-NLS-1$
			// If the URL is a file, then create the HashCode from the file
			File f = new File(getURL().getFile());
			if (f != null)
				result = prime * result + f.hashCode();
		} else
			// Otherwise create it from the External form of the URL (in lower case)
			result = prime * result + getExternalForm(this.getURL()).hashCode();
		return result;
	}

	/**
	 * Returns the names of categories the referenced bundle belongs to.
	 * 
	 * @return an array of names, or an empty array.
	 */
	public String[] getCategoryNames() {
		if (categoryNames == null)
			return new String[0];

		return categoryNames.toArray(new String[0]);
	}

	/**
	 * Returns the bundle identifier as a string
	 * 
	 * @return bundle identifier
	 */
	public String getBundleIdentifier() {
		return bundleId;
	}

	/**
	 * Returns the bundle version as a string
	 * 
	 * @return bundle version
	 */
	public String getBundleVersion() {
		return bundleVersion;
	}

	/**
	 * Retrieve the displayable label for the bundle reference. If the model object
	 * has been resolved, the label is localized.
	 *
	 * @return displayable label, or <code>null</code>.
	 */
	public String getLabel() {
		return label;
	}

	/**
	 * Retrieve the non-localized displayable label for the bundle reference.
	 *
	 * @return non-localized displayable label, or <code>null</code>.
	 */
	public String getLabelNonLocalized() {
		return label;
	}

	/**
	 * Get optional locale specification as a comma-separated string.
	 *
	 * @return the locale specification string, or <code>null</code>.
	 */
	public String getNL() {
		return nl;
	}

	/**
	 * Get optional operating system specification as a comma-separated string.
	 *
	 * @return the operating system specification string, or <code>null</code>.
	 */
	public String getOS() {
		return os;
	}

	/**
	 * Get optional system architecture specification as a comma-separated string.
	 *
	 * @return the system architecture specification string, or <code>null</code>.
	 */
	public String getOSArch() {
		return arch;
	}

	/**
	 * Returns the patch mode.
	 */
	public String getPatch() {
		return patch;
	}

	/**
	 * Returns the site model for the reference.
	 * 
	 * @return site model
	 * @since 2.0
	 */
	public SiteModel getSiteModel() {
		return site;
	}

	/**
	 * Returns the referenced bundle type.
	 * 
	 * @return bundle type, or <code>null</code> representing the default bundle
	 *         type for the site
	 */
	public String getType() {
		return type;
	}

	/**
	 * Returns the resolved URL for the bundle reference.
	 * 
	 * @return url string
	 */
	public URL getURL() {
		delayedResolve();
		return url;
	}

	/**
	 * Returns the unresolved URL string for the reference.
	 *
	 * @return url string
	 */
	public String getURLString() {
		return urlString;
	}

	/**
	 * Get optional windowing system specification as a comma-separated string.
	 *
	 * @return the windowing system specification string, or <code>null</code>.
	 */
	public String getWS() {
		return ws;
	}

	/**
	 * Resolve the model object. Any URL strings in the model are resolved relative
	 * to the base URL argument. Any translatable strings in the model that are
	 * specified as translation keys are localized using the supplied resource
	 * bundle.
	 * 
	 * @param resolveBase URL
	 * @param bundleURL   resource bundle URL
	 * @exception MalformedURLException
	 */
	public void resolve(URL resolveBase, URL bundleURL) throws MalformedURLException {
		this.base = resolveBase;
	}

	/**
	 * Sets the system architecture specification. Throws a runtime exception if
	 * this object is marked read-only.
	 *
	 * @param arch system architecture specification as a comma-separated list
	 */
	public void setArch(String arch) {
		this.arch = arch;
	}

	/**
	 * Sets the names of categories this bundle belongs to. Throws a runtime
	 * exception if this object is marked read-only.
	 * 
	 * @param categoryNames an array of category names
	 */
	public void setCategoryNames(String[] categoryNames) {
		if (categoryNames == null)
			this.categoryNames = null;
		else
			this.categoryNames = new ArrayList<>(Arrays.asList(categoryNames));
	}

	/**
	 * Sets the bundle identifier. Throws a runtime exception if this object is
	 * marked read-only.
	 * 
	 * @param bundleId bundle identifier
	 */
	public void setBundleIdentifier(String bundleId) {
		this.bundleId = bundleId;
	}

	/**
	 * Sets the bundle version. Throws a runtime exception if this object is marked
	 * read-only.
	 * 
	 * @param bundleVersion bundle version
	 */
	public void setBundleVersion(String bundleVersion) {
		this.bundleVersion = bundleVersion;
	}

	/**
	 * Sets the label.
	 * 
	 * @param label The label to set
	 */
	public void setLabel(String label) {
		this.label = label;
	}

	/**
	 * Sets the locale specification. Throws a runtime exception if this object is
	 * marked read-only.
	 *
	 * @param nl locale specification as a comma-separated list
	 */
	public void setNL(String nl) {
		this.nl = nl;
	}

	/**
	 * Sets the operating system specification. Throws a runtime exception if this
	 * object is marked read-only.
	 *
	 * @param os operating system specification as a comma-separated list
	 */
	public void setOS(String os) {
		this.os = os;
	}

	/**
	 * Sets the patch mode.
	 */
	public void setPatch(String patch) {
		this.patch = patch;
	}

	/**
	 * Sets the site for the referenced. Throws a runtime exception if this object
	 * is marked read-only.
	 * 
	 * @param site site for the reference
	 */
	public void setSiteModel(SiteModel site) {
		this.site = site;
	}

	/**
	 * Sets the referenced bundle type. Throws a runtime exception if this object is
	 * marked read-only.
	 * 
	 * @param type referenced bundle type
	 */
	public void setType(String type) {
		this.type = type;
	}

	/**
	 * Sets the unresolved URL for the bundle reference. Throws a runtime exception
	 * if this object is marked read-only.
	 * 
	 * @param urlString unresolved URL string
	 */
	public void setURLString(String urlString) {
		this.urlString = urlString;
		this.url = null;
	}

	/**
	 * Sets the windowing system specification. Throws a runtime exception if this
	 * object is marked read-only.
	 *
	 * @param ws windowing system specification as a comma-separated list
	 */
	public void setWS(String ws) {
		this.ws = ws;
	}

	/**
	 * @see Object#toString()
	 */
	@Override
	public String toString() {
		StringBuilder buffer = new StringBuilder();
		buffer.append(getClass().toString() + " :"); //$NON-NLS-1$
		buffer.append(" at "); //$NON-NLS-1$
		if (url != null)
			buffer.append(url.toExternalForm());
		return buffer.toString();
	}

}
