//------------------------------------------------------------------------------
// Copyright (c) 2005, 2006 IBM Corporation and others.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// Contributors:
// IBM Corporation - initial implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.publishing.services;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.epf.library.layout.DefaultContentValidator;
import org.eclipse.epf.library.layout.LinkInfo;
import org.eclipse.epf.library.layout.util.XmlElement;
import org.eclipse.epf.library.util.LibraryUtil;
import org.eclipse.epf.library.util.ResourceHelper;
import org.eclipse.epf.publishing.PublishingPlugin;
import org.eclipse.epf.publishing.PublishingResources;
import org.eclipse.epf.publishing.util.http.HttpResponse;
import org.eclipse.epf.publishing.util.http.HttpUtil;
import org.eclipse.epf.uma.ContentCategory;
import org.eclipse.epf.uma.MethodConfiguration;
import org.eclipse.epf.uma.MethodElement;
import org.eclipse.osgi.util.NLS;

import com.ibm.icu.util.Calendar;


/**
 * content validator used for publishing
 * 
 * @author Jinhua Xi
 * @since 1.0
 *
 */
public class PublishingContentValidator extends DefaultContentValidator {

	private boolean debug = PublishingPlugin.getDefault().isDebugging();

	private boolean showExtraInfoForDescriptors = false;
	
	public class InvalidExternalLinkInfo {
		public MethodElement owner;

		public String url;

		public String message;

		public InvalidExternalLinkInfo(MethodElement owner, String url,
				String message) {
			this.owner = owner;
			this.url = url;
			this.message = message;
		}
	}

	public class MissingReference {
		public MethodElement owner;

		public MethodElement refElement;

		public String guid;

		public String linkedText;

		public MissingReference(MethodElement owner, MethodElement refElement) {
			this.owner = owner;
			this.refElement = refElement;
		}

		public MissingReference(MethodElement owner, String guid,
				String linkedText) {
			this.owner = owner;
			this.guid = guid;
			this.linkedText = linkedText;
		}
	}

	public class MissingResource {
		public MethodElement owner;

		public File resourceFile;

		public String url;

		public MissingResource(MethodElement owner, File resourceFile,
				String url) {
			this.owner = owner;
			this.resourceFile = resourceFile;
			this.url = url;
		}
	}

	public static final String LOGS_FOLDER = "logs"; //$NON-NLS-1$

	public static final String ERROR_LOG_FILENAME = "error.log"; //$NON-NLS-1$

	public static final String WARNING_LOG_FILENAME = "warning.log"; //$NON-NLS-1$

	public static final String INFO_LOG_FILENAME = "info.log"; //$NON-NLS-1$

	protected File logPath;

	protected boolean validateExternalLinks = false;

	protected List invalidExternalLinks = new ArrayList();

	// cache the valided external links to avoid multiple checking
	protected List validatedExternalLinks = new ArrayList();

	protected List missingReferences = new ArrayList();

	protected List missingResources = new ArrayList();

	protected List discardedElements = new ArrayList();

	protected List validCategories = new ArrayList();

	protected long publishing_start = 0;

	protected long time_for_external_link_checking = 0;

	// collect the elements referenced by the published contents so we can publish them
	// this will be the elements to be published
	protected List referencedElements = new ArrayList();
	
	// published elements
	protected List publishedElements = new ArrayList();
	
	// this is the default target element for the content validator
	// set this before publishing the element and set to null after the publishign is done
	protected MethodElement defaultTarget = null;
	
	public PublishingContentValidator(String pubDir,
			boolean validateExternalLinks) {
		super(pubDir);
		this.validateExternalLinks = validateExternalLinks;
		
		this.logPath = new File(pubDir, LOGS_FOLDER);
		super.info = getStream(INFO_LOG_FILENAME);
		super.warning = getStream(WARNING_LOG_FILENAME);
		super.error = getStream(ERROR_LOG_FILENAME);

		// set the start time
		publishing_start = Calendar.getInstance().getTimeInMillis();
	}

	public void dispose() {
		invalidExternalLinks.clear();
		validatedExternalLinks.clear();
		missingReferences.clear();
		missingResources.clear();
		discardedElements.clear();
		validCategories.clear();

		referencedElements.clear();
		publishedElements.clear();
		
		info.close();
		warning.close();
		error.close();

	}

	protected PrintStream getStream(String fileName) {
		try {
			File f = new File(logPath, fileName);
			File dir = f.getParentFile();
			dir.mkdirs();

			if (!f.exists()) {
				f.createNewFile();
			}

			return new PrintStream(new FileOutputStream(f), true);
		} catch (Exception e) {

		}

		return null;
	}

