/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 flex2.compiler;

import flex2.compiler.common.SinglePathResolver;
import flex2.compiler.io.FileUtil;
import flex2.compiler.io.LocalFile;
import flex2.compiler.io.VirtualFile;
import flex2.compiler.util.CompilerMessage;
import java.io.File;
import java.util.*;

/**
 * A list of paths specified by the -source-path option, where
 * dependencies, following the single public definition rule, can be
 * resolved.
 *
 * @author Clement Wong
 */
public class SourcePath extends SourcePathBase
    implements SinglePathResolver
{
    protected final List<File> directories;
    //private ApplicationCache applicationCache;

	public SourcePath(VirtualFile[] classPath, VirtualFile appPath, String[] mimeTypes, boolean allowSourcePathOverlap)
	{
		this(mimeTypes, allowSourcePathOverlap);

		addApplicationParentToSourcePath(appPath, classPath, this.directories);
		addPathElements(classPath, this.directories, allowSourcePathOverlap, warnings);
	}
	
	public SourcePath(String[] mimeTypes, boolean allowSourcePathOverlap)
	{
		super(mimeTypes, allowSourcePathOverlap);
		directories = new LinkedList<File>();
	}

	public void addPathElements(VirtualFile[] classPath)
	{
		addPathElements(classPath, directories, allowSourcePathOverlap, warnings);
	}	

	/*
	private Source newSource(File file, File pathRoot, String namespaceURI, String localPart)
	{
        Source source = new Source(new LocalFile(file), new LocalFile(pathRoot),
                                   namespaceURI.replace('.', '/'), localPart, this, false, false);

        if (applicationCache != null)
        {
            String className = CompilerAPI.constructClassName(namespaceURI, localPart);
            Source cachedSource = applicationCache.getSource(className);

            if ((cachedSource != null) && !cachedSource.isUpdated())
            {
                CompilationUnit cachedCompilationUnit = cachedSource.getCompilationUnit();

                if (cachedSource.getPathRoot().equals(source.getPathRoot()) &&
                    (cachedCompilationUnit != null) && cachedCompilationUnit.hasTypeInfo)
                {
                    CompilationUnit compilationUnit =
                        source.newCompilationUnit(cachedCompilationUnit.getSyntaxTree(),
                                                  new CompilerContext());

                    Source.copyCompilationUnit(cachedCompilationUnit, compilationUnit, true);
                    source.setFileTime(cachedSource.getFileTime());
                    cachedSource.reused();

                    // We somehow need to validate that other reused
                    // sources, which depend on this source, reference
                    // the same slots.  Or maybe it's good enough just
                    // to validate that the slots referenced by this
                    // source are the same as the slots referenced by
                    // the dependencies.  Something to ponder.
                }
            }
        }

        return source;
	}
	
	// see if the Source object continues to be the first choice given a QName.
	boolean checkPreference(Source s)
	{
		assert s.getOwner() == this;
		
		String relativePath = constructRelativePath(s), pathRoot = s.getPathRoot().getName();
		if (relativePath == null)
		{
			// not possible, but don't disrupt the flow...
			return true;
		}
		
		boolean thisPath = false;
		
		for (int i = 0, size = directories.size(); i < size; i++)
		{
			File d = directories.get(i), f = null;
			if (pathRoot.equals(FileUtil.getCanonicalPath(d)))
			{
				thisPath = true;
			}
			
			try
			{
				f = findFile(d, relativePath, mimeTypes);
			}
			catch (CompilerException ex)
			{
				removeSource(s);
				return false;
			}

			if (f != null && !thisPath)
			{
				removeSource(s);
				return false;
			}
		}
		
		return true;
	}
	
	protected Source findFile(String className, String namespaceURI, String localPart) throws CompilerException
	{
		String p = className.replace(':', '.').replace('.', File.separatorChar);
		Source s = null;

		for (int i = 0, size = directories.size(); i < size; i++)
		{
			File f, d = directories.get(i);

			if ((f = findFile(d, p, mimeTypes)) != null)
			{
				sources.put(className, s = newSource(f, d, namespaceURI, localPart));
				return s;
			}
		}
		
		return null;
	}
	
	public boolean hasPackage(String packageName)
	{
		for (int i = 0, size = directories.size(); i < size; i++)
		{
			File d = directories.get(i);
			if (hasDirectory(d, packageName))
			{
				return true;
			}
		}

		return false;
	}

	public boolean hasDefinition(QName qName)
	{
		String className = CompilerAPI.constructClassName(qName.getNamespace(), qName.getLocalPart());

		if (misses.contains(className))
		{
			return false;
		}

		if (hits.contains(className))
		{
			return true;
		}

		String p = className.replace(':', '.').replace('.', File.separatorChar);

		for (int i = 0, size = directories.size(); i < size; i++)
		{
			File f, d = directories.get(i);

			try
			{
				if ((f = findFile(d, p, mimeTypes)) != null)
				{
					hits.add(className);
					return true;
				}
			}
			catch (CompilerException ex)
			{
			}
		}

		misses.add(className);
		return false;
	}

	private boolean hasDirectory(File dir, String packageName)
	{
		if (packageName.length() == 0)
		{
			return true;
		}

		String relativePath = packageName.replace('.', File.separatorChar);
		String fullPath = dir.getPath() + File.separator + relativePath;

		if (dirs.get(fullPath) == NO_DIR)
		{
			return false;
		}

		boolean result = new File(dir, relativePath).isDirectory();
		dirs.put(fullPath, result ? fullPath : NO_DIR);

		return result;
	}
	
	public List<File> getPaths()
	{
		return directories;
	}
    */
    /**
     * Resolves paths with a leading slash and relative to a Source
     * Path directory.
     */
    public VirtualFile resolve(String path)
    {
        if (path.charAt(0) == '/')
        {
            String relativePath = path.substring(1);

            for (File directory : directories)
            {
                File file = FileUtil.openFile(directory, relativePath);

                if ((file != null) && file.exists())
                {
                    return new LocalFile(file);
                }
            }
        }

        return null;
    }

    /*
    public void setApplicationCache(ApplicationCache applicationCache)
    {
        this.applicationCache = applicationCache;
    }
     */
}

