package org.gudy.azureus2.core3.download.impl;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.FileUtil;

import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.savelocation.DefaultSaveLocationManager;
import org.gudy.azureus2.plugins.download.savelocation.SaveLocationChange;
import org.gudy.azureus2.pluginsimpl.local.download.DownloadImpl;

import com.aelitis.azureus.core.tag.Tag;
import com.aelitis.azureus.core.tag.TagFeature;
import com.aelitis.azureus.core.tag.TagFeatureFileLocation;
import com.aelitis.azureus.core.tag.TagManagerFactory;


public class DownloadManagerDefaultPaths extends DownloadManagerMoveHandlerUtils {
	
	public final static DefaultSaveLocationManager DEFAULT_HANDLER = new DefaultSaveLocationManager() {
		public SaveLocationChange onInitialization(Download d, boolean for_move, boolean on_event) {
			
			/**
			 * This manager object isn't the sort of object which decides on
			 * an alternate initialisation place for a download - if a user
			 * has chosen a path for it, we don't interfere with it under any
			 * circumstances (though if plugins want to, then that's up to them). 
			 */
			if (on_event) {return null;}
			
			DownloadManager dm = ((DownloadImpl)d).getDownload();
			return determinePaths(dm, UPDATE_FOR_MOVE_DETAILS[1], for_move, false); // 1 - incomplete downloads
		}
		public SaveLocationChange onCompletion(Download d, boolean for_move, boolean on_event) {
			DownloadManager dm = ((DownloadImpl)d).getDownload();
			MovementInformation mi = getTagMovementInformation( dm, COMPLETION_DETAILS );
			return determinePaths(dm, mi, for_move, false);
		}
		public SaveLocationChange testOnCompletion(Download d, boolean for_move, boolean on_event) {
			DownloadManager dm = ((DownloadImpl)d).getDownload();
			MovementInformation mi = getTagMovementInformation( dm, COMPLETION_DETAILS );
			return determinePaths(dm, mi, for_move, true );
		}
		public SaveLocationChange onRemoval(Download d, boolean for_move, boolean on_event) {
			DownloadManager dm = ((DownloadImpl)d).getDownload();
			return determinePaths(dm, REMOVAL_DETAILS, for_move, false );
		}
		public boolean isInDefaultSaveDir(Download d) {
			DownloadManager dm = ((DownloadImpl)d).getDownload();
			return DownloadManagerDefaultPaths.isInDefaultDownloadDir(dm);
		}
	};
	
    private final static MovementInformation COMPLETION_DETAILS;
    private final static MovementInformation REMOVAL_DETAILS;
    private final static MovementInformation[] UPDATE_FOR_MOVE_DETAILS;
    private final static TargetSpecification[] DEFAULT_DIRS;

    private final static String SUBDIR_PARAM = "File.move.subdir_is_default";