	public LinkInfo validateLink(MethodElement owner, String attributes,
			String text, MethodConfiguration config) {
		LinkInfo info = super.validateLink(owner, attributes, text, config);

		if (validateExternalLinks) {
			String url = info.getUrl();
			if ((url != null) && ResourceHelper.isExternalLink(url)
					&& !url.startsWith("ftp://")) //$NON-NLS-1$
			{
				if (!validatedExternalLinks.contains(url)) {
					try {
						long start = Calendar.getInstance().getTimeInMillis();
						HttpResponse resp = HttpUtil.doGet(url, null, 6000); // timeout
						// at 6
						// seconds
						long time = Calendar.getInstance().getTimeInMillis()
								- start;
						time_for_external_link_checking += time;
//						System.out
//								.println(time
//										+ " mini-seconds querying Url '" + url + "', return status=" + resp.getStatus()); //$NON-NLS-1$ //$NON-NLS-2$
					} catch (java.net.UnknownHostException e) {
						logInvalidExternalLink(owner, url, null);
					} catch (Exception e) {
						logInvalidExternalLink(owner, url, e.getMessage());
					}

					// cache it
					validatedExternalLinks.add(url);
				}

				// do we need to log the info so that user know what external
				// urls are referenced in the content?
				logInfo(owner, NLS.bind(PublishingResources.externalUrl_msg, url)); 

			}
		}
		return info;
	}

	public void logMissingReference(MethodElement owner,
			MethodElement refElement) {
		super.logMissingReference(owner, refElement);
		missingReferences.add(new MissingReference(owner, refElement));
	}

	public void logMissingReference(MethodElement owner, String guid,
			String linkedText) {
		super.logMissingReference(owner, guid, linkedText);
		missingReferences.add(new MissingReference(owner, guid, linkedText));
	}

	public void logMissingResource(MethodElement owner, File resourceFile,
			String url) {
		super.logMissingResource(owner, resourceFile, url);
		missingResources.add(new MissingResource(owner, resourceFile, url));
	}

	public void logInvalidExternalLink(MethodElement owner, String url,
			String message) {
		super.logInvalidExternalLink(owner, url, message);
		invalidExternalLinks.add(new InvalidExternalLinkInfo(owner, url,
				message));
	}

