/*******************************************************************************
 * Copyright (c) 2002, 2011 IBM Corporation and others.
 * All rights reserved. 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:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     Jesper Steen Moeller - Added XML Catalogs 1.1 support
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.catalog;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Platform;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.XMLCorePlugin;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalog;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalogElement;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalogEntry;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalogEvent;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalogListener;
import org.eclipse.wst.xml.core.internal.catalog.provisional.IDelegateCatalog;
import org.eclipse.wst.xml.core.internal.catalog.provisional.INextCatalog;
import org.eclipse.wst.xml.core.internal.catalog.provisional.IRewriteEntry;
import org.eclipse.wst.xml.core.internal.catalog.provisional.ISuffixEntry;
public class Catalog implements ICatalog
{

	class CatalogLS
	{
		public void load()
		{
		}

		public synchronized void save()
		{
			try
			{
				new CatalogWriter().write(Catalog.this, location);
			} catch (Exception e)
			{
				Logger.logException(e);
			}
		}
	}

	class DefaultCatalogLS extends CatalogLS
	{
		public void load()
		{
			NextCatalog userCatalogReference = new NextCatalog();
			userCatalogReference.setId(XMLCorePlugin.USER_CATALOG_ID);
			userCatalogReference.setCatalogLocation(USER_CATALOG_FILE);
			addCatalogElement(userCatalogReference);

			NextCatalog systemCatalogReference = new NextCatalog();
			systemCatalogReference.setId(XMLCorePlugin.SYSTEM_CATALOG_ID);
			systemCatalogReference.setCatalogLocation(SYSTEM_CATALOG_FILE);
			addCatalogElement(systemCatalogReference);

			/*
			 * Here we save the file in order to 'reflect' the catalog that
			 * we've created from plug-in extensions to disk. The 'default'
			 * catalog is only ever written to disk and never read from disk.
			 */
			save();
		}
	}

	private static Comparator LONGEST_REWRITE_FIRST = new Comparator() 
	{
		public int compare(Object entry1, Object entry2) 
		{
			String start1 = ((IRewriteEntry)entry1).getStartString();
			String start2 = ((IRewriteEntry)entry2).getStartString();
			
			// Bigger is earlier
			return start2.length() - start1.length();
		}
	};

	private static Comparator LONGEST_SUFFIX_FIRST = new Comparator() 
	{
		public int compare(Object entry1, Object entry2) 
		{
			String suffix1 = ((ISuffixEntry)entry1).getSuffix();
			String suffix2 = ((ISuffixEntry)entry2).getSuffix();
			
			// Bigger is earlier
			return suffix2.length() - suffix1.length();
		}
	};

	private static Comparator LONGEST_DELEGATE_PREFIX_FIRST = new Comparator() 
	{
		public int compare(Object entry1, Object entry2) 
		{
			String prefix1 = ((IDelegateCatalog)entry1).getStartString();
			String prefix2 = ((IDelegateCatalog)entry2).getStartString();
			
			// Bigger is earlier
			return prefix2.length() - prefix1.length();
		}
	};

	class InternalResolver
	{
		protected Map publicMap = new HashMap();

		protected Map systemMap = new HashMap();

		protected Map uriMap = new HashMap();

		// These are sorted by longest "key" first.
		protected List rewriteSystemList = new LinkedList();
		protected List rewriteUriList = new LinkedList();
		protected List suffixSystemList = new LinkedList();
		protected List suffixUriList = new LinkedList();
		protected List delegatePublicList = new LinkedList();
		protected List delegateSystemList = new LinkedList();
		protected List delegateUriList = new LinkedList();
		
		InternalResolver()
		{
			synchronized (catalogElements) {
				for (Iterator i = catalogElements.iterator(); i.hasNext();)
				{
					ICatalogElement catalogElement = (ICatalogElement) i.next();
					if (catalogElement.getType() == ICatalogElement.TYPE_ENTRY)
					{
						ICatalogEntry entry = (ICatalogEntry) catalogElement;
						Map map = getEntryMap(entry.getEntryType());
						map.put(entry.getKey(), entry);
					} 
					else if (catalogElement.getType() == ICatalogElement.TYPE_REWRITE) 
					{
						IRewriteEntry entry = (IRewriteEntry) catalogElement;
						if (entry.getEntryType() == IRewriteEntry.REWRITE_TYPE_SYSTEM) 
						{
							rewriteSystemList.add(entry);
						} 
						else 
						{
							rewriteUriList.add(entry);
						}
					} 
					else if (catalogElement.getType() == ICatalogElement.TYPE_SUFFIX) 
					{
						ISuffixEntry entry = (ISuffixEntry) catalogElement;
						if (entry.getEntryType() == ISuffixEntry.SUFFIX_TYPE_SYSTEM) 
						{
							suffixSystemList.add(entry);
						} 
						else 
						{
							suffixUriList.add(entry);
						}
					} 
					else if (catalogElement.getType() == ICatalogElement.TYPE_DELEGATE) 
					{
						IDelegateCatalog delegate = (IDelegateCatalog) catalogElement;
						if (delegate.getEntryType() == IDelegateCatalog.DELEGATE_TYPE_PUBLIC) 
						{
							delegatePublicList.add(delegate);
						}
						else if (delegate.getEntryType() == IDelegateCatalog.DELEGATE_TYPE_SYSTEM) 
						{
							delegateSystemList.add(delegate);
						}
						else 
						{
							delegateUriList.add(delegate);
						}
					}
				}
			}
			
			Collections.sort(rewriteSystemList, LONGEST_REWRITE_FIRST);
			Collections.sort(rewriteUriList, LONGEST_REWRITE_FIRST);

			Collections.sort(suffixSystemList, LONGEST_SUFFIX_FIRST);
			Collections.sort(suffixUriList, LONGEST_SUFFIX_FIRST);
		
			Collections.sort(delegatePublicList, LONGEST_DELEGATE_PREFIX_FIRST);
			Collections.sort(delegateSystemList, LONGEST_DELEGATE_PREFIX_FIRST);
			Collections.sort(delegateUriList, LONGEST_DELEGATE_PREFIX_FIRST);
		}

		private Map getEntryMap(int entryType)
		{
			Map map = systemMap;
			switch (entryType)
			{
			case ICatalogEntry.ENTRY_TYPE_PUBLIC:
				map = publicMap;
				break;
			case ICatalogEntry.ENTRY_TYPE_URI:
				map = uriMap;
				break;
			default:
				break;
			}
			return map;
		}

		protected String getMappedURI(Map map, String key)
		{
			CatalogEntry entry = (CatalogEntry) map.get(key);
			if(entry == null) return null;
			String uri = entry.getURI();
			try
			{
                // TODO CS : do we really want to resolve these here?
                // I'm guessing we should return the 'platform:' form of the URI
                // to the caller.
                if (uri.startsWith("platform:")) //$NON-NLS-1$
                {  
				  URL entryURL = new URL(entry.getAbsolutePath(uri));                
				  uri = Platform.resolve(entryURL).toString();
                  
                  // we need to ensure URI's are of form "file:///D:/XXX" and NOT  
                  // "file:D:/XXX".  Otherwise the EMF URI class gets confused
                  // (see bug 103607) 
                  String FILE_SCHEME = "file:"; //$NON-NLS-1$
                  if (uri.startsWith(FILE_SCHEME) && !uri.startsWith(FILE_SCHEME + "/")) //$NON-NLS-1$
                  {
                    uri = FILE_SCHEME + "///" + uri.substring(FILE_SCHEME.length()); //$NON-NLS-1$
                  }  
                }  
                return uri; 
			} catch (IOException e)
			{
				return null;
			}
		}

		public String resolvePublic(String publicId, String systemId)
				throws MalformedURLException, IOException
		{
			String result = getMappedURI(publicMap, publicId);
			if (result == null)
			{
				result = getMappedURI(systemMap, systemId);
			}
			// our clients used to pass namespace in place of public id, so we need to check uri map for those
			if (result == null)
			{
				result = getMappedURI(uriMap, publicId);
			}
			if (result == null)
			{
				result = resolveDelegateCatalogs(delegatePublicList, publicId, systemId);
			}
			if (result == null)
			{
				result = resolveSubordinateCatalogs(
						ICatalogEntry.ENTRY_TYPE_PUBLIC, publicId, systemId);
			}
			return result;
		}

		public String resolveSystem(String systemId)
				throws MalformedURLException, IOException
		{
			String result = getMappedURI(systemMap, systemId);
			if (result == null)
			{
				result = resolveRewrite(rewriteSystemList, systemId);
			}
			if (result == null)
			{
				result = resolveSuffix(suffixSystemList, systemId);
			}
			if (result == null)
			{
				result = resolveDelegateCatalogs(delegateSystemList, systemId, systemId); // systemId is the key for "startString"
			}
			if (result == null)
			{
				result = resolveSubordinateCatalogs(
						ICatalogEntry.ENTRY_TYPE_SYSTEM, null, systemId);
			}
			return result;
		}

		private String resolveRewrite(List rewriteList, String searchString) 
		{
			for (Iterator it = rewriteList.iterator(); it.hasNext();) 
			{
				IRewriteEntry entry = (IRewriteEntry) it.next();
				String startString = entry.getStartString();
				if (searchString.startsWith(startString)) 
				{
					return entry.getRewritePrefix() + searchString.substring(startString.length());
				}
			}
			return null;
		}

		private String resolveSuffix(List suffixList, String searchString) {
			for (Iterator it = suffixList.iterator(); it.hasNext();) {
				ISuffixEntry entry = (ISuffixEntry) it.next();
				if (searchString.endsWith(entry.getSuffix())) {
					return entry.getURI();
				}
			}
			return null;
		}

		protected String resolveDelegateCatalogs(List delegateCatalogs, String key,
				String systemId) throws MalformedURLException, IOException
		{
			String result = null;
			for (Iterator iterator = delegateCatalogs.iterator(); iterator
					.hasNext();) {
				IDelegateCatalog delegate = (IDelegateCatalog) iterator.next();
				
				if (key.startsWith(delegate.getStartString())) {
	
					ICatalog catalog = delegate.getReferencedCatalog();
					if (catalog != null)
					{
						switch (delegate.getEntryType())
						{
						case IDelegateCatalog.DELEGATE_TYPE_PUBLIC:
							result = catalog.resolvePublic(key, systemId);
							break;
						case IDelegateCatalog.DELEGATE_TYPE_SYSTEM:
							result = catalog.resolveSystem(systemId);
							break;
						case IDelegateCatalog.DELEGATE_TYPE_URI:
							result = catalog.resolveURI(systemId);
							break;
						default:
							break;
						}
						if (result != null)
						{
							return result;
						}
					}
				}
			}
			return null;
		}
		
		public String resolveURI(String uri) throws MalformedURLException,
				IOException
		{
			String result = getMappedURI(uriMap, uri);
			if (result == null)
			{
				result = resolveRewrite(rewriteUriList, uri);
			}
			if (result == null)
			{
				result = resolveSuffix(suffixUriList, uri);
			}
			if (result == null)
			{
				result = resolveDelegateCatalogs(delegateUriList, uri, uri); // uri is treated as the systemId
			}
			if (result == null)
			{
				result = resolveSubordinateCatalogs(
						ICatalogEntry.ENTRY_TYPE_URI, null, uri);
			}
			return result;
		}
	}

	class SystemCatalogLS extends CatalogLS
	{
		public void load()
		{
			new CatalogContributorRegistryReader(Catalog.this).readRegistry();

			/*
			 * Here we save the file in order to 'reflect' the catalog that
			 * we've created from plugin extensions to disk. 
			 * The 'system' catalog is only ever written to disk and never read from disk.
			 */
			save();
		}
	}

	class UserCatalogLS extends CatalogLS
	{
		public void load()
		{
			InputStream inputStream = null;
			try
			{
				if (location != null && location.length() > 0) 
				{
					URL url = new URL(location);
					inputStream = url.openStream();
					boolean oldNotificationEnabled = isNotificationEnabled();
					setNotificationEnabled(false);
					clear();
					try
					{
						CatalogReader.read(Catalog.this, inputStream);
					} finally
					{
						setNotificationEnabled(oldNotificationEnabled);
					}
				}
				else
				{
					clear();
				}
				notifyChanged();
			} catch (Exception e)
			{
				// This is OK since the catalog may not exist before we create it
			} finally
			{
				if (inputStream != null)
				{
					try
					{
						inputStream.close();
					} catch (Exception e)
					{
					}
				}
			}
		}
	}

	public static final String DEFAULT_CATALOG_FILE = "default_catalog.xml"; //$NON-NLS-1$

	public static final String SYSTEM_CATALOG_FILE = "system_catalog.xml"; //$NON-NLS-1$

	public static final String USER_CATALOG_FILE = "user_catalog.xml"; //$NON-NLS-1$

	protected String base;

	protected List catalogElements = new ArrayList();

	protected CatalogLS catalogLS;

	protected String id;

	protected InternalResolver internalResolver;

	protected boolean isNotificationEnabled;

	protected List listenerList = new ArrayList();

	protected String location;

	protected CatalogSet resourceSet;

	public Catalog(CatalogSet catalogResourceSet, String id, String location)
	{
		this.resourceSet = catalogResourceSet;
		this.id = id;
		this.location = location;

		if (XMLCorePlugin.DEFAULT_CATALOG_ID.equals(id))
		{
			catalogLS = new DefaultCatalogLS();
		} else if (XMLCorePlugin.SYSTEM_CATALOG_ID.equals(id))
		{
			catalogLS = new SystemCatalogLS();
		} else
		{
			catalogLS = new UserCatalogLS();
		}
	}

	public void addCatalogElement(ICatalogElement element)
	{
		synchronized (catalogElements) {
			catalogElements.add(element);
		}
		element.setOwnerCatalog(this);
		internalResolver = null;
		notifyAddElement(element);
	}

	public void addEntriesFromCatalog(ICatalog catalog)
	{
		try
		{
			setNotificationEnabled(false);
			if (catalog != null)
			{
				ICatalogElement[] entries = ((Catalog)catalog).getCatalogElements();
				for (int i = 0; i < entries.length; i++)
				{
					CatalogElement clone = (CatalogElement)((CatalogElement)entries[i]).clone();
					addCatalogElement(clone);
				}
			} else
			{
				Logger.log(Logger.ERROR, "argument was null in Catalog.addEntriesFromCatalog"); //$NON-NLS-1$
			}
		} finally
		{
			setNotificationEnabled(true);
		}
		internalResolver = null;
		notifyChanged();
	}

	public void addListener(ICatalogListener listener)
	{
		listenerList.add(listener);
	}

	public void clear()
	{
		synchronized (catalogElements) {
			catalogElements.clear();
		}
		internalResolver = null;
		notifyChanged();
	}

	public ICatalogElement createCatalogElement(int type)
	{
		switch (type)
		{
		case ICatalogElement.TYPE_ENTRY:
			return new CatalogEntry(); // TODO: Should be kind of deprecated
		case ICatalogElement.TYPE_NEXT_CATALOG:
			return new NextCatalog();
		case ICatalogEntry.ENTRY_TYPE_PUBLIC:
		case ICatalogEntry.ENTRY_TYPE_SYSTEM:
		case ICatalogEntry.ENTRY_TYPE_URI:
			return new CatalogEntry(type);
		case ICatalogElement.TYPE_REWRITE:	
		case IRewriteEntry.REWRITE_TYPE_SYSTEM:
		case IRewriteEntry.REWRITE_TYPE_URI:
			return new RewriteEntry(type);
		case ICatalogElement.TYPE_SUFFIX:	
		case ISuffixEntry.SUFFIX_TYPE_SYSTEM:
		case ISuffixEntry.SUFFIX_TYPE_URI:
			return new SuffixEntry(type);
		case ICatalogElement.TYPE_DELEGATE:	
		case IDelegateCatalog.DELEGATE_TYPE_PUBLIC:
		case IDelegateCatalog.DELEGATE_TYPE_SYSTEM:
		case IDelegateCatalog.DELEGATE_TYPE_URI:
			return new DelegateCatalog(type);
		default:
			throw new IllegalArgumentException("Unknown element type " + type);//$NON-NLS-1 // Makes no sense at all!
		}
	}

	public String getBase()
	{
		return base;
	}

	private List getCatalogElements(int type)
	{
		List result = new ArrayList();
		ICatalogElement[] elements = (ICatalogElement[]) catalogElements
				.toArray(new ICatalogElement[catalogElements.size()]);
		for (int i = 0; i < elements.length; i++)
		{
			ICatalogElement element = elements[i];
			if (element.getType() == type)
			{
				result.add(element);
			}
		}
		return result;
	}

	public ICatalogEntry[] getCatalogEntries()
	{
		List result = getCatalogElements(ICatalogElement.TYPE_ENTRY);
		return (ICatalogEntry[]) result
				.toArray(new ICatalogEntry[result.size()]);
	}

	public IDelegateCatalog[] getDelegateCatalogs() 
	{
		List result = getCatalogElements(ICatalogElement.TYPE_DELEGATE);
		return (IDelegateCatalog[]) result
				.toArray(new IDelegateCatalog[result.size()]);
	}
	
	public IRewriteEntry[] getRewriteEntries() 
	{
		List result = getCatalogElements(ICatalogElement.TYPE_REWRITE);
		return (IRewriteEntry[]) result
				.toArray(new IRewriteEntry[result.size()]);
	}
	
	public ISuffixEntry[] getSuffixEntries() 
	{
		List result = getCatalogElements(ICatalogElement.TYPE_SUFFIX);
		return (ISuffixEntry[]) result
				.toArray(new ISuffixEntry[result.size()]);
	}
	
	protected CatalogSet getCatalogSet()
	{
		return resourceSet;
	}

	public String getId()
	{
		return id;
	}

	public String getLocation()
	{
		return location;
	}

	public INextCatalog[] getNextCatalogs()
	{
		List result = getCatalogElements(ICatalogElement.TYPE_NEXT_CATALOG);
		return (INextCatalog[]) result.toArray(new INextCatalog[result.size()]);
	}

	protected InternalResolver getOrCreateInternalResolver()
	{
		if (internalResolver == null)
		{
			internalResolver = new InternalResolver();
		}
		return internalResolver;
	}

	protected boolean isNotificationEnabled()
	{
		return isNotificationEnabled;
	}

	public void load() throws IOException
	{
		catalogLS.load();
	}

	protected void notifyAddElement(ICatalogElement entry)
	{
		if (isNotificationEnabled)
		{
			ICatalogEvent event = new CatalogEvent(this, entry,
					ICatalogEvent.ELEMENT_ADDED);
			notifyListeners(event);
		}
	}

	protected void notifyChanged()
	{
		ICatalogEvent event = new CatalogEvent(this, null,
				ICatalogEvent.CHANGED);
		notifyListeners(event);
	}

	protected void notifyListeners(ICatalogEvent event)
	{
		List list = new ArrayList();
		list.addAll(listenerList);
		for (Iterator i = list.iterator(); i.hasNext();)
		{
			ICatalogListener listener = (ICatalogListener) i.next();
			listener.catalogChanged(event);
		}
	}

	protected void notifyRemoveElement(ICatalogElement element)
	{
		if (isNotificationEnabled)
		{
			ICatalogEvent event = new CatalogEvent(this, element,
					ICatalogEvent.ELEMENT_REMOVED);
			notifyListeners(event);
		}
	}

	public void removeCatalogElement(ICatalogElement element)
	{
		synchronized (catalogElements) {
			catalogElements.remove(element);
		}
		internalResolver = null;
		notifyRemoveElement(element);
		
	}

	public void removeListener(ICatalogListener listener)
	{
		listenerList.remove(listener);
	}

	public String resolvePublic(String publicId, String systemId)
			throws MalformedURLException, IOException
	{
		return getOrCreateInternalResolver().resolvePublic(publicId, systemId);
	}

	protected String resolveSubordinateCatalogs(int entryType, String publicId,
			String systemId) throws MalformedURLException, IOException
	{
		String result = null;
		INextCatalog[] nextCatalogs = getNextCatalogs();
		for (int i = 0; i < nextCatalogs.length; i++)
		{
			INextCatalog nextCatalog = nextCatalogs[i];
			ICatalog catalog = nextCatalog.getReferencedCatalog();
			if (catalog != null)
			{
				switch (entryType)
				{
				case ICatalogEntry.ENTRY_TYPE_PUBLIC:
					result = catalog.resolvePublic(publicId, systemId);
					break;
				case ICatalogEntry.ENTRY_TYPE_SYSTEM:
					result = catalog.resolveSystem(systemId);
					break;
				case ICatalogEntry.ENTRY_TYPE_URI:
					result = catalog.resolveURI(systemId);
					break;
				default:
					break;
				}
				if (result != null)
				{
					return result;
				}
			}
		}
		return null;
	}

	public String resolveSystem(String systemId) throws MalformedURLException,
			IOException
	{
		return getOrCreateInternalResolver().resolveSystem(systemId);
	}

	public String resolveURI(String uri) throws MalformedURLException,
			IOException
	{
		return getOrCreateInternalResolver().resolveURI(uri);
	}

	public void save() throws IOException
	{
		catalogLS.save();
	}

	public void setBase(String base)
	{
		this.base = base;
	}

	public void setId(String id)
	{
		this.id = id;
	}

	public void setLocation(String location)
	{
		this.location = location;
	}

	protected void setNotificationEnabled(boolean b)
	{
		isNotificationEnabled = b;
	}

	public ICatalogElement[] getCatalogElements()
	{
		return (ICatalogElement[]) catalogElements.toArray(new ICatalogElement[catalogElements.size()]);
	}
	

	

}
