/*
 *
 *  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.tools.oem.internal;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import flex2.compiler.Source;
import flex2.compiler.SourceList;
import flex2.compiler.common.Configuration;
import flex2.compiler.common.Configuration.RslPathInfo;
import flex2.compiler.io.FileUtil;
import flex2.compiler.io.VirtualFile;
import flex2.linker.SimpleMovie;
import flex2.tools.VersionInfo;
import flex2.tools.oem.Message;
import flex2.tools.oem.Report;

/**
 * A Report implementation.
 * 
 * @version 2.0.1
 * @author Clement Wong
 */
public class OEMReport implements Report
{
    public OEMReport(List<Source> sources, SimpleMovie movie, Configuration configuration,
                     SourceList sourceList, String configurationReport, List<Message> messages)
    {
        this(sources, movie, configuration, sourceList, configurationReport, messages, null);
    }
    
    public OEMReport(List<Source> sources, SimpleMovie movie, Configuration configuration,
                     SourceList sourceList, String configurationReport, List<Message> messages,
					 Map<String, VirtualFile> archiveFiles)
	{
        if (sourceList != null)
        {
            sourceListPaths = sourceList.getPaths();
        }

		init(sources, /* null,*/
			 configuration == null ? null : configuration.getResourceBundles(),
			 archiveFiles, configuration);

		//processFrames(movie);
		processMessages(messages);
		
		this.frameCount = 1 ;
		this.configurationReport = configurationReport;
		
		defaultWidth = configuration != null ? configuration.defaultWidth() : 0;
		defaultHeight = configuration != null ? configuration.defaultHeight() : 0;

		width = 0;
		height = 0;
		widthPercent = 0.0;            
		heightPercent = 0.0;
        
        if (movie != null)
		{
			linkReport = movie.getLinkReport();
			sizeReport = movie.getSizeReport();
			bgColor = movie.bgcolor.color;
			pageTitle = movie.pageTitle;
			
			if (movie.userSpecifiedWidth)
			{
				width = movie.width;
			}
			else if (configuration != null)
            {
                String percent = configuration.widthPercent();
                if (percent != null)
                {
                    percent = percent.trim();
                    // Percent character is expected but allow it to be optional.
                    if (percent.length() >= 1 &&
                        percent.charAt(percent.length() - 1) == '%')
                    {
                        percent = percent.substring(0, percent.length() - 1);
                    }
                    try
                    {
                        widthPercent = Double.parseDouble(percent) / 100;
                    }
                    catch(NumberFormatException ex) {}
                }
            }

			if (movie.userSpecifiedHeight)
			{
				height = movie.height;
			}
			else if (configuration != null)
            {
                String percent = configuration.heightPercent();
                if (percent != null)
                {
                    percent = percent.trim();
                    // Percent character is expected but allow it to be optional.
                    if (percent.length() >= 1 &&
                        percent.charAt(percent.length() - 1) == '%')
                    {
                        percent = percent.substring(0, percent.length() - 1);
                    }
                    try
                    {
                        heightPercent = Double.parseDouble(percent) / 100;
                    }
                    catch(NumberFormatException ex) {}
                }
            }
		}
		else
		{
			linkReport = null;
			sizeReport = null;
			bgColor = 0;
			pageTitle = null;
		}
	}
	
