/*
 * 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 flex.tools.debugger.cli;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;

import flash.tools.debugger.InProgressException;
import flash.tools.debugger.Isolate;
import flash.tools.debugger.NoResponseException;
import flash.tools.debugger.Session;
import flash.tools.debugger.SourceFile;
import flash.tools.debugger.SwfInfo;
import flash.util.IntMap;

/**
 * FileInfoCache manages a list of files that are unique
 * across multiple swfs.
 */
public class FileInfoCache implements Comparator<SourceFile>
{
	Session m_session;

	/**
	 * We can get at files by module id or path
	 */
	IntMap				m_byInt = new IntMap();
	HashMap<Integer, IntMap> m_isolateState = new HashMap<Integer, IntMap> ();
	
	private IntMap getIsolateState(int isolateId) {
		IntMap isolateState = null;
		if (!m_isolateState.containsKey(isolateId)) {
			isolateState = new IntMap();
			m_isolateState.put(isolateId, isolateState);
		}
		else
			isolateState = m_isolateState.get(isolateId);
		return isolateState;
	}
	
	SourceFile[]		m_files = null;
	SourceFile[]		m_isolateFiles = null;
	SwfInfo				m_swfFilter = null;
	int					m_swfsLoaded = 0;
    boolean             m_dirty = false;
    int lastActiveIsolate = Isolate.DEFAULT_ID;

	public FileInfoCache() {
		
	}

	public void			bind(Session s)									{ setSession(s); }
	public void			unbind()										{ m_session = null; }
	
	public SourceFile getFile(int i) {
		return getFile(i, Isolate.DEFAULT_ID);
	}

	public SourceFile getFile(int i, int isolateId) {
		populate();
		if (isolateId == Isolate.DEFAULT_ID)
			return (SourceFile) m_byInt.get(i);
		else
			return (SourceFile)getIsolateState(isolateId).get(i);
	}
	
	public SourceFile[] getFileList() {
		populate();
		return m_files;
	}
	
	public SourceFile[] getFileList(int isolateId) {
		populate();
		if (isolateId == Isolate.DEFAULT_ID) {
			final Object[] valuesToArray = m_byInt.valuesToArray(new Object[m_byInt.size()]);
	        return Arrays.copyOf(valuesToArray, valuesToArray.length, SourceFile[].class);
		} 
		else if (isolateId != lastActiveIsolate) {
			buildIsolateFiles(isolateId);
		}
		return m_isolateFiles;
	}
	
	private void buildIsolateFiles(int isolateId) {
		SwfInfo[] swfs = getSwfs(isolateId);
		boolean worked = true; // check that all worked correctly
		ArrayList<SourceFile> files = new ArrayList<SourceFile>();

		for(int i=0; i<swfs.length; i++)
		{
			if (swfs[i] != null)
				worked = loadSwfFiles(files, swfs[i]) ? worked : false;
		}

		// trim the file list
		ArrayList<SourceFile> fa = trimFileList(files);
		m_isolateFiles = fa.toArray( new SourceFile[fa.size()] );

		// sort this array in place so calls to getFileList will be ordered
		Arrays.sort(m_isolateFiles, this);
	}

	public Iterator getAllFiles(int isolateId) {
		populate();		
		if (isolateId == Isolate.DEFAULT_ID)
			return m_byInt.iterator();
		else
			return getIsolateState(isolateId).iterator();
	}
	
    public SwfInfo      getSwfFilter()                                  { return m_swfFilter; }
    public boolean      isSwfFilterOn()                                 { return (m_swfFilter != null); }
    public void         setDirty()                                      { m_dirty = true; }

	void setSession(Session s)
	{
		m_session = s;
		m_swfFilter = null;
		clear();
	}
	
	SwfInfo[] getAllSwfs() {
		ArrayList<SwfInfo> result = new ArrayList<SwfInfo>();
		
		for ( Isolate isolate : m_session.getWorkers()) {
			SwfInfo[] swfs = new SwfInfo[0];
			try {
				swfs = m_session.getWorkerSession(isolate.getId()).getSwfs();
			} catch (NoResponseException e) {
				swfs = new SwfInfo[0];
			}
			
			for (SwfInfo swf : swfs)
				result.add(swf);
		}
		
		return result.toArray(new SwfInfo[0]);
	}