/**
 * @author Clement Wong
 */
abstract class SourcePathBase
{
	protected final static String NO_DIR = "";

	static void addApplicationParentToSourcePath(VirtualFile appPath, VirtualFile[] classPath, List<File> directories)
	{
		if (appPath != null)
		{
			File f = FileUtil.openFile(appPath.getParent());
			// if (f != null && f.isDirectory())
			if (f != null && f.isDirectory() && (FileUtil.isSubdirectoryOf(appPath.getParent(), classPath) == -1))
			{
				directories.add(f);
			}
		}
	}

	static void addPathElements(VirtualFile[] classPath, List<File> directories, boolean allowSourcePathOverlap, List<ClasspathOverlap> warnings)
	{
		boolean badPaths = false;

		for (int i = 0, length = (classPath == null) ? 0 : classPath.length; i < length; i++)
		{
			String path = classPath[i].getName();
			File f = FileUtil.openFile(path);
			if (f != null && f.isDirectory())
			{
				if (!allowSourcePathOverlap && !badPaths)
				{
					int index = FileUtil.isSubdirectoryOf(f, directories);
					if (index != -1)
					{
						String dirPath = directories.get(index).getAbsolutePath();
						if (checkValidPackageName(path, dirPath))
						{
							// C: don't want to use ThreadLocalToolkit here...

	                        // preilly: don't use logError below, because we don't stop
	                        // compiling and if the error count is non-zero downstream mayhem
	                        // occurs.  For example, no SWC's get loaded, which makes it
	                        // alittle tough to compile.

							warnings.add(new ClasspathOverlap(path, dirPath));
							badPaths = true;
						}
					}
				}
				directories.add(f);
			}
		}		
	}
	