	private void init(List<Source> sources, /* List<CompilationUnit> exportedUnits,*/ Set<String> resourceBundles,
	                  Map<String, VirtualFile> archiveFiles,
	                  Configuration configuration)
	{
		TreeSet<String> sourceNames = new TreeSet<String>();
		TreeSet<String> assetNames = new TreeSet<String>();
		TreeSet<String> libraryNames = new TreeSet<String>();
		
		data = new HashMap<String, Data>();
		locations = new HashMap<String, String>();
		
        if (sources != null)
        {
            processSources(sources, sourceNames, assetNames, libraryNames, data, locations);
        }

		timestamps = new HashMap<String, Long>();

        // Store timestamps for each path in the SourceList, so
        // contentUpdated() can report true if new sources are added
        // to the SourceList.
        if (sourceListPaths != null)
        {
            for (File path : sourceListPaths)
            {
                storeTimestamps(path);
            }
        }

		compiler_SourceNames = toArray(sourceNames);
		storeTimestamps(compiler_SourceNames);

		compiler_AssetNames = toArray(assetNames);
		storeTimestamps(compiler_AssetNames);

		compiler_LibraryNames = toArray(libraryNames);
		storeTimestamps(compiler_LibraryNames);
		
		resourceBundleNames = toArray(resourceBundles);

		sourceNames.clear();
		assetNames.clear();
		libraryNames.clear();
		
		//processCompilationUnits(exportedUnits, sourceNames, assetNames, libraryNames);
			
        // Add files to the assets set so they are included in the timestamps.
        // FB calls the OEMReports.contentUpdated() method to determine if a 
		// file has changed and if so it will recompile. This code was added 
		// for the specific case where a default.css file was modified but FB
		// didn't think it needed to recompile because default.css wasn't in
		// the timestamps map.
        if (archiveFiles != null)
        {
            for (String fileName: archiveFiles.keySet())
            {
                assetNames.add(fileName);
            }
        }

		linker_SourceNames = toArray(sourceNames);
		storeTimestamps(linker_SourceNames);

		linker_AssetNames = toArray(assetNames);
		storeTimestamps(linker_AssetNames);

		linker_LibraryNames = toArray(libraryNames);		
		storeTimestamps(linker_LibraryNames);

        // Add libraries linked as RSLs to the timestamp map.
        // Saving the digests of the libraries would be better than a 
        // timestamp since we would know if the digest changed, not
        // just something in the library. We can't compare digests because
        // contentUpdated() does not have access to the swcs we will 
        // compile with. Timestamps may cause us to recompile an app when
        // the RSL has not been modified but that should be pretty rare.
        if (configuration != null)
        {
            List<RslPathInfo> rslPathInfoList = configuration.getRslPathInfo();
            if (rslPathInfoList.size() > 0)
            {
                String[] rslPaths = new String[rslPathInfoList.size()];
                int i = 0;
                for (RslPathInfo rslPathInfo : rslPathInfoList)
                {
                    rslPaths[i++] = rslPathInfo.getSwcVirtualFile().getName();
                }

                storeTimestamps(rslPaths);
            }
        }
	}

    private void storeTimestamps(File path)
    {
        // (SDK-30367) Timestamp caching was added after Flex 3 but it causes
        // performance issues as it also checks files under hidden directories,
        // e.g. .svn. Skipping checking the timestamp of hidden files and
        // folders helps reduce the time spent in this method.
        if (path.isHidden()) 
            return;

        timestamps.put(FileUtil.getCanonicalPath(path), path.lastModified());

        for (File file : path.listFiles())
        {
            if (file.isDirectory())
            {
                storeTimestamps(file);
            }
        }
    }
	
	private void storeTimestamps(String[] a)
	{
        if (a != null)
        {
            for (String fileName : a)
            {
                if ((fileName != null) && !timestamps.containsKey(fileName))
                {
                    timestamps.put(fileName, new File(fileName).lastModified());
                }
            }
        }
	}
	
	private String[] compiler_SourceNames, compiler_AssetNames, compiler_LibraryNames;
	private String[] linker_SourceNames, linker_AssetNames, linker_LibraryNames;
	private String[] resourceBundleNames;
	private Map<String, Data> data;
    private Map<String, String> locations;
    private Map<String, Long> timestamps;
	
	private int frameCount;
	private int bgColor, defaultWidth, defaultHeight, width, height;
	private String pageTitle;
	private double widthPercent, heightPercent;
	
	private String linkReport, sizeReport, configurationReport;
	private Message[] messages;
	