	void populate()
	{
		// do we have a new swf to load?
		if (m_session != null && (m_dirty || getAllSwfs().length > m_swfsLoaded))
			reloadCache();
	}

	void reloadCache()
	{
		clear();
		loadCache();
        m_dirty = false;
	}

	void clear()
	{
		m_byInt.clear();
		m_isolateState.clear();
		m_files = null;
	}

	/**
	 * Determine if the given SourceFile is in the current fileList
	 */
	public boolean inFileList(SourceFile f)
	{
		boolean isIt = false;

		SourceFile[] files = getFileList();
		for(int i=0; i<files.length && !isIt; i++)
		{
			if (files[i] == f)
				isIt = true;
		}
		return isIt;
	}

	/**
	 * Go out to the session and request a list of files
	 * But we dump ones that have a name collision.
	 * Also if selectedSwf is set then we only add files
	 * that are contained within the given swf.
	 */
	void loadCache()
	{
		boolean worked = true; // check that all worked correctly
		ArrayList<SourceFile> files = new ArrayList<SourceFile>();
		SwfInfo[] swfs = getAllSwfs();
		for(int i=0; i<swfs.length; i++)
		{
			if (swfs[i] != null)
				worked = loadSwfFiles(files, swfs[i]) ? worked : false;
		}

		// trim the file list
		ArrayList<SourceFile> fa = trimFileList(files);
		m_files = fa.toArray( new SourceFile[fa.size()] );

		// sort this array in place so calls to getFileList will be ordered
		Arrays.sort(m_files, this);

		// mark our cache complete if all was good.
		if (worked)
			m_swfsLoaded = swfs.length;
	}

	boolean loadSwfFiles(ArrayList<SourceFile> ar, SwfInfo swf)
	{
		boolean worked = true;
		try
		{
			// @todo should we include unloaded swfs?
			SourceFile[] files = swf.getSourceList(m_session);
			ar.ensureCapacity(ar.size()+files.length);

			// add each file to our global source file IntMap and our list
			for(int i=0; i<files.length; i++)
			{
				putFile(files[i], swf.getIsolateId());
				ar.add(files[i]);
			}
		}
		catch(InProgressException ipe)
		{
			// can't load this one, its not ready yet
			worked = false;
		}
		return worked;
	}

	/**
	 * Walk the file list looking for name collisions.
	 * If we find one, then we remove it
	 */
	ArrayList<SourceFile> trimFileList(ArrayList<SourceFile> files)
	{
		HashMap<String, String> names = new HashMap<String, String>();
		ArrayList<SourceFile> list = new ArrayList<SourceFile>();

		int size = files.size();
		for(int i=0; i<size; i++)
		{
			boolean addIt = false;

			SourceFile fi = files.get(i);
			// no filter currently in place so we add the file as long
			// as no duplicates exist.  We use the original Swd full
			// name for matching.
			String fName = fi.getRawName();
			if (m_swfFilter == null)
			{
				// If it exists, then we don't add it!
				if (names.get(fName) == null)
					addIt = true;
			}
			else
			{
				// we have a filter in place so, see
				// if the source file is in our currently
				// selected swf.
				addIt = m_swfFilter.containsSource(fi);
			}

			// did we mark this one to add?
			if (addIt)
			{
				names.put(fName, fName);
				list.add(fi);
			}
		}
		return list;
	}
	
	/**
	 * All files from all swfs are placed into our byInt map
	 * since we know that ids are unique across the entire
	 * Player session.
	 *
	 * This is also important in the case that the player
	 * halts in one of these files, that the debugger
	 * be able to locate the SourceFile so that we can
	 * display the correct context for the user.
	 */
	void putFile(SourceFile s, int isolateId)
	{
		int i = s.getId();
		if (isolateId == Isolate.DEFAULT_ID)
			m_byInt.put(i, s);
		else
			getIsolateState(isolateId).put(i, s);
	}