    static {
        SourceSpecification source;
        TargetSpecification dest;
        TransferSpecification trans;
        MovementInformation mi_1, mi_2;

        /**
         * There are three sets of directories that we consider a "default"
         * directory (perhaps it should just be two):
         *
         * - default save dir
         * - completed save dir
         * - removed save dir
         */
        DEFAULT_DIRS = new TargetSpecification[3];
        dest = new TargetSpecification();
        dest.setBoolean("enabled", true);
        dest.setString("target", "Default save path");
        dest.setContext("default save dir");
        DEFAULT_DIRS[0] = dest;

        // First - download completion details.
		source = new SourceSpecification();
		source.setBoolean("default dir", "Move Only When In Default Save Dir");
		source.setBoolean("default subdir", SUBDIR_PARAM);
		source.setBoolean("incomplete dl", false);

		dest = new TargetSpecification();
		dest.setBoolean("enabled", "Move Completed When Done");
		dest.setString("target", "Completed Files Directory");
		dest.setContext("completed files dir");

		trans = new TransferSpecification();
		trans.setBoolean("torrent", "Move Torrent When Done");

		mi_1 = new MovementInformation(source, dest, trans, "Move on completion");
		COMPLETION_DETAILS = mi_1;
		DEFAULT_DIRS[1] = dest;

		// Next - download removal details.
		source = new SourceSpecification();
		source.setBoolean("default dir", "File.move.download.removed.only_in_default");
		source.setBoolean("default subdir", SUBDIR_PARAM);
		source.setBoolean("incomplete dl", false);

		dest = new TargetSpecification();
		dest.setBoolean("enabled", "File.move.download.removed.enabled");
		dest.setString("target", "File.move.download.removed.path");
		dest.setContext("removed files dir");

		trans = new TransferSpecification();
		trans.setBoolean("torrent", "File.move.download.removed.move_torrent");

		mi_1 = new MovementInformation(source, dest, trans, "Move on removal");
		REMOVAL_DETAILS = mi_1;
		DEFAULT_DIRS[2] = dest;

	    /**
	     * Next - updating the current path (complete dl's first)
	     * 
	     * We instantiate the "update incomplete download" source first, and then
	     * we instantiate the "update complete download", but when we process, we
	     * will do the complete download bit first.
	     * 
	     * We do this, because in the "update incomplete download" section, completed
	     * downloads are enabled for it. And the reason it is, is because this will
	     * allow the code to behave properly if move on completion is not enabled.
	     *
	     * Complete downloads apply to this bit, just in case the "move on completion"
	     * section isn't active.
	     */
		source = new SourceSpecification();
		source.updateSettings(COMPLETION_DETAILS.source.getSettings());
		source.setBoolean("default dir", true);

		mi_1 = new MovementInformation(source, COMPLETION_DETAILS.target,
				COMPLETION_DETAILS.transfer, "Update completed download");
		
		// Now incomplete downloads. We have to define completely new settings for
		// it, since we've never defined it before.
		source = new SourceSpecification();
		source.setBoolean("default dir", true); // Must be in default directory to update.
		source.setBoolean("default subdir", SUBDIR_PARAM);
		source.setBoolean("incomplete dl", true);

		dest = new TargetSpecification();
		dest.setBoolean("enabled", true);
		dest.setString("target", "Default save path");

		trans = new TransferSpecification();
		trans.setBoolean("torrent", false);

        // Rest of the settings are the same.
		mi_2 = new MovementInformation(source, dest, trans, "Update incomplete download");
		UPDATE_FOR_MOVE_DETAILS = new MovementInformation[] {mi_1, mi_2};

    }
    
    private static MovementInformation
    getTagMovementInformation(
    	DownloadManager			dm,
    	MovementInformation		def_mi )
    {
    	List<Tag> dm_tags = TagManagerFactory.getTagManager().getTagsForTaggable( dm );
    	
    	if ( dm_tags == null || dm_tags.size() == 0 ){
    		
    		return( def_mi );
    	}
    	
    	List<Tag>	applicable_tags = new ArrayList<Tag>();
    	
    	for ( Tag tag: dm_tags ){
    		
    		if ( tag.getTagType().hasTagTypeFeature( TagFeature.TF_FILE_LOCATION )){
    	
    			TagFeatureFileLocation fl = (TagFeatureFileLocation)tag;
    			
    			if ( fl.supportsTagMoveOnComplete()){
    				
	    			File move_to = fl.getTagMoveOnCompleteFolder();
	    			
	    			if ( move_to != null ){
	    				
	    				if ( !move_to.exists()){
	    					
	    					move_to.mkdirs();
	    				}
	    				
	    				if ( move_to.isDirectory() && move_to.canWrite()){
	    			
	    					applicable_tags.add( tag );
	    					
	    				}else{
	    				
	    					logInfo( "Ignoring invalid tag move-to location: " + move_to, dm );
	    				}
	    			}
    			}
    		}
    	}
    	
    	if ( applicable_tags.size() == 0 ){
    		
    		return( def_mi );
    		
    	}else if ( applicable_tags.size() > 1 ){
    		
    		Collections.sort(
    			applicable_tags,
    			new Comparator<Tag>()
    			{
    				public int 
    				compare(
    					Tag o1, 
    					Tag o2) 
    				{
    					return( o1.getTagID() - o2.getTagID());
    				}
    			});
    		
    		String str = "";
    		
    		for ( Tag tag: applicable_tags ){
    			
    			str += (str.length()==0?"":", ") + tag.getTagName( true );
    		}
    		
    		logInfo( "Multiple applicable tags found: " + str + " - selecting first", dm );
    	}
    	
    	Tag tag_target = applicable_tags.get(0);
    	
		TagFeatureFileLocation fl = (TagFeatureFileLocation)tag_target;
			
		File move_to = fl.getTagMoveOnCompleteFolder();

		if ( move_to != null ){
			
	    	SourceSpecification source = new SourceSpecification();
			source.setBoolean( "default dir", "Move Only When In Default Save Dir" );
			source.setBoolean( "default subdir", SUBDIR_PARAM );
			source.setBoolean( "incomplete dl", false );
				
			TargetSpecification dest = new TargetSpecification();
			dest.setBoolean( "enabled", true );
			dest.setString( "target_raw", move_to.getAbsolutePath());
			dest.setContext( "Tag '" + tag_target.getTagName( true ) + "' move-on-complete directory" );
	
			TransferSpecification trans = new TransferSpecification();
			trans.setBoolean("torrent", "Move Torrent When Done");
	
			MovementInformation tag_mi = new MovementInformation(source, dest, trans, "Tag Move on Completion");
			
	    	return( tag_mi );
		}
		
		return( def_mi );
    }
    