	public XmlElement getReport() {
		XmlElement reportXml = new XmlElement("validatorInfo"); //$NON-NLS-1$

		if (invalidExternalLinks.size() > 0) {
			logInfo(time_for_external_link_checking / 1000
					+ " seconds validating external links"); //$NON-NLS-1$

			XmlElement invalidExternalLinksXml = reportXml
					.newChild("invalidExternalLinks"); //$NON-NLS-1$
			for (Iterator it = invalidExternalLinks.iterator(); it.hasNext();) {
				InvalidExternalLinkInfo info = (InvalidExternalLinkInfo) it
						.next();
				invalidExternalLinksXml.newChild("entry") //$NON-NLS-1$
						.setAttribute("url", info.url) //$NON-NLS-1$
						.setAttribute(
								"owner", LibraryUtil.getTypeName(info.owner)) //$NON-NLS-1$
						.setAttribute("message", info.message); //$NON-NLS-1$
			}
		}

		if (missingReferences.size() > 0) {
			XmlElement invalidReferencesXml = reportXml
					.newChild("invalidReferences"); //$NON-NLS-1$
			XmlElement missingReferencesXml = reportXml
					.newChild("missingReferences"); //$NON-NLS-1$
			for (Iterator it = missingReferences.iterator(); it.hasNext();) {
				MissingReference info = (MissingReference) it.next();
				if (info.refElement == null) {
					invalidReferencesXml
							.newChild("entry") //$NON-NLS-1$
							.setAttribute("element", info.linkedText) //$NON-NLS-1$
							.setAttribute("guid", info.guid) //$NON-NLS-1$
							.setAttribute(
									"owner", LibraryUtil.getTypeName(info.owner)); //$NON-NLS-1$
				} else {
					missingReferencesXml
							.newChild("entry") //$NON-NLS-1$
							.setAttribute(
									"element", LibraryUtil.getTypeName(info.refElement)) //$NON-NLS-1$
							.setAttribute("guid", info.refElement.getGuid()) //$NON-NLS-1$
							.setAttribute(
									"owner", LibraryUtil.getTypeName(info.owner)); //$NON-NLS-1$
				}
			}
		}

		if (missingResources.size() > 0) {
			XmlElement missingResourcesXml = reportXml
					.newChild("missingResources"); //$NON-NLS-1$
			for (Iterator it = missingResources.iterator(); it.hasNext();) {
				MissingResource info = (MissingResource) it.next();
				missingResourcesXml
						.newChild("entry") //$NON-NLS-1$
						.setAttribute("url", info.url) //$NON-NLS-1$
						.setAttribute(
								"resource", (info.resourceFile == null) ? "" : info.resourceFile.getPath()) //$NON-NLS-1$ //$NON-NLS-2$
						.setAttribute(
								"owner", (info.owner == null) ? "" : LibraryUtil.getTypeName(info.owner)); //$NON-NLS-1$ //$NON-NLS-2$

			}
		}

		long publishing_time = (Calendar.getInstance().getTimeInMillis() - publishing_start) / 1000;
		int minutes = (int) publishing_time / 60;
		int seconds = (int) publishing_time - minutes * 60;

		logInfo("Publishing time: " + minutes + " minutes " + seconds + " seconds"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

		return reportXml;
	}

	public void addValidCategory(ContentCategory e) {
		if (!validCategories.contains(e)) {
			validCategories.add(e);
		}
	}

	/**
	 * check if the element is discarded or not
	 * discarded elements will be treated as out side the configursation
	 * 
	 * @param owner MethodElement the owner of the element
	 * @param Object feature EStructuralFeature or OppositeFeature
	 * @param e MethodElement the element to be checked
	 */
	public boolean isDiscarded(MethodElement owner, Object feature, MethodElement e) {
		if (discardedElements.contains(e)) {
			return true;
		}
		
		// if the element is a ContentCategory and is not discarded
		if (e instanceof ContentCategory) {
			if (validCategories.contains(e)) {
				return false;
			}

			// otherwise, check if it should be discarded or not
			// TODO
			// for now, discard all referenced content categories if they are
			// not included in the publishing view.
			// NO!!!!!!!!!!!! This will lead to a lot of broken links and
			// missing element
			// TOO strong limitation. Let open it for now
			// RATLC00383988 - Publishing:Overview page in published website
			// have broken links to BM
			// return true;
			return false;
		}
		return false;
	}

	public void addReferencedElement(MethodElement owner, MethodElement e)
	{
		if ( e == null ) {
			return;
		}
		
		// don't add discarded elements
		if ( isDiscarded(owner, null, e) ) {
			if ( debug ) {
				System.out.println("Element is discarded: " + LibraryUtil.getTypeName(e)); //$NON-NLS-1$
			}
			return;
		}
		
		if ( e != null &&  !referencedElements.contains(e) && !publishedElements.contains(e)) {
			referencedElements.add(e);
			logReference(owner, e);
		}
	}
	
	public void logReference(MethodElement owner, MethodElement e)
	{
		if ( debug ) {
			System.out.println("--- Referenece Element Added: " + LibraryUtil.getTypeName(e)); //$NON-NLS-1$
		}
	}
	
	public void removeReferencedElement(MethodElement e) {
		if ( referencedElements.contains(e) ) {
			referencedElements.remove(e);
			if ( debug ) {
				System.out.println("--- Reference Element Removed: " + LibraryUtil.getTypeName(e));	 //$NON-NLS-1$
			}
		}
	}
	
	public List getReferencedElements()
	{
		return referencedElements;
	}
	
	/**
	 * aet the discarded element for this publication. If an element is
	 * discarded, it should not be published and link to it should be link to
	 * mising element page
	 * 
	 * @param e
	 *            MethodElement
	 */
	public void setDiscardedElement(MethodElement e) {
		
		 if ( e == null ) {
			 return;
		 }
		 
		if (!discardedElements.contains(e)) {
			discardedElements.add(e);
		}
		
		// if th fdiscarded element is in the reference list, remove it as well
		removeReferencedElement(e);
	}
	
	public boolean isReferencedElement(MethodElement e) {
		return (e != null) && referencedElements.contains(e);
	}
	
	public List getPublishedElements() {
		return publishedElements;
	}	
	
	public void setTargetElement(MethodElement target) {
		this.defaultTarget = target;
	}
	
	public boolean hasClosure() {
		return false;
	}

	public boolean inClosure(MethodElement e) {
		return true;
	}
	
	public void addClosureElements(List items) {
		// do nothing
	}
	
	public void makeElementClosure() {
		// do nothing
	}
	
	public boolean showExtraInfoForDescriptors() {
		return showExtraInfoForDescriptors;
	}
	
	public void setShowExtraInfoForDescriptors(boolean show) {
		showExtraInfoForDescriptors = show;
	}
}