	/**
	 * Attempt to set a swf as a filter
	 * for the file list that we create
	 */
	public boolean setSwfFilter(String swfName)
	{
		// look for a match in our list
		boolean worked = false;
		if (swfName == null)
		{
			m_swfFilter = null;
			worked = true;
		}
		else
		{
			SwfInfo[] swfs = getAllSwfs();
			for(int i=0; i<swfs.length; i++)
			{
				SwfInfo e = swfs[i];
				if (e != null && nameOfSwf(e).equalsIgnoreCase(swfName))
				{
					worked = true;
					m_swfFilter = e;
					break;
				}
			}
		}

		// reload if it worked
		if (worked)
			reloadCache();

		return worked;
	}

	// list all swfs we know about
	public SwfInfo[] getSwfs(int isolateId)
	{
		return getSwfsIsolate(isolateId);
	}
	
	public SwfInfo[] getSwfsIsolate(int isolateId)
	{
		SwfInfo[] swfs = null;
		try
		{
			swfs = m_session.getWorkerSession(isolateId).getSwfs();
		}
		catch(NoResponseException nre)
		{
			swfs = new SwfInfo[] {};  // oh bery bad
		}
		return swfs;
	}

	/**
	 * Given a SourceFile locate the swf which it came from
	 */
	public SwfInfo swfForFile(SourceFile f, int isolateId)
	{
		// We use the id to determine which swf this source files resides in
		int id = f.getId();
		SwfInfo info = null;
		SwfInfo[] swfs = getSwfs(isolateId);//getAllSwfs();
		for(int i=0; ( i<swfs.length && (info == null) ); i++)
		{
			if (swfs[i] != null && swfs[i].containsSource(f))
				info = swfs[i];
		}
		return info;
	}

	// locate the name of the swf
	public static String nameOfSwf(SwfInfo e)
	{
		int at = -1;
		String name = e.getUrl();
		if ( (at = e.getUrl().lastIndexOf('/')) > -1)
			name = e.getUrl().substring(at+1);
		if ( (at = e.getUrl().lastIndexOf('\\')) > -1)
			name = e.getUrl().substring(at+1);
		else if ( (at = e.getPath().lastIndexOf('\\')) > -1)
			name = e.getPath().substring(at+1);
		else if ( (at = e.getPath().lastIndexOf('/')) > -1)
			name = e.getPath().substring(at+1);

		// now rip off any trailing ? options
		at = name.lastIndexOf('?');
		name = (at > -1) ? name.substring(0, at) : name;

		return name;
	}

	// locate the name of the swf
	public static String shortNameOfSwf(SwfInfo e)
	{
		String name = nameOfSwf(e);

		// now strip off any leading path
		int at = -1;
		if ( (at = name.lastIndexOf('/')) > -1)
			name = name.substring(at+1);
		else if ( (at = name.lastIndexOf('\\')) > -1)
			name = name.substring(at+1);
		return name;
	}

    /**
     * Given the URL of a specfic swf determine
     * if there is a file within it that appears
     * to be the same as the given source file
     * @param f
     * @return
     */
    public SourceFile similarFileInSwf(SwfInfo info, SourceFile f) throws InProgressException
    {
        SourceFile hit = null;
		SourceFile[] files = info.getSourceList(m_session);
		if (!info.isProcessingComplete())
			throw new InProgressException();

		for(int i=0; i<files.length; i++)
		{
			if (filesMatch(f, files[i]))
				hit = files[i];
		}
        return hit;
    }

	/**
	 * Comparator interface for sorting SourceFiles
	 */
	public int compare(SourceFile o1, SourceFile o2)
	{
		String n1 = o1.getName();
		String n2 = o2.getName();

		return n1.compareTo(n2);
	}