	//private String[][] assetNames, definitionNames;
    private List<File> sourceListPaths;
	
	public boolean contentUpdated()
	{
		for (Iterator<String> i = timestamps.keySet().iterator(); i.hasNext(); )
		{
			String path = i.next();
			Long ts = timestamps.get(path);
			File f = new File(path);
			
			if (!f.exists() || f.lastModified() != ts.longValue())
			{
				return true;
			}
		}
		return false;
	}
	
	public String[] getSourceNames(Object report)
	{
		return (COMPILER.equals(report)) ? compiler_SourceNames : (LINKER.equals(report)) ? linker_SourceNames : null;
	}

	public String[] getAssetNames(int frame)
	{
	    return new String[0];
		//return assetNames[frame - 1];
	}
	
	public String[] getAssetNames(Object report)
	{
		return (COMPILER.equals(report)) ? compiler_AssetNames : (LINKER.equals(report)) ? linker_AssetNames : null;
	}

	public String[] getLibraryNames(Object report)
	{
		return (COMPILER.equals(report)) ? compiler_LibraryNames : (LINKER.equals(report)) ? linker_LibraryNames : null;
	}

	public String[] getResourceBundleNames()
	{
		return resourceBundleNames;
	}
	
	public String[] getDefinitionNames(int frame)
	{
        return new String[0];
		//return definitionNames[frame - 1];
	}
	
	public String[] getDefinitionNames(String sourceName)
	{
		Data d = data.get(sourceName);
		return d == null ? null : d.definitions;
	}
	
	public String getLocation(String definition)
	{
		return locations.get(definition);
	}
	
	public String[] getDependencies(String definition)
	{
		String location = getLocation(definition);
		
		if (location != null)
		{
			Data d = data.get(location);
			return d == null ? null : d.dependencies;
		}
		else
		{
			return null;
		}
	}

	public String[] getPrerequisites(String definition)
	{
		String location = getLocation(definition);
		
		if (location != null)
		{
			Data d = data.get(location);
			return d == null ? null : d.prerequisites;
		}
		else
		{
			return null;
		}
	}
	
	public long writeLinkReport(Writer out) throws IOException
	{
		long size = 0;
		
		if (linkReport != null)
		{
			out.write(linkReport);
			out.flush();
			size = linkReport.length();
		}
		
		return size;
	}
	
	public long writeSizeReport(Writer out) throws IOException
	{
		long size = 0;
		
		if (sizeReport != null)
		{
			out.write(sizeReport);
			out.flush();
			size = sizeReport.length();
		}
		
		return size;
	}

	public long writeConfigurationReport(Writer out) throws IOException
	{
		long size = 0;
		
		if (configurationReport != null)
		{
			out.write(configurationReport);
			out.flush();
			size = configurationReport.length();
		}
		
		return size;
	}
	
	public int getBackgroundColor()
	{
		return bgColor;
	}
	
	public String getPageTitle()
	{
		return pageTitle;
	}
	
	public int getDefaultWidth()
	{
		return defaultWidth;
	}
	
	public int getDefaultHeight()
	{
		return defaultHeight;
	}
	
	public int getWidth()
	{
		return width;
	}
	
	public int getHeight()
	{
		return height;
	}
	
	public double getWidthPercent()
	{
		return widthPercent;
	}
	
	public double getHeightPercent()
	{
		return heightPercent;
	}
	
	public String getCompilerVersion()
	{
		return VersionInfo.buildMessage();
	}

	public Message[] getMessages()
	{
		return messages;
	}
	
	public int getFrameCount()
	{
		return frameCount;
	}
	