	private static boolean checkValidPackageName(String path1, String path2)
	{
		if (path1.equals(path2)) return true;
		String packagePath = path1.length() > path2.length() ? path1.substring(path2.length()) : path2.substring(path1.length());

		for (StringTokenizer t = new StringTokenizer(packagePath, File.separator); t.hasMoreTokens(); )
		{
			String s = t.nextToken();
			if (!flex2.compiler.mxml.lang.TextParser.isValidIdentifier(s))
			{
				return false;
			}
		}
		
		return true;
	}

	public SourcePathBase(String[] mimeTypes, boolean allowSourcePathOverlap)
	{
		this.mimeTypes = mimeTypes;
		this.allowSourcePathOverlap = allowSourcePathOverlap;
		sources = new HashMap<String, Source>();

		hits = new HashSet<String>();
		misses = new HashSet<String>(1024);
		dirs = new HashMap<String, String>();
		warnings = new ArrayList<ClasspathOverlap>(5);
	}

	protected final String[] mimeTypes;
	protected final Map<String, Source> sources;
	protected boolean allowSourcePathOverlap;

	protected final Set<String> hits, misses;
	protected final HashMap<String, String> dirs;
	protected final List<ClasspathOverlap> warnings;

    /*
	public Source findSource(String namespaceURI, String localPart) throws CompilerException
	{
		assert localPart.indexOf('.') == -1 && localPart.indexOf('/') == -1 && localPart.indexOf(':') == -1
                : "findSource(" + namespaceURI + "," + localPart + ") has bad localPart";

		// classname format is a.b:c
		String className = CompilerAPI.constructClassName(namespaceURI, localPart);
		
		return findSource(className, namespaceURI, localPart);
	}
	
	protected Source findSource(String className, String namespaceURI, String localPart) throws CompilerException
	{
		if (misses.contains(className))
		{
			return null;
		}

		Source s = sources.get(className);

		if (s == null)
		{
			if ((s = findFile(className, namespaceURI, localPart)) != null)
			{
				return s;
			}
		}

		CompilationUnit u = (s != null) ? s.getCompilationUnit() : null;

		if (s != null && !s.exists())
		{
			sources.remove(className);
			s = null;
		}

		if (adjustDefinitionName(namespaceURI, localPart, s, u))
		{
			u = null;
			s = null;
		}

		if (s != null && ((u != null && !u.isDone()) || s.isUpdated()))
		{
			// s.removeCompilationUnit();
		}
		else if (s != null && u != null)
		{
			s = s.copy();
			assert s != null;
		}

		if (s == null)
		{
			misses.add(className);
		}

		return s;
	}

	protected boolean adjustDefinitionName(String namespaceURI, String localPart, Source s, CompilationUnit u)
	{
		// If the compilation unit does exist and the top level definition name doesn't match
		// the specified class name, we don't count it as a match.
		if (s != null && u != null && u.topLevelDefinitions.size() == 1)
		{
			if (!u.topLevelDefinitions.contains(namespaceURI, localPart))
			{
				String realName = (u.topLevelDefinitions.first()).toString();
				sources.put(realName, s);
				misses.remove(realName);
				return true;
			}
		}
		
		return false;
	}
	
	abstract boolean checkPreference(Source s);

	protected abstract Source findFile(String className, String namespaceURI, String localPart) throws CompilerException;

	protected File findFile(File directory, String relativePath, String[] mimeTypes) throws CompilerException
	{
		File found = null;

		for (int k = 0, length = mimeTypes.length; k < length; k++)
		{
			File f = findFile(directory, relativePath, mimeTypes[k]);

			if (f != null && found == null)
			{
				found = f;
				// break;
			}
			else if (f != null)
			{
				throw new MoreThanOneComponentOfTheSameName(found.getAbsolutePath(), f.getAbsolutePath());
			}
		}

		return found;
	}

	protected File findFile(File directory, String relativePath, String mimeType)
	{
		String fullPath = directory.getPath() + File.separator + relativePath;
		int lastSlash = fullPath.lastIndexOf(File.separator);
		String dir = null;
		if (lastSlash != -1)
		{
			dir = fullPath.substring(0, lastSlash);
			if (dirs.get(dir) == NO_DIR)
			{
				return null;
			}
		}

		String path = relativePath + MimeMappings.getExtension(mimeType);
		File f = FileUtil.openFile(directory, path);

		if ((f != null) && f.isFile() && FileUtil.getCanonicalPath(f).endsWith(path))
		{
			return f;
		}
		else if (f != null && dir != null && !dirs.containsKey(dir))
		{
			File p = f.getParentFile();
			dirs.put(dir, p != null && p.isDirectory() ? dir : NO_DIR);
		}

		return null;
	}

	String[] checkClassNameFileName(Source s)
	{
		String defName = null, pathName = null;
		
		if (s.getOwner() == this)
		{
			QName def = s.getCompilationUnit().topLevelDefinitions.last();
			
			defName = def.getLocalPart();
			pathName = s.getShortName();
			
			if (defName.equals(pathName))
			{
				return null;
			}
		}
		
		return new String[] { pathName, defName };
	}

	String[] checkPackageNameDirectoryName(Source s)
	{
		String defPackage = null, pathPackage = null;
		
		if (s.getOwner() == this)
		{
			QName def = s.getCompilationUnit().topLevelDefinitions.last();
			
			defPackage = NameFormatter.normalizePackageName(def.getNamespace());		
			pathPackage = NameFormatter.toDot(s.getRelativePath(), '/');
			
			if (defPackage.equals(pathPackage))
			{
				return null;
			}
		}
		
		return new String[] { pathPackage, defPackage };
	}

	protected String constructRelativePath(Source s)
	{
		// + 1 removes the leading /
		String relativePath = s.getName().substring(s.getPathRoot().getName().length() + 1);
		for (int k = 0, length = mimeTypes.length; k < length; k++)
		{
			String ext = MimeMappings.getExtension(mimeTypes[k]);
			if (relativePath.endsWith(ext))
			{
				relativePath = relativePath.substring(0, relativePath.length() - ext.length());
				return relativePath;
			}
		}
		
		assert false;
		return null;
	}
	
	// used by CompilerAPI.validateCompilationUnits()... not efficient, but we rarely call it...
	public void removeSource(Source s)
	{
		for (Iterator i = sources.entrySet().iterator(); i.hasNext(); )
		{
			Map.Entry e = (Map.Entry) i.next();
			if (e.getValue() == s)
			{
				i.remove();
				return;
			}
		}

        assert false : "couldn't find " + s;
	}

	public void clearCache()
	{
		hits.clear();
		misses.clear();
		dirs.clear();
	}

	String[] getMimeTypes()
	{
		return mimeTypes;
	}

	public Map<String, Source> sources()
	{
		return sources;
	}

    public String toString()
    {
        StringBuilder buffer = new StringBuilder("SourcePath: \n");
		Iterator<Source> iterator = sources.values().iterator();

        while (iterator.hasNext())
		{
            Source source = iterator.next();
            buffer.append("\tsource = " + source + ", cu = " + source.getCompilationUnit() + "\n");
		}

        return buffer.toString();
    }
	
	public void displayWarnings()
	{
		for (int i = 0, size = warnings.size(); i < size; i++)
		{
			ThreadLocalToolkit.log(warnings.get(i));
		}
	}
    */

	// error messages

	public static class ClasspathOverlap extends CompilerMessage.CompilerWarning
	{
		private static final long serialVersionUID = -6314431057641028497L;

        public ClasspathOverlap(String path, String directory)
		{
			super();
			this.cpath = path;
			this.directory = directory;
		}

		public final String cpath, directory;
	}

	public static class MoreThanOneComponentOfTheSameName extends CompilerMessage.CompilerInfo
	{
		private static final long serialVersionUID = 5943423934006966281L;

        public MoreThanOneComponentOfTheSameName(String file1, String file2)
		{
			super();
			this.file1 = file1;
			this.file2 = file2;
		}

		public final String file1, file2;
	}
}