    /**
     * Compare two files and determine if they are the same.
     * Our criteria included only line count package names
     * and the name of the class itself.  If there are
     * any other differences then we won't be able to detect
     * them.  We should probably do something like an MD5
     * computation on the characters in ScriptText. Then
     * we'd really be sure of a match.
     * @param a first file to compare
     * @param b second file to compare
     * @return  true if files appear to be the same
     */
    public boolean filesMatch(SourceFile a, SourceFile b)
    {
        boolean yes = true;

		if (a == null || b == null)
			yes = false;
        else if (a.getPackageName().compareTo(b.getPackageName()) != 0)
            yes = false;
        else if (a.getName().compareTo(b.getName()) != 0)
            yes = false;
        else if (a.getLineCount() != b.getLineCount()) // warning, this is sometimes expensive, so do it last
            yes = false;

        return yes;
    }
    /**
     * Return a array of SourceFiles whose names match
     * the specified string. The array is sorted by name.
	 * The input can be mx.controls.xxx which will
     */
    public SourceFile[] getFiles(String matchString)
    {
        return getFiles(matchString, -1);
    }

    public SourceFile[] getFiles(String matchString, int isolateId)
    {
        boolean doStartsWith = false;
        boolean doIndexOf = false;
        boolean doEndsWith = false;

        boolean leadingAsterisk = matchString.startsWith("*") && matchString.length() > 1; //$NON-NLS-1$
        boolean trailingAsterisk = matchString.endsWith("*"); //$NON-NLS-1$
        boolean usePath = matchString.indexOf('.') > -1;

        if (leadingAsterisk && trailingAsterisk)
        {
            matchString = matchString.substring(1, matchString.length() - 1);
            doIndexOf = true;
        }
        else if (leadingAsterisk)
        {
            matchString = matchString.substring(1);
            doEndsWith = true;
        }
        else if (trailingAsterisk)
        {
            matchString = matchString.substring(0, matchString.length() - 1);
            doStartsWith = true;
        }
		else if (usePath)
		{
			doIndexOf = true;
		}
		else
        {
            doStartsWith = true;
        }

        SourceFile[] files = isolateId > -1 ? getFileList(isolateId) : getFileList();
        ArrayList<SourceFile> fileList = new ArrayList<SourceFile>();
        int n = files.length;
		int exactHitAt = -1;
		// If the matchString already starts with "." (e.g. ".as" or ".mxml"), then dotMatchString
		// will be equal to matchString; otherwise, dotMatchString will be "." + matchString
		String dotMatchString = (matchString.startsWith(".")) ? matchString : ("." + matchString); //$NON-NLS-1$ //$NON-NLS-2$
        for (int i = 0; i < n; i++)
        {
            SourceFile sourceFile = files[i];
			boolean pathExists = (usePath && sourceFile.getFullPath().matches(".*[/\\\\].*")); //$NON-NLS-1$
            String name = pathExists ? sourceFile.getFullPath() : sourceFile.getName();

			// if we are using the full path string, then prefix a '.' to our matching string so that abc.as and Gabc.as don't both hit
			String match = (usePath && pathExists) ? dotMatchString : matchString;

            match = match.replace('/', '.');  // get rid of path identifiers and use dots
            match = match.replace('\\', '.');

			name = name.replace('/', '.');  // get rid of path identifiers and use dots
			name = name.replace('\\', '.'); // would be better to modify the input string, but we don't know which path char will be used.

			// exact match? We are done
			if (name.equals(match))
			{
				exactHitAt = i;
				break;
			}
            else if (doStartsWith && name.startsWith(match))
				fileList.add(sourceFile);
			else if (doEndsWith && name.endsWith(match))
                fileList.add(sourceFile);
			else if (doIndexOf && name.contains(match))
				fileList.add(sourceFile);
        }

		// trim all others if we have an exact file match
		if (exactHitAt > -1)
		{
			fileList.clear();
			fileList.add(files[exactHitAt]);
		}

		SourceFile[] fileArray = fileList.toArray( new SourceFile[fileList.size()] );
		Arrays.sort(fileArray, this);
        return fileArray;
    }

}