	/*
	private void processCompilationUnits(List<CompilationUnit> units, TreeSet<String> sourceNames,
                                         TreeSet<String> assetNames, TreeSet<String> libraryNames)
	{
		for (int i = 0, length = units == null ? 0 : units.size(); i < length; i++)
		{
			CompilationUnit u = units.get(i);
			Source s = (u == null) ? null : u.getSource();
			
			if (s == null)
			{
				continue;
			}
			
			if (s.isFileSpecOwner() || s.isSourceListOwner() || s.isSourcePathOwner() || s.isResourceBundlePathOwner())
			{
				sourceNames.add(s.getName());
				
				for (Iterator j = s.getFileIncludes(); j.hasNext(); )
				{
					VirtualFile f = (VirtualFile) j.next();
					sourceNames.add(f.getName());
				}

				if (u.hasAssets())
                {
                    for (Iterator j = u.getAssets().iterator(); j.hasNext(); )
                    {
                        Map.Entry e = (Map.Entry) j.next();
                        AssetInfo assetInfo = (AssetInfo) e.getValue();
                        VirtualFile path = assetInfo.getPath();
                        if (path != null)
                        {
                            assetNames.add(path.getName());
                        }
                    }
                }
			}
			else if (s.isSwcScriptOwner())
			{
				String location = ((SwcScript) s.getOwner()).getLibrary().getSwcLocation(); 
				libraryNames.add(location);
			}
		}
	}
	*/
	
	private void processSources(List<Source> sources, TreeSet<String> sourceNames, TreeSet<String> assetNames,
                                TreeSet<String> libraryNames, Map<String, Data> data, Map<String, String> locations)
	{
		// use this version for now
		for (Source s : sources)
		{
			sourceNames.add(s.getSourceFileName());
		}
		/*
		// AJH not sure why all this is needed
		for (Source s : sources)
		{
			CompilationUnit u = (s == null) ? null : s.getCompilationUnit();
			
			if (s == null)
			{
				continue;
			}
			
			if (s.isFileSpecOwner() || s.isSourceListOwner() || s.isSourcePathOwner() || s.isResourceBundlePathOwner())
			{
				sourceNames.add(s.getName());
				
				for (Iterator j = s.getFileIncludes(); j.hasNext(); )
				{
					VirtualFile f = (VirtualFile) j.next();
					sourceNames.add(f.getName());
				}

				if (u.hasAssets())
                {
                    for (Iterator j = u.getAssets().iterator(); j.hasNext(); )
                    {
                        Map.Entry e = (Map.Entry) j.next();
                        AssetInfo assetInfo = (AssetInfo) e.getValue();
                        VirtualFile path = assetInfo.getPath();
                        if (path != null)
                        {
                            assetNames.add(assetInfo.getPath().getName());
                        }
                    }
                }

				if (locations != null)
				{
					for (int j = 0, size = u.topLevelDefinitions.size(); j < size; j++)
					{
						locations.put(u.topLevelDefinitions.get(j).toString(), s.getName());
					}
				}
			}
			else if (s.isSwcScriptOwner())
			{
				String location = ((SwcScript) s.getOwner()).getLibrary().getSwcLocation(); 
				libraryNames.add(location);
				
				if (locations != null)
				{
					for (int j = 0, size = u.topLevelDefinitions.size(); j < size; j++)
					{
						locations.put(u.topLevelDefinitions.get(j).toString(), location);
					}
				}
			}
		}

		for (Source s : sources)
		{
			CompilationUnit u = (s == null) ? null : s.getCompilationUnit();
			
			if (s == null)
			{
				continue;
			}
			
			if (s.isFileSpecOwner() || s.isSourceListOwner() || s.isSourcePathOwner() || s.isResourceBundlePathOwner())
			{
				Data d = new Data();
				d.definitions = toArray(u.topLevelDefinitions);
				d.prerequisites = toArray(u.inheritance, null, locations);
                Set<Name> nameSet = new HashSet<Name>();
                nameSet.addAll(u.namespaces);
                nameSet.addAll(u.types);
                nameSet.addAll(u.expressions);
				d.dependencies = toArray(nameSet, new Set[] { u.extraClasses, u.resourceBundleHistory }, locations);
					
				data.put(s.getName(), d);
			}
		}
		*/
	}