    private static interface ContextDescriptor {
    	public String getContext(); 
    }
    
    private static String normaliseRelativePathPart(String name) {
    	name = name.trim();
    	if (name.length() == 0) {return "";}
    	if (name.equals(".") || name.equals("..")) {
    		return null;
    	}
    	return FileUtil.convertOSSpecificChars(name, false).trim();
    }
    
    public static File normaliseRelativePath(File path) {
    	if (path.isAbsolute()) {return null;}
    	
    	File parent = path.getParentFile();
    	String child_name = normaliseRelativePathPart(path.getName());
    	if (child_name == null) {
    		return null;
    	}
    	
    	//  Simple one-level path.
    	if (parent == null) {
    		return new File(child_name);
    	}
    	
    	ArrayList parts = new ArrayList();
    	parts.add(child_name);
    	
    	String filepart = null;
    	while (parent != null) {
    		filepart = normaliseRelativePathPart(parent.getName());
    		if (filepart == null) {return null;}
    		else if (filepart.length()==0) {/* continue */}
    		else {parts.add(0, filepart);} 
    		parent = parent.getParentFile();
    	}
    	
    	StringBuffer sb = new StringBuffer((String)parts.get(0));
    	for (int i=1; i<parts.size(); i++) {
    		sb.append(File.separatorChar);
    		sb.append(parts.get(i));
    	}
    	
    	return new File(sb.toString());
    }
    	
    private static File[] getDefaultDirs() {
		List results = new ArrayList();
		File location = null;
		TargetSpecification ts = null;
		for (int i=0; i<DEFAULT_DIRS.length; i++) {
			ts = DEFAULT_DIRS[i];
			location = ts.getTarget(null, ts);
			if (location != null) {
				results.add(location);
			}
		}
		return (File[])results.toArray(new File[results.size()]);
	}



    /**
     * This does the guts of determining appropriate file paths.
     */
    private static SaveLocationChange determinePaths(DownloadManager dm, MovementInformation mi, boolean check_source, boolean is_test) {
		boolean proceed = !check_source || mi.source.matchesDownload(dm, mi, is_test );
		if (!proceed) {
			logInfo("Cannot consider " + describe(dm, mi) +
			    " - does not match source criteria.", dm);
			return null;
		}

		File target_path = mi.target.getTarget(dm, mi);
		if (target_path == null) {
			logInfo("Unable to determine an appropriate target for " +
			    describe(dm, mi) + ".", dm);
			return null;
		}

        logInfo("Determined path for " + describe(dm, mi) + ".", dm);
		return mi.transfer.getTransferDetails(dm, mi, target_path);
	}

	static boolean isInDefaultDownloadDir(DownloadManager dm) {
		// We don't create this object properly, but just enough to get it
		// to be usable.
		SourceSpecification source = new SourceSpecification();
		source.setBoolean("default subdir", SUBDIR_PARAM);
		return source.checkDefaultDir(dm.getSaveLocation().getParentFile(), getDefaultDirs());
	}
    
    private static class MovementInformation implements ContextDescriptor {
        final SourceSpecification source;
        final TargetSpecification target;
        final TransferSpecification transfer;
        final String title;

        MovementInformation(SourceSpecification source, TargetSpecification target,
            TransferSpecification transfer, String title) {
            this.source = source;
            this.target = target;
            this.transfer = transfer;
            this.title = title;
        }
        
        public String getContext() {return title;}
    }

    private abstract static class ParameterHelper implements ContextDescriptor {
		private Map settings = new HashMap();
		private String context = null;

		protected boolean getBoolean(String key) {
			Object result = this.settings.get(key);
			if (result == null) {throw new RuntimeException("bad key: " + key);}
			if (result instanceof Boolean) {return ((Boolean)result).booleanValue();}
            return COConfigurationManager.getBooleanParameter((String)result);
        }

        protected void setBoolean(String key, boolean value) {
        	settings.put(key, Boolean.valueOf(value));
        }

        protected void setBoolean(String key, String param) {
        	settings.put(key, param);
        }

        protected void setString(String key, String param) {
        	settings.put(key, param);
        }
        
        protected String getStringRaw(String key) {
			return((String)this.settings.get(key));
        }
        