	/*
	private void processFrames(SimpleMovie movie)
	{
		int count = movie == null ? 0 : movie.frames.size();
		assetNames = new String[count][];
		definitionNames = new String[count][];
		
		for (int i = 0; i < count; i++)
		{
			Frame f = movie.frames.get(i);
			List<CompilationUnit> units = movie.getExportedUnitsByFrame(f);
            List<String> aList = new ArrayList<String>(), dList = new ArrayList<String>();
			for (int j = 0, size = units == null ? 0 : units.size(); j < size; j++)
			{
				CompilationUnit u = units.get(j);
				Source s = u.getSource();
				
                if (u.hasAssets())
                {
                    for (Iterator k = u.getAssets().iterator(); k.hasNext(); )
                    {
                        Map.Entry e = (Map.Entry) k.next();
                        AssetInfo assetInfo = (AssetInfo) e.getValue();
                        VirtualFile path = assetInfo.getPath();
                        if (path != null)
                        {
                            String assetName = path.getName();
                            if (!aList.contains(assetName))
                            {
                                aList.add(assetName);
                            }
                        }
					}
				}

				if (s.isFileSpecOwner() || s.isResourceBundlePathOwner() || s.isSourceListOwner() ||
					s.isSourcePathOwner() || s.isSwcScriptOwner())
				{
					for (Iterator k = u.topLevelDefinitions.iterator(); k.hasNext(); )
					{
						String definitionName = k.next().toString();
						dList.add(definitionName);
					}
				}
			}
			
			if (aList.size() > 0)
			{
				assetNames[i] = new String[aList.size()];
				aList.toArray(assetNames[i]);
			}
			
			if (dList.size() > 0)
			{
				definitionNames[i] = new String[dList.size()];
				dList.toArray(definitionNames[i]);
			}
		}
	}
	*/
	private void processMessages(List<Message> messages)
	{
		if (messages != null && messages.size() > 0)
		{
			List<Message> filtered = new ArrayList<Message>();
			
			for (int i = 0, length = messages.size(); i < length; i++)
			{
				Message m = messages.get(i);
				if (m != null && !Message.INFO.equals(m.getLevel()))
				{
					filtered.add(m);
				}
			}
			
			messages = filtered;
		}
			
		if (messages != null && messages.size() > 0)
		{
			this.messages = new Message[messages.size()];
			for (int i = 0, length = this.messages.length; i < length; i++)
			{
				this.messages[i] = new GenericMessage(messages.get(i));
			}
		}
		else
		{
			this.messages = null;
		}
	}
	
	private String[] toArray(Set<String> set)
	{
		String[] a = new String[set == null ? 0 : set.size()];
		int j = 0;
		
		if (set != null)
		{
            for (String source : set)
			{
				a[j++] = source;
			}
		}
		
		return a.length == 0 ? null : a;
	}

	/*
	private String[] toArray(QNameList definitions)
	{
		String[] a = new String[definitions == null ? 0 : definitions.size()];
		
		for (int i = 0; i < a.length; i++)
		{
			a[i] = definitions.get(i).toString();
		}
		
		return a.length == 0 ? null : a;
	}

	private String[] toArray(Set<Name> nameSet, Set[] sets, Map locations)
	{
		TreeSet<String> set = new TreeSet<String>();
		
        for (Name name : nameSet)
		{
            String qName = null;
            if (name instanceof QName && (locations == null || locations.containsKey(qName = name.toString())))
            {
                set.add(qName);
            }
        }

		for (int i = 0, length = sets == null ? 0 : sets.length; i < length; i++)
		{
			if (sets[i] != null)
			{
				for (Object obj : sets[i])
				{
					if ((obj instanceof String) && (locations == null || locations.containsKey(obj)))
					{
						set.add((String)obj);
					}
				}
			}
		}

		return toArray(set);
	}
    */
	static class Data
	{
		String[] definitions;
		String[] prerequisites;
		String[] dependencies;
	}
}