        protected String getString(String key) {
			String result = (String)this.settings.get(key);
			if (result == null) {throw new RuntimeException("bad key: " + key);}
			
			// This try-catch should be removed, it's only here for debugging purposes.
        	return COConfigurationManager.getStringParameter(result);
        }
        
        public Map getSettings() {return this.settings;}
        public void updateSettings(Map settings) {this.settings.putAll(settings);}
        
        public String getContext() {return this.context;}
        public void setContext(String context) {this.context = context;}
    }

    private static class SourceSpecification extends ParameterHelper {

		public boolean matchesDownload(DownloadManager dm, ContextDescriptor context, boolean ignore_completeness ) {
			if (this.getBoolean("default dir")) {
				logInfo("Checking if " + describe(dm, context) + " is inside default dirs.", dm);
				File[] default_dirs = getDefaultDirs();
				File current_location = dm.getSaveLocation().getParentFile();
				
				/**
                 * Very very rare, but I have seen this on fscked up downloads which don't appear
                 * to have a blank / malformed download path.
				 */ 
				if (current_location == null) {
					logWarn(describe(dm, context) + " appears to have a malformed save directory, skipping.", dm);
					return false;
				}
				
				if (!this.checkDefaultDir(current_location, default_dirs)) {
					logWarn(describe(dm, context) +
					    " doesn't exist in any of the following default directories" +
					    " (current dir: " + current_location + ", subdirectories checked: " +
					    this.getBoolean("default subdir") + ") - " + Arrays.asList(default_dirs), dm);
					return false;
				}
				logInfo(describe(dm, context) + " does exist inside default dirs.", dm);
			}

			// Does it work for incomplete downloads?
  			if (!dm.isDownloadComplete(false)) {
  				boolean can_move = ignore_completeness || this.getBoolean("incomplete dl");
  				String log_message = describe(dm, context) + " is incomplete which is " +
  			    	((can_move) ? "" : "not ") + "an appropriate state.";
  				if (!can_move) {
  					logInfo(log_message, dm);
  					return false;
  				}
  			}
  			
			return true;
		}
		
		public boolean checkDefaultDir(File location, File[] default_dirs) {
			location = FileUtil.canonise(location);
			boolean subdir = this.getBoolean("default subdir");
			for (int i=0; i<default_dirs.length; i++) {
				if (subdir) {
					if (FileUtil.isAncestorOf(default_dirs[i], location)) {return true;}
				}
				else {
					if (default_dirs[i].equals(location)) {return true;}
				}
			}
			return false;
		}
		
    }
			
	private static class TargetSpecification extends ParameterHelper {

		public File getTarget(DownloadManager dm, ContextDescriptor cd) {
			//logInfo("Calculating target location for " + describe(dm, cd), lr);
			if (!this.getBoolean("enabled")) {
				logInfo("Target for " + describe(dm, cd) + " is not enabled.", dm);
				return null;
			}
			String location = getStringRaw( "target_raw" );
			if ( location == null ){
				location = this.getString("target").trim();
			}
		    if (location.length() == 0) {
				logInfo("No explicit target for " + describe(dm, cd) + ".", dm);
				return null;
			}
		    
		    File target = new File(FileUtil.getCanonicalFileName(location));
		    String relative_path = null;
          
          if( dm != null && dm.getDownloadState() != null ) {
             relative_path = dm.getDownloadState().getRelativeSavePath();
          }
		    
          if (relative_path != null && relative_path.length() > 0) {
		    	logInfo("Consider relative save path: " + relative_path, dm);
		    	
		    	// Doesn't matter if File.separator is required or not, it seems to
		    	// remove duplicate file separators.
		    	target = new File(target.getPath() + File.separator + relative_path);
		    }
			return target;
		}

	}

	private static class TransferSpecification extends ParameterHelper {

		public SaveLocationChange getTransferDetails(DownloadManager dm, 
				ContextDescriptor cd, File target_path) {
			
			if (target_path == null) {throw new NullPointerException();}

			SaveLocationChange result = new SaveLocationChange();
			result.download_location = target_path;
			if (this.getBoolean("torrent")) {
				result.torrent_location = target_path;
			}
			return result;
		}

	}

	public static File getCompletionDirectory(DownloadManager dm) {
		return COMPLETION_DETAILS.target.getTarget(dm, null);
	}
	
	static String describe(DownloadManager dm, ContextDescriptor cs) {
		if (cs == null) {return describe(dm);}
		if (dm == null) {
			return "\"" + cs.getContext() + "\"";
		}
		return "\"" + dm.getDisplayName() + "\" with regard to \"" + cs.getContext() + "\"";
	}

}