/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */


package com.biglybt.core.disk.impl;

import com.biglybt.core.CoreOperation;
import com.biglybt.core.CoreOperationTask;
import com.biglybt.core.config.COConfigurationManager;
import com.biglybt.core.config.ConfigKeys;
import com.biglybt.core.config.ParameterListener;
import com.biglybt.core.disk.*;
import com.biglybt.core.disk.impl.piecemapper.*;
import com.biglybt.core.disk.impl.resume.RDResumeHandler;
import com.biglybt.core.diskmanager.access.DiskAccessController;
import com.biglybt.core.diskmanager.cache.CacheFile;
import com.biglybt.core.diskmanager.cache.CacheFileManagerFactory;
import com.biglybt.core.diskmanager.cache.CacheFileOwner;
import com.biglybt.core.diskmanager.file.FMFileManagerException;
import com.biglybt.core.download.*;
import com.biglybt.core.download.impl.DownloadManagerStatsImpl;
import com.biglybt.core.internat.LocaleTorrentUtil;
import com.biglybt.core.internat.LocaleUtilDecoder;
import com.biglybt.core.logging.*;
import com.biglybt.core.peermanager.piecepicker.util.BitFlags;
import com.biglybt.core.torrent.TOTorrent;
import com.biglybt.core.torrent.TOTorrentException;
import com.biglybt.core.torrent.TOTorrentFile;
import com.biglybt.core.util.*;
import com.biglybt.core.util.FileUtil.ProgressListener;
import com.biglybt.core.util.StringInterner.FileKey;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.util.*;

public class
DiskManagerUtil
{
    private static final LogIDs LOGID = LogIDs.DISK;

	protected static int        max_read_block_size;

	protected static final Object	skip_lock = new Object();
	
	static{

		ParameterListener param_listener = new ParameterListener() {
			@Override
			public void
			parameterChanged(
					String  str )
			{
				max_read_block_size = COConfigurationManager.getIntParameter( "BT Request Max Block Size" );
			}
		};

		COConfigurationManager.addAndFireParameterListener( "BT Request Max Block Size", param_listener);
	}

	public static boolean
	checkBlockConsistencyForHint(
		DiskManager	dm,
		String		originator,
		int 		pieceNumber,
		int 		offset,
		int 		length)
	{
		if (length <= 0 ) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " length=" + length + " <= 0"));
			return false;
		}
		if (pieceNumber < 0) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " pieceNumber=" + pieceNumber + " < 0"));
			return false;
		}
		if (pieceNumber >= dm.getNbPieces()) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " pieceNumber=" + pieceNumber + " >= this.nbPieces="
						+ dm.getNbPieces()));
			return false;
		}
		int pLength = dm.getPieceLength(pieceNumber);

		if (offset < 0) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " offset=" + offset + " < 0"));
			return false;
		}
		if (offset > pLength) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " offset=" + offset + " > pLength=" + pLength));
			return false;
		}
		if (offset + length > pLength) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Hint invalid: " + originator + " offset=" + offset + " + length=" + length
						+ " > pLength=" + pLength));
			return false;
		}

		return true;
	}

	public static boolean
	checkBlockConsistencyForRead(
		DiskManager	dm,
		String		originator,
		boolean		peer_request,
		int 		pieceNumber,
		int 		offset,
		int 		length)
	{
		if ( !checkBlockConsistencyForHint( dm, originator, pieceNumber, offset, length )){

			return( false );
		}

		if (length > max_read_block_size && peer_request) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
						"Read invalid: " + originator + " length=" + length + " > " + max_read_block_size));
			return false;
		}

		if(!dm.getPiece(pieceNumber).isDone()) {
			Logger.log(new LogEvent(dm, LOGID, LogEvent.LT_ERROR,
					"Read invalid: " + originator + " piece #" + pieceNumber + " not done"));
			return false;
		}

		return true;
	}

	public static void
	doFileExistenceChecksAfterSkipChange(
		DiskManagerFileInfoSet 	fileSet,
		boolean[] 				toCheck,
		boolean					isSkipped,
		DownloadManager 		dm )
	{
			// we can end up here during construction, the file situation should be correct so no point
			// in running the tests
		
		if ( !dm.isConstructed()){
			
			return;
		}
		
		DiskManagerFileInfo[] files = fileSet.getFiles();

		boolean[]	interesting = new boolean[files.length];
		
		for (int i = 0; i<files.length;i++ ){
			
			DiskManagerFileInfo file = files[i];
			
			if ( toCheck[i] ){
			
				interesting[i] = true;
				
				int firstPiece 	= file.getFirstPieceNumber();

				for ( int j=i-1; j>=0; j-- ){
					
					if ( files[j].getLastPieceNumber() == firstPiece ){
				
						interesting[j] = true;
						
					}else{
						
						break;
					}
				}
				
				int lastPiece 	= file.getLastPieceNumber();

				for ( int j=i+1; j<files.length; j++ ){
					
					if ( files[j].getFirstPieceNumber() == lastPiece ){
				
						interesting[j] = true;
						
					}else{
						
						break;
					}
				}
			}
		}
		
		List<DiskManagerFileInfo>	reallocs = new ArrayList<>();
		
		String[] types = DiskManagerImpl.getStorageTypes(dm);

		for ( int i=0; i<interesting.length; i++ ){
			
			if ( interesting[i] ){

				DiskManagerFileInfo file = files[i];
				
				if ( RDResumeHandler.fileMustExist(dm, fileSet, file )){
					
					if ( !isSkipped ){
						
						if ( !file.exists()){
							
							reallocs.add( file );
						}
					}
				}else{
					
					if ( isSkipped ){
						
						int	st = convertDMStorageTypeFromString( types[i] );
						
						if ( st == DiskManagerFileInfo.ST_COMPACT || st == DiskManagerFileInfo.ST_REORDER_COMPACT ){
							
							File currentFile = file.getFile(true);
							
								// these are compact files and can't be recovered as they just contain
								// start/end piece data
							
							if ( FileUtil.exists( currentFile )){
								
								FileUtil.log( "Deleting \"" + currentFile.getAbsolutePath() + "\" as no longer required", true );

								boolean no_recycle = true;
								
								FileUtil.deleteWithRecycle( 
										currentFile, 
										dm.getDownloadState().getFlag( DownloadManagerState.FLAG_LOW_NOISE ) || no_recycle );
							}
						}
					}
				}
			}
		}
		
		if ( !reallocs.isEmpty()){
			
			dm.requestAllocation( reallocs );
		}
	}

	static final AEMonitor    cache_read_mon  = new AEMonitor( "DiskManager:cacheRead" );

	static String
	setFileLink(
	    DownloadManager         	download_manager,
	    DiskManagerFileInfo[]   	info,
	    DiskManagerFileInfo    		file_info,
	    File                   		from_file,
	    File                   		to_link,
	    boolean						dont_delete_existing,
	    FileUtil.ProgressListener	pl )
	{
	    if ( to_link != null ){

		    File    existing_file = file_info.getFile( true );

		    if ( to_link.equals( existing_file )){
		    	
		    	if ( !FileUtil.areFilePathsIdentical( to_link, existing_file )){
	        		
		    		if ( FileUtil.exists( existing_file )){
		    			
			            if ( !FileUtil.renameFile( existing_file, to_link )){
	
			            	String error = "Failed to rename '" + existing_file.toString() + "' to '" + to_link.toString() + "'";
			            			
			                Logger.log(new LogAlert(download_manager, LogAlert.REPEATABLE, LogAlert.AT_ERROR, error ));
	
			                return( error );
			            }
		    		}
		    	}
		    }else{

			    for (int i=0;i<info.length;i++){
	
			        if ( to_link.equals( info[i].getFile( true ))){
	
			        	String error = "Attempt to link to existing file '" + info[i].getFile(true) + "'";
			        	
			            Logger.log(new LogAlert(download_manager, LogAlert.REPEATABLE, LogAlert.AT_ERROR, error ));
	
			            return( error );
			        }
			    }
	
			    if ( FileUtil.exists( to_link )){
	
			        if ( !FileUtil.exists( existing_file )){
	
			                // using a new file, make sure we recheck
	
			            download_manager.recheckFile( file_info );
	
			        }else{
	
				        		// On OSX (at least) a rename of a file here all that has changed is the capitalisation results in us getting here
				        		// as the 'to_link.exists()' returns true for the file even though case differs
				        	
				        	if ( 	to_link.getParent().equals( existing_file.getParent()) &&
				        			to_link.getName().equalsIgnoreCase( existing_file.getName())){
				        		
					            if ( !FileUtil.renameFile( existing_file, to_link )){
		
					            	String error = "Failed to rename '" + existing_file.toString() + "' to '" + to_link.toString() + "'";
					            	
					                Logger.log(new LogAlert(download_manager, LogAlert.REPEATABLE, LogAlert.AT_ERROR, error ));
		
					                return( error );
					            }
				        	}else{
				        				
					        		// user is manually re-targetting a file - this hack is to stop us from deleting the old file which
					        		// we really have no right to go and delete
			
					        	if ( dont_delete_existing ){
			
					        		 download_manager.recheckFile( file_info );
			
					        	}else{
			
					        		int st = file_info.getStorageType();

										// don't bother recycling if compact as just contain start/end piece data
									
									boolean no_recycle = st == DiskManagerFileInfo.ST_COMPACT || st == DiskManagerFileInfo.ST_REORDER_COMPACT;
									
									FileUtil.log( "Deleting \"" + existing_file.getAbsolutePath() + "\" as no longer required", true );
									
					                if ( FileUtil.deleteWithRecycle(
					                		existing_file,
					                		download_manager.getDownloadState().getFlag( DownloadManagerState.FLAG_LOW_NOISE ) || no_recycle )){
			
						                    // new file, recheck
			
						                download_manager.recheckFile( file_info );
			
						            }else{
			
						            	String error = "Failed to delete '" + existing_file.toString() + "'";
						            	
						                Logger.log(new LogAlert(download_manager, LogAlert.REPEATABLE, LogAlert.AT_ERROR, error ));
			
						                return( error );
						            }
					        	}
				        }
			        }
			    }else{
	
			        if ( FileUtil.exists( existing_file )){
	
			            if ( !FileUtil.renameFile( existing_file, to_link, pl )){
	
			            	String error = "Failed to rename '" + existing_file.toString() + "' to '" + to_link.toString() + "'";
			            	
			                Logger.log(new LogAlert(download_manager, LogAlert.REPEATABLE, LogAlert.AT_ERROR, error ));
	
			                return( error );
			            }
			        }
			    }
		    }
	    }
	    
	    DownloadManagerState    state = download_manager.getDownloadState();

	    state.setFileLink( file_info.getIndex(), from_file, to_link );

	    return( null );
	}

	static abstract class FileSkeleton implements DiskManagerFileInfoHelper {
	    protected int     priority;
	    protected boolean skipped_internal;
		protected long    downloaded;
		protected String  last_error;
		
		protected void
		load(
			int		_priority,
			boolean	_skipped )
		{
			priority				= _priority;
			skipped_internal		= _skipped;
		}
		
		protected abstract File
		setSkippedInternal( boolean skipped );
	}


	public static DiskManagerFileInfoSet
	getFileInfoSkeleton(
	    final DownloadManager       download_manager,
	    final DiskManagerListener   listener )
	{
	    final TOTorrent   torrent = download_manager.getTorrent();

	    if ( torrent == null ){

	        return( new DiskManagerFileInfoSetImpl(new DiskManagerFileInfoImpl[0],null) );
	    }

		File tempRootDir = download_manager.getAbsoluteSaveLocation().getParentFile();

		if (tempRootDir == null) { // in case we already are at the root
			tempRootDir = download_manager.getAbsoluteSaveLocation();
		}

		if ( !torrent.isSimpleTorrent()){
			tempRootDir = FileUtil.newFile(tempRootDir, download_manager.getAbsoluteSaveLocation().getName());
		}

	    	// prevent attempted state saves and associated nastyness during population of
	    	// the file skeleton entries

	    final boolean[] loading = { true };

	    try{
		    final File root_dir = tempRootDir;

		    try{
		        final LocaleUtilDecoder locale_decoder = LocaleTorrentUtil.getTorrentEncoding( torrent );

		        TOTorrentFile[] torrent_files = torrent.getFiles();

		        final FileSkeleton[]   res = new FileSkeleton[ torrent_files.length ];

				final String incomplete_suffix = download_manager.getDownloadState().getAttribute( DownloadManagerState.AT_INCOMP_FILE_SUFFIX );

		        final DiskManagerFileInfoSet fileSetSkeleton = new DiskManagerFileInfoSet() {

		        	@Override
		        	public void 
		        	load(
		        		int[] priorities, boolean[] skipped)
		        	{
		        		for ( int i=0;i<priorities.length;i++){
		        			
		        			res[i].load(priorities[i],skipped[i]);
		        		}
		        	}
		        	
					@Override
					public DiskManagerFileInfo[] getFiles() {
						return res;
					}

					private DiskManager
					getDiskManager()
					{
						return( null );
					}
					
					@Override
					public int nbFiles() {
						return res.length;
					}

					@Override
					public void 
					setPriority(
						int[] newPriorities ) 
					{
						if ( newPriorities.length != res.length){
							
							throw new IllegalArgumentException("array length mismatches the number of files");
						}
						
						List<DiskManagerFileInfo> priorityChanged = new ArrayList<>( res.length );
						
						for ( int i=0; i<res.length; i++){
							
							FileSkeleton file = res[i];
							
							int np = newPriorities[i];
							
							if ( np != Integer.MIN_VALUE ){
								
								if ( file.priority != np ){
								
									file.priority = np;
																									
									priorityChanged.add( file );
								}
							}
						}
						if ( !loading[0] ){

							DiskManagerImpl.storeFilePriorities( download_manager, res );
						}

						if ( !priorityChanged.isEmpty()){
							
							listener.filePriorityChanged(getDiskManager(),priorityChanged);
						}
					}

					@Override
					public void 
					setSkipped(
							boolean[] toChange, boolean setSkipped) 
					{
						synchronized( DiskManagerUtil.skip_lock ){
							if ( toChange.length != res.length ){
								
								throw new IllegalArgumentException("array length mismatches the number of files");
							}
							
			        		if ( !setSkipped ){
			        			
			    				String[] types = DiskManagerImpl.getStorageTypes(download_manager);
	
			    				boolean[]	toLinear 	= new boolean[toChange.length];
			    				boolean[]	toReorder 	= new boolean[toChange.length];
	
			    				int	num_linear 	= 0;
			    				int num_reorder	= 0;
	
			    				for ( int i=0;i<toChange.length;i++){
	
			    					if ( toChange[i] ){
	
			    						int old_type = DiskManagerUtil.convertDMStorageTypeFromString( types[i] );
	
			    						if ( old_type == DiskManagerFileInfo.ST_COMPACT ){
	
			    							toLinear[i] = true;
	
			    							num_linear++;
	
			    						}else if ( old_type == DiskManagerFileInfo.ST_REORDER_COMPACT ){
	
			    							toReorder[i] = true;
	
			    							num_reorder++;
			    						}
			    					}
			    				}
	
			    				if ( num_linear > 0 ){
	
			    					if (!Arrays.equals(toLinear, setStorageTypes(toLinear, DiskManagerFileInfo.ST_LINEAR))){
	
			    						return;
			    					}
			    				}
	
			    				if ( num_reorder > 0 ){
	
			    					if (!Arrays.equals(toReorder, setStorageTypes(toReorder, DiskManagerFileInfo.ST_REORDER ))){
	
			    						return;
			    					}
			    				}
			        		}
	
			        		File[]	to_link = new File[res.length];
	
							for(int i=0;i<res.length;i++){
								if(toChange[i]){
									to_link[i] = res[i].setSkippedInternal( setSkipped );
								}
							}
	
							if ( !loading[0] ){
	
								DiskManagerImpl.storeFilePriorities( download_manager, res);
							}
	
							List<Integer>	from_indexes 	= new ArrayList<>();
							List<File>		from_links 		= new ArrayList<>();
							List<File>		to_links		= new ArrayList<>();
	
							for(int i=0;i<res.length;i++){
								if ( to_link[i] != null ){
									from_indexes.add( i );
									from_links.add( res[i].getFile( false ));
									to_links.add( to_link[i] );
								}
							}
	
							if ( from_links.size() > 0 ){
								download_manager.getDownloadState().setFileLinks( from_indexes, from_links, to_links );
							}
	
							List<DiskManagerFileInfo> priority_change = new ArrayList<>( res.length );
							
							for( int i=0;i<res.length;i++){
								
								if ( toChange[i] ){
									
									priority_change.add( res[i] );
								}
							}
	
							if ( !priority_change.isEmpty()){
							
								listener.filePriorityChanged(getDiskManager(), priority_change);
							}
							
							doFileExistenceChecksAfterSkipChange(this, toChange, setSkipped, download_manager );
						}
					}

					@Override
					public boolean[] setStorageTypes(boolean[] toChange, int newStorageType, boolean force) {
						if(toChange.length != res.length)
							throw new IllegalArgumentException("array length mismatches the number of files");

						String[] types = DiskManagerImpl.getStorageTypes(download_manager);
						boolean[] modified = new boolean[res.length];
						boolean[] toSkip = new boolean[res.length];
						int toSkipCount = 0;
						DownloadManagerState dmState = download_manager.getDownloadState();

						try {
							dmState.suppressStateSave(true);

							boolean	recalc_dl = false;
							
							for(int i=0;i<res.length;i++){
							
								if(!toChange[i])
									continue;


								final int idx = i;

								int old_type = DiskManagerUtil.convertDMStorageTypeFromString( types[i] );

								//System.out.println(old_type + " <> " + newStroageType);

								if ( newStorageType == old_type )
								{
									modified[i] = true;
									continue;
								}

								try{
									File    target_file = res[i].getFile( true );

									// if the file doesn't exist then this is the start-of-day, most likely
									// being called from the torrent-opener, so we don't need to do any
									// file fiddling (in fact, if we do, we end up leaving zero length
									// files for dnd files which then force a recheck when the download
									// starts for the first time)

									if ( FileUtil.exists( target_file )){

										CacheFile cache_file =
											CacheFileManagerFactory.getSingleton().createFile(
												new CacheFileOwner()
												{
													@Override
													public String
													getCacheFileOwnerName()
													{
														return( download_manager.getInternalName());
													}

													@Override
													public TOTorrentFile
													getCacheFileTorrentFile()
													{
														return( res[idx].getTorrentFile() );
													}

													@Override
													public File
													getCacheFileControlFileDir()
													{
														return( download_manager.getDownloadState().getStateDir());
													}
													@Override
													public int
													getCacheMode()
													{
														return( CacheFileOwner.CACHE_MODE_NORMAL );
													}
													
													@Override
													public FileKey 
													getCacheFileLink(
														FileKey file )
													{
														return( download_manager.getDownloadState().getFileLink( idx, file ));
													}
												},
												new StringInterner.FileKey( target_file ),
												DiskManagerUtil.convertDMStorageTypeToCache( newStorageType ), force );

										try{
											cache_file.getLength();	// need this to trigger recovery for re-order files :(

										}finally{
											
											cache_file.close();
										}
									}

									toSkip[i] = ( newStorageType == FileSkeleton.ST_COMPACT || newStorageType == FileSkeleton.ST_REORDER_COMPACT )&& !res[i].isSkipped();

									if ( toSkip[i] ){

										toSkipCount++;
									}

									modified[i] = true;

								}catch( Throwable e ){

									Debug.printStackTrace(e);

									Logger.log(
										new LogAlert(download_manager,
											LogAlert.REPEATABLE,
											LogAlert.AT_ERROR,
											"Failed to change storage type for '" + res[i].getFile(true) +"': " + Debug.getNestedExceptionMessage(e)));

									// download's not running - tag for recheck

									RDResumeHandler.recheckFile( download_manager, res[i] );

								}

								types[i] = DiskManagerUtil.convertDMStorageTypeToString( newStorageType );
							}

							/*
							 * set storage type and skipped before we do piece clearing and file
							 * clearing checks as those checks work better when skipped/stype is set
							 * properly
							 */
							dmState.setListAttribute( DownloadManagerState.AT_FILE_STORE_TYPES, types);

							if ( toSkipCount > 0){

								setSkipped( toSkip, true );
							}

							for(int i=0;i<res.length;i++){
								
								if(!toChange[i]){
								
									continue;
								}
								
								// download's not running, update resume data as necessary

								int cleared = RDResumeHandler.storageTypeChanged( download_manager, res[i] );

								// try and maintain reasonable figures for downloaded. Note that because
								// we don't screw with the first and last pieces of the file during
								// storage type changes we don't have the problem of dealing with
								// the last piece being smaller than torrent piece size

								if ( cleared > 0 ){
								
									res[i].downloaded = res[i].downloaded - cleared * res[i].getTorrentFile().getTorrent().getPieceLength();
									
									if (res[i].downloaded < 0) res[i].downloaded = 0;
									
									recalc_dl = true;
								}
							}
							
							if ( recalc_dl ){
								
								download_manager.getStats().setDownloadCompletedBytes( -1 );	// force recalc
							}

							DiskManagerImpl.storeFileDownloaded( download_manager, res, true, false );

						} finally {
							dmState.suppressStateSave(false);
							dmState.save(false);
						}

							// hijack the priority change event to report storage change to detect DND->delete 'priority' change
						
						List<DiskManagerFileInfo> priorityChanged = new ArrayList<>( res.length );

						for(int i=0;i<res.length;i++){
							if(toChange[i]){
								priorityChanged.add( res[i] );
							}
						}
						
						if ( !priorityChanged.isEmpty()){
							listener.filePriorityChanged(getDiskManager(),priorityChanged);
						}
						
						return modified;
					}
		        };

		        for (int i=0;i<res.length;i++){

		            final TOTorrentFile torrent_file    = torrent_files[i];

		            final int file_index = i;

		            FileSkeleton info = new FileSkeleton() {

		            	private volatile CacheFile   read_cache_file;
		            	// do not access this field directly, use lazyGetFile() instead
		            	private WeakReference<StringInterner.FileKey> dataFile = new WeakReference<>(null);

		            	private volatile Boolean skipping;
		            	
		            	@Override
			            public void
		            	setPriority(int b)
		            	{
		            		priority    = b;

		            		DiskManagerImpl.storeFilePriorities( download_manager, res );

		            		listener.filePriorityChanged( getDiskManager(), this );
		            	}

		            	@Override
			            public void
		            	setSkipped(boolean skipped)
		            	{
		            		synchronized( DiskManagerUtil.skip_lock ){
			            		try{
			            			skipping = skipped;
				            		if ( !skipped && getStorageType() == ST_COMPACT ){
				            			if ( !setStorageType( ST_LINEAR )){
				            				return;
				            			}
				            		}
		
				            		if ( !skipped && getStorageType() == ST_REORDER_COMPACT ){
				            			if ( !setStorageType( ST_REORDER )){
				            				return;
				            			}
				            		}
		
				            		File to_link = setSkippedInternal( skipped );
		
				            		DiskManagerImpl.storeFilePriorities( download_manager, res );
		
				            		if ( to_link != null ){
		
				            			download_manager.getDownloadState().setFileLink( file_index, getFile( false ), to_link );
				            		}
		
				            		listener.filePriorityChanged( getDiskManager(), this );
		
			            			boolean[] toCheck = new boolean[fileSetSkeleton.nbFiles()];
				            			
			            			toCheck[file_index] = true;
				            			
			            			doFileExistenceChecksAfterSkipChange( fileSetSkeleton, toCheck, skipped, download_manager );
			            		}finally{
			            			skipping = null;
			            		}
		            		}
		            	}

		            	@Override
		            	public Boolean isSkipping(){
		            		return( skipping );
		            	}
		            	 
		            	@Override
			            public int
		            	getAccessMode()
		            	{
		            		return( READ );
		            	}

		            	@Override
			            public long
		            	getDownloaded()
		            	{
		            		return( downloaded );
		            	}

		            	@Override
		            	public long getLastModified(){
		            		return( getFile( true ).lastModified());
		            	}
		            	
		            	@Override
			            public void
		            	setDownloaded(
		            		long    l )
		            	{
		            		downloaded  = l;
		            	}

		            	@Override
			            public String
		            	getExtension()
		            	{
		            		String    ext   = lazyGetFile().getName();

		                    if ( incomplete_suffix != null && ext.endsWith( incomplete_suffix )){

		                    	ext = ext.substring( 0, ext.length() - incomplete_suffix.length());
		                    }

		            		int separator = ext.lastIndexOf(".");
		            		if (separator == -1)
		            			separator = 0;
		            		return ext.substring(separator);
		            	}

		            	@Override
			            public int
		            	getFirstPieceNumber()
		            	{
		            		return( torrent_file.getFirstPieceNumber());
		            	}

		            	@Override
			            public int
		            	getLastPieceNumber()
		            	{
		            		return( torrent_file.getLastPieceNumber());
		            	}

		            	@Override
			            public long
		            	getLength()
		            	{
		            		return( torrent_file.getLength());
		            	}

		            	@Override
			            public int
		            	getIndex()
		            	{
		            		return( file_index );
		            	}

		            	@Override
			            public int
		            	getNbPieces()
		            	{
		            		return( torrent_file.getNumberOfPieces());
		            	}

		            	@Override
			            public int
		            	getPriority()
		            	{
		            		return( priority );
		            	}

		            	@Override
			            protected File
		            	setSkippedInternal(
		            		boolean	_skipped )
		            	{
		            			// returns the file to link to if linkage is required

		            		synchronized( DiskManagerUtil.skip_lock ){
		            			
			            		skipped_internal = _skipped;
	
		    					if ( !download_manager.isDestroyed()){
	
		    						return( setSkippedInternalSupport( download_manager, this, null, _skipped ));
				            		
		    					}
	
		    					return( null );
		            		}
		            	}

		            	@Override
			            public boolean
		            	isSkipped()
		            	{
		            		return( skipped_internal );
		            	}

		            	@Override
			            public DiskManager
		            	getDiskManager()
		            	{
		            		return( null );
		            	}

		            	@Override
			            public DownloadManager
		            	getDownloadManager()
		            	{
		            		return( download_manager );
		            	}

		            	@Override
		            	public boolean exists(){
		            		if ( torrent_file.isPadFile()){
		            			return( true );
		            		}else{
		            			return( getFile( true ).exists());
		            		}
		            	}
		            	  
		            	@Override
			            public File
		            	getFile(
		            		boolean follow_link )
		            	{
		            		if ( follow_link ){

		            			File link = getLink();

		            			if ( link != null ){

		            				return( link );
		            			}
		            		}
		            		return lazyGetFile();
		            	}

		            	private File lazyGetFile()
		            	{
		            		StringInterner.FileKey fk = dataFile.get();
		            		
		            		if ( fk == null ){
		            					            		
			            		TOTorrent tor = download_manager.getTorrent();
	
			            		// for a simple torrent the target file can be changed
	
			            		if ( tor == null || tor.isSimpleTorrent()){
	
			            				// rumour has it tor can sometimes be null
	
			            			fk = new StringInterner.FileKey( download_manager.getAbsoluteSaveLocation());
	
			            		}else{
	
			            			fk = new StringInterner.FileKey( root_dir, torrent_file.getRelativePath( locale_decoder ));
			            		}
	
			            		dataFile = new WeakReference<>( fk );
		            		}
		            		
	            			return( fk.getFile());
		            	}

		            	@Override
			            public TOTorrentFile
		            	getTorrentFile()
		            	{
		            		return( torrent_file );
		            	}

	                	@Override
		                public boolean
	                	setLink(
	                		File    	link_destination,
	                		boolean		no_delete )
	                	{
	                		last_error = null;
	                		
	                		/**
	                		 * If we a simple torrent, then we'll redirect the call to the download and move the
	                		 * data files that way - that'll keep everything in sync.
	                		 */
	                		if (download_manager.getTorrent().isSimpleTorrent()) {
	                			try {
	                				download_manager.moveDataFiles(link_destination.getParentFile(), link_destination.getName());
	                				return true;
	                			}
	                			catch (DownloadManagerException e) {
	                				
	                				Debug.out( e );
	                				
	                				last_error = Debug.getNestedExceptionMessage( e );
	                				
	                				return false;
	                			}
	                		}
	                		return setLinkAtomic(link_destination, no_delete );
	                	}

	    				@Override
	    				public String getLastError(){
	    					return( last_error );
	    				}
	    				
	                	@Override
		                public boolean
	                	setLinkAtomic(
	                		File    	link_destination,
	                		boolean		no_delete)
	                	{
	                		last_error = setFileLink( download_manager, res, this, lazyGetFile(), link_destination, no_delete, null );
	                		
	                		return( last_error == null );
	                	}

	                	@Override
		                public boolean
	                	setLinkAtomic(
	                		File    						link_destination,
	                		boolean							no_delete,
	                		FileUtil.ProgressListener		pl )
	                	{
	                		last_error = setFileLink( download_manager, res, this, lazyGetFile(), link_destination, no_delete, pl);
	                		
	                		return( last_error == null );
	                	}
	                	
	                	@Override
		                public File
	                	getLink()
	                	{
	                		return( download_manager.getDownloadState().getFileLink( file_index));
	                	}

	                	@Override
		                public boolean setStorageType(int type, boolean force) {
	                		boolean[] change = new boolean[res.length];
	                		change[file_index] = true;
	                		return fileSetSkeleton.setStorageTypes(change, type, force)[file_index];
	                	}

	                	@Override
		                public int
	                	getStorageType()
	                	{
	                		return( DiskManagerUtil.convertDMStorageTypeFromString( DiskManagerImpl.getStorageType(download_manager, file_index)));
	                	}

	                	@Override
		                public void
	                	flushCache()
	                	{
	                	}

	                	@Override
		                public DirectByteBuffer
	                	read(
	                		long    offset,
	                		int     length )

	                		throws IOException
	                	{
	                		CacheFile temp;

			                try{
	                			cache_read_mon.enter();

	                			if ( read_cache_file == null ){

	                				try{
	                					int type = convertDMStorageTypeFromString( DiskManagerImpl.getStorageType(download_manager, file_index));

	                					read_cache_file =
	                						CacheFileManagerFactory.getSingleton().createFile(
	                							new CacheFileOwner()
	                							{
	                								@Override
									                public String
	                								getCacheFileOwnerName()
	                								{
	                									return( download_manager.getInternalName());
	                								}

	                								@Override
									                public TOTorrentFile
	                								getCacheFileTorrentFile()
	                								{
	                									return( torrent_file );
	                								}

	                								@Override
									                public File
	                								getCacheFileControlFileDir()
	                								{
	                									return( download_manager.getDownloadState().getStateDir());
	                								}
	                								@Override
									                public int
	                								getCacheMode()
	                								{
	                									return( CacheFileOwner.CACHE_MODE_NORMAL );
	                								}
	                								@Override
													public FileKey 
													getCacheFileLink(
														FileKey file )
													{
														return( download_manager.getDownloadState().getFileLink( file_index, file ));
													}
	                							},
	                							new StringInterner.FileKey( getFile( true )),
	                							convertDMStorageTypeToCache( type ), false );

	                				}catch( Throwable e ){

	                					Debug.printStackTrace(e);

	                					throw( new IOException( e.getMessage()));
	                				}
	                			}

	                			temp = read_cache_file;

	                		}finally{

	                			cache_read_mon.exit();
	                		}

	                		DirectByteBuffer    buffer =
	                			DirectByteBufferPool.getBuffer( DirectByteBuffer.AL_DM_READ, length );

	                		try{
	                			temp.read( buffer, offset, CacheFile.CP_READ_CACHE );

	                		}catch( Throwable e ){

	                			buffer.returnToPool();

	                			Debug.printStackTrace(e);

	                			throw( new IOException( e.getMessage()));
	                		}

	                		return( buffer );
	                	}

	                	@Override
		                public int
	                	getReadBytesPerSecond()
	                	{
	                		CacheFile temp = read_cache_file;

	                		if ( temp == null ){

	                			return( 0 );
	                		}

	                			// could do something here one day I guess

	                		return( 0 );
	                	}

	                	@Override
		                public int
	                	getWriteBytesPerSecond()
	                	{
	                		return( 0 );
	                	}

	                	@Override
		                public long
	                	getETA()
	                	{
	                		return( -1 );
	                	}

	                	@Override
	                	public void recheck()
	                	{
	                		DiskManagerFactory.recheckFile( getDownloadManager(), this );
	                	}
	                	
	                	@Override
		                public void
	                	close()
	                	{
	                		CacheFile temp;

	                   		try{
	                			cache_read_mon.enter();

	                			temp = read_cache_file;

	                			read_cache_file = null;

	                   		}finally{

	                   			cache_read_mon.exit();
	                   		}

	                		if ( temp != null ){

	                			try{
	                				temp.close();

	                			}catch( Throwable e ){

	                				Debug.printStackTrace(e);
	                			}
	                		}
	                	}

		            	@Override
			            public void
		            	addListener(
		            		DiskManagerFileInfoListener listener )
		            	{
		            		if ( getDownloaded() == getLength()){

		            			try{
		            				listener.dataWritten( 0, getLength(), null);

		            				listener.dataChecked( 0, getLength());

		            			}catch( Throwable e ){

		            				Debug.printStackTrace(e);
		            			}
		            		}
		            	}

		            	@Override
			            public void
		            	removeListener(
		            		DiskManagerFileInfoListener listener )
		            	{
		            	}
		            };

		            res[i]  = info;
		        }

		        loadFilePriorities( download_manager, fileSetSkeleton);

		        loadFileDownloaded( download_manager, res );

		        return( fileSetSkeleton );

		    }finally{

		    	loading[0] = false;
		    }

	    }catch( Throwable e ){

	        Debug.printStackTrace(e);

	        return( new DiskManagerFileInfoSetImpl(new DiskManagerFileInfoImpl[0],null) );

	    }
	}

	public static int
	convertDMStorageTypeFromString(
		String		str )
	{
		char c = str.charAt(0);

		switch( c ){
			case 'L':{
				return( DiskManagerFileInfo.ST_LINEAR );
			}
			case 'C':{
				return( DiskManagerFileInfo.ST_COMPACT );
			}
			case 'R':{
				return( DiskManagerFileInfo.ST_REORDER );
			}
			case 'X':{
				return( DiskManagerFileInfo.ST_REORDER_COMPACT );
			}
		}

		Debug.out( "eh?" );

		return( DiskManagerFileInfo.ST_LINEAR );
	}

	public static String
	convertDMStorageTypeToString(
		int		dm_type )
	{
		switch( dm_type ){
			case DiskManagerFileInfo.ST_LINEAR:{
				return( "L" );
			}
			case DiskManagerFileInfo.ST_COMPACT:{
				return( "C" );
			}
			case DiskManagerFileInfo.ST_REORDER:{
				return( "R" );
			}
			case DiskManagerFileInfo.ST_REORDER_COMPACT:{
				return( "X" );
			}
		}

		Debug.out( "eh?" );

		return( "?" );
	}

	public static String
	convertCacheStorageTypeToString(
		int		cache_type )
	{
		switch( cache_type ){
			case CacheFile.CT_LINEAR:{
				return( "L" );
			}
			case CacheFile.CT_COMPACT:{
				return( "C" );
			}
			case CacheFile.CT_PIECE_REORDER:{
				return( "R" );
			}
			case CacheFile.CT_PIECE_REORDER_COMPACT:{
				return( "X" );
			}
		}

		Debug.out( "eh?" );

		return( "?" );
	}
	public static int
	convertDMStorageTypeToCache(
		int	dm_type )
	{
		switch( dm_type ){
			case DiskManagerFileInfo.ST_LINEAR:{
				return( CacheFile.CT_LINEAR );
			}
			case DiskManagerFileInfo.ST_COMPACT:{
				return( CacheFile.CT_COMPACT );
			}
			case DiskManagerFileInfo.ST_REORDER:{
				return( CacheFile.CT_PIECE_REORDER );
			}
			case DiskManagerFileInfo.ST_REORDER_COMPACT:{
				return( CacheFile.CT_PIECE_REORDER_COMPACT );
			}
		}

		Debug.out( "eh?" );

		return( CacheFile.CT_LINEAR );
	}

	protected static void
	storeFilePriorities(
		DownloadManager         download_manager,
		DiskManagerFileInfo[]   files )
	{
		if ( files == null ) return;
		// bit confusing this: priorities are stored as
		// >= 1   : priority, 1 = high, 2 = higher etc
		// 0      : skipped
		// -1     : normal priority
		// <= -2  : negative priority , so -2 -> priority -1, -3 -> priority -2 etc

		List file_priorities = new ArrayList(files.length);
		boolean all_normal = true;
		for (int i=0; i < files.length; i++) {
			DiskManagerFileInfo file = files[i];
			if (file == null) return;
			boolean skipped = file.isSkipped();
			int priority = file.getPriority();
			int value;
			if ( skipped ){
				value = 0;
			}else if ( priority > 0 ){
				value = priority;
			}else{
				value = priority - 1;
				if ( value > 0 ){
					value = Integer.MIN_VALUE;
				}
			}
			if ( all_normal && value != -1 ){
				all_normal = false;
			}
			file_priorities.add( i, Long.valueOf(value));
		}

	   download_manager.setUserData( "file_priorities", all_normal?null:file_priorities );

	   		// for whatever reason we don't set the skipped file stats here if we have a REAL set of
	   		// file info (maybe because the code tries to maintain its own active version of these values)

	   if (files.length > 0 && !(files[0] instanceof DiskManagerFileInfoImpl)) {

	       long skipped_file_set_size   = 0;
	       long skipped_but_downloaded  = 0;

	       for (int i=0;i<files.length;i++){

	           DiskManagerFileInfo file = files[i];

	           if ( file.isSkipped()){

	               skipped_file_set_size   += file.getLength();
	               skipped_but_downloaded  += file.getDownloaded();
	           }
	       }

	       DownloadManagerStats stats = download_manager.getStats();
	       if (stats instanceof DownloadManagerStatsImpl) {
	       	((DownloadManagerStatsImpl) stats).setSkippedFileStats(skipped_file_set_size, skipped_but_downloaded);
	       }
	   }
	}

	static void
	loadFilePriorities(
		DownloadManager         	download_manager,
		DiskManagerFileInfoSet   	fileSet )
	{
		//  TODO: remove this try/catch.  should only be needed for those upgrading from previous snapshot
		try {
			DiskManagerFileInfo[] files = fileSet.getFiles();

			if ( files == null ) return;
			List file_priorities = (List)download_manager.getUserData( "file_priorities" );
			if ( file_priorities == null ) return;

			boolean[] toSkip = new boolean[files.length];
			int[] prio = new int[files.length];

			boolean all_normal = true;
			for (int i=0; i < files.length; i++) {
				DiskManagerFileInfo file = files[i];
				if (file == null) return;
				try {
  				int priority = ((Long)file_priorities.get( i )).intValue();
  				if ( all_normal && priority != -1 ){
  					all_normal = false;
  				}
  				if ( priority == 0 ){
  					toSkip[i] = true;
  				}else{
  					if ( priority < 0 ){
  						priority++;
  					}
  					prio[i] = priority;
  				}
				} catch (Throwable t2) {
					Debug.printStackTrace(t2);
				}
			}

			if ( all_normal ){
					// TODO: remove this logic sometime after 4.0 as only for migration
				download_manager.setUserData( "file_priorities", null );
				
			}else{
				
				fileSet.load( prio, toSkip );
			}
		}
		
		catch (Throwable t) {Debug.printStackTrace( t );}
	}

	protected static void
	  loadFileDownloaded(
	    DownloadManager             download_manager,
	    DiskManagerFileInfoHelper[] files )
	  {
	      DownloadManagerState  state = download_manager.getDownloadState();

	      Map   details = state.getMapAttribute( DownloadManagerState.AT_FILE_DOWNLOADED );

	      if ( details == null ){

	          return;
	      }

	      List  downloaded = (List)details.get( "downloaded" );

	      if ( downloaded == null ){

	          return;
	      }

	      try{
	    	  	// somehow it is possible to end up with a mismatch in files/downloaded size - handle best as possible
	    	  
	          for (int i=0, max=Math.min( files.length, downloaded.size());i<max;i++){

	              files[i].setDownloaded(((Long)downloaded.get(i)).longValue());
	          }
	     }catch( Throwable e ){

	         Debug.printStackTrace(e);
	     }

	  }

	public static boolean
	isNoSpaceException(
		Throwable e )
	{
    	String exception_str  = Debug.getNestedExceptionMessage(e);
    		
		String lc = exception_str.toLowerCase( Locale.US );
		
			// not enough space; no space left; insufficient space
		
		if ( lc.contains( " space")){ 	
	
			return( true );
		}
		
		return( false );
	}
	
	public static boolean
	isFileWriteException(
		Throwable e )
	{
		while( e != null ){
			
			if ( e instanceof FMFileManagerException ){
				
				if (((FMFileManagerException)e).getOperation() == FMFileManagerException.OP_WRITE ){
					
					return( true );
				}
			}
			
			e = e.getCause();
		}
		
		return( false );
	}
	
	public static DiskManagerPiece[]
	getDiskManagerPiecesSnapshot(
		DownloadManager				dm,
		DiskManagerFileInfoSet		file_set )
	{
		try{
			TOTorrent torrent = dm.getTorrent();
			
			if ( torrent == null ){
				
				return( new DiskManagerPiece[0] );
			}
			
	        DMPieceMapper piece_mapper    = DMPieceMapperFactory.create( torrent );

	        int nbPieces    = torrent.getNumberOfPieces();
	    	
	        int pieceLength     = (int)torrent.getPieceLength();
	        int lastPieceLength = piece_mapper.getLastPieceLength();
	
	        DiskManagerPiece[] pieces      = new DiskManagerPiece[nbPieces];

	        /*
			DiskManagerFileInfoSet file_set = 
				getFileInfoSkeleton(
					dm,
					new DiskManagerListener(){
						
						@Override
						public void stateChanged(DiskManager dm, int oldState, int newState){
						}
						
						@Override
						public void pieceDoneChanged(DiskManager dm, DiskManagerPiece piece){
						}
						
						@Override
						public void filePriorityChanged(DiskManager dm, List<DiskManagerFileInfo> files){
						}
					});
			*/
	        
			DiskManagerFileInfo[] files = file_set.getFiles();
	
	        LocaleUtilDecoder   locale_decoder = LocaleTorrentUtil.getTorrentEncoding( torrent );
	
	        piece_mapper.construct( locale_decoder, dm.getAbsoluteSaveLocation().getName());
	
	        DMPieceMapperFile[] pm_files = piece_mapper.getFiles();
	        
	        for ( int i=0; i<pm_files.length; i++ ){
	        	
	        	pm_files[i].setFileInfo( files[i] );
	        }
	        
	        DMPieceMap piece_map = piece_mapper.getPieceMap();       
	        	        	        
	
	        DiskManagerHelper disk_manager =
	        	new DiskManagerHelper(){
					
	        		@Override
		        	public String getDisplayName(){
		        		return( "" );
		        	}
	        		
	        		@Override
	        		public DownloadManager
	        		getDownload()
	        		{
	        			return( dm );
	        		}
	        		
					@Override
					public boolean stop(boolean closing){
						return false;
					}
					
					@Override
					public void start(){
					}
					
					@Override
					public void setPieceCheckingEnabled(boolean enabled){
					}
					
					@Override
					public void setMoveState(int state){
					}
					
					@Override
					public void saveState( boolean interim ){
					}
					
					@Override
					public void saveResumeData(boolean interim_save) throws Exception{
					}
					
					@Override
					public void removeListener(DiskManagerListener l){
					}
					
					@Override
					public DirectByteBuffer readBlock(int pieceNumber, int offset, int length){
						return null;
					}
					
					@Override
					public void moveDataFiles(File new_parent_dir, String dl_name ){
					}
					
					@Override
					public void 
					rateLimitChanged()
					{
					}
					
					@Override
					public boolean isStopped(){
						return true;
					}
					
					@Override
					public boolean isInteresting(int pieceNumber){
						return false;
					}
					
					@Override
					public boolean isDone(int pieceNumber){
						return false;
					}
					
					@Override
					public boolean hasOutstandingWriteRequestForPiece(int piece_number){
						return false;
					}
					
					@Override
					public boolean hasOutstandingReadRequestForPiece(int piece_number){
						return false;
					}
					
					@Override
					public boolean hasOutstandingCheckRequestForPiece(int piece_number){
						return false;
					}
					
					@Override
					public boolean hasListener(DiskManagerListener l){
						return false;
					}
					
					@Override
					public long getTotalLength(){
						return 0;
					}
					
					@Override
					public int getState(){
						return( SNAPSHOT );
					}
					
					@Override
					public long getSizeExcludingDND(){
						return 0;
					}
					
					@Override
					public File getSaveLocation(){
						return null;
					}
					
					@Override
					public long getRemainingExcludingDND(){
						return 0;
					}
					
					@Override
					public long getRemaining(){
						return 0;
					}
					
					@Override
					public long[] getReadStats(){
						return null;
					}
					
					@Override
					public long[] getWriteStats(){
						return null;
					}
					
					@Override
					public long getPriorityChangeMarker(){
						return 0;
					}
					
					@Override
					public DiskManagerPiece[] getPieces(){
						return pieces;
					}
					
					@Override
					public DMPieceMap getPieceMap(){
						return piece_map;
					}
					
					@Override
					public BitFlags getAvailability(){
						return null;
					}
					
					@Override
					public int getPieceLength(int piece_number){
						return( piece_number==nbPieces-1?lastPieceLength:pieceLength );
					}
					
					@Override
					public int getPieceLength(){
						return pieceLength;
					}
					
					@Override
					public DiskManagerPiece getPiece(int piece_number){
						return pieces[piece_number];
					}
					
					@Override
					public int getPercentDoneExcludingDND(){
						return 0;
					}
					
					@Override
					public int getPercentDone(){
						return 0;
					}
					
					@Override
					public int getPercentAllocated(){
						return 0;
					}
					
					@Override
					public int getNbPieces(){
						return pieces.length;
					}
					
					@Override
					public String getMoveSubTask(){
						return null;
					}
					
					@Override
					public long[] getMoveProgress(){
						return null;
					}
					
					@Override
					public DiskManagerFileInfo[] getFiles(){
						return null;
					}
					
					@Override
					public DiskManagerFileInfoSet getFileSet(){
						return null;
					}
					
					@Override
					public int getErrorType(){
						return 0;
					}
					
					@Override
					public String getErrorMessage(){
						return null;
					}
					
					@Override
					public int getCompleteRecheckStatus(){
						return 0;
					}
					
					@Override
					public boolean
					getRecheckCancelled()
					{
						return( false );
					}
					
					@Override
					public int getCacheMode(){
						return 0;
					}
					
					@Override
					public void generateEvidence(IndentWriter writer){					
					}
					
					@Override
					public boolean filesExist(){
						return false;
					}
					
					@Override
					public void enqueueWriteRequest(DiskManagerWriteRequest request, DiskManagerWriteRequestListener listener){					
					}
					
					@Override
					public void enqueueReadRequest(DiskManagerReadRequest request, DiskManagerReadRequestListener listener){						
					}
					
					@Override
					public void enqueueCompleteRecheckRequest(DiskManagerCheckRequest request,
							DiskManagerCheckRequestListener listener){						
					}
					
					@Override
					public void enqueueCheckRequest(DiskManagerCheckRequest request, DiskManagerCheckRequestListener listener){
					}
					
					@Override
					public void downloadRemoved(){
					}
					
					@Override
					public DownloadEndedProgress
					downloadEnded( boolean start_of_day )
					{
						return( new DownloadEndedProgress(){
							@Override
							public boolean isComplete(){
								return( true );
							}
						});
					}
					
					@Override
					public DiskManagerWriteRequest createWriteRequest(int pieceNumber, int offset, DirectByteBuffer data,
							Object user_data){
						return null;
					}
					
					@Override
					public DiskManagerReadRequest createReadRequest(int pieceNumber, int offset, int length){
						return null;
					}
					
					@Override
					public DiskManagerCheckRequest createCheckRequest(int pieceNumber, Object user_data){
						return null;
					}
					
					@Override
					public boolean checkBlockConsistencyForWrite(String originator, int pieceNumber, int offset, DirectByteBuffer data){
						return false;
					}
					
					@Override
					public boolean checkBlockConsistencyForRead(String originator, boolean peer_request, int pieceNumber, int offset,
							int length){
						return false;
					}
					
					@Override
					public boolean checkBlockConsistencyForHint(String originator, int pieceNumber, int offset, int length){
						return false;
					}
					
					@Override
					public void addListener(DiskManagerListener l){
					}
					
					@Override
					public void skippedFileSetChanged(DiskManagerFileInfo file){
					}
					
					@Override
					public void setPieceDone(DiskManagerPieceImpl piece, boolean done){
						piece.setDoneSupport( done );
					}
										
					@Override
					public void setFailedAndRecheck(DiskManagerFileInfo file, String reason){
					}
					
					@Override
					public boolean isUploadOnly(){
						return( false );
					}
					
					@Override
					public void setFailed(int type, String reason, Throwable cause, boolean can_continue ){
					}
										
					@Override
					public void priorityChanged(DiskManagerFileInfo file){
					}
					
					@Override
					public void storageTypeChanged(DiskManagerFileInfo file){
					}
					
					@Override
					public TOTorrent getTorrent(){
						return null;
					}
					
					@Override
					public String[] getStorageTypes(){
						return null;
					}
					
					@Override
					public String getStorageType(int fileIndex){
						return null;
					}
					
					@Override
					public DiskManagerRecheckScheduler getRecheckScheduler(){
						return null;
					}
					
					@Override
					public DMPieceList getPieceList(int piece_number){
						return piece_map.getPieceList(piece_number);
					}
					
					@Override
					public byte[] getPieceHash(int piece_number) throws TOTorrentException{
						return null;
					}
					
					@Override
					public String getInternalName(){
						return null;
					}
					
					@Override
					public DownloadManagerState getDownloadState(){
						return null;
					}
					
					@Override
					public DiskAccessController getDiskAccessController(){
						return null;
					}
				};
	        
	        for ( int i=0; i<nbPieces; i++) {
	        
	            pieces[i] = new DiskManagerPieceImpl( disk_manager, i, i==nbPieces-1?lastPieceLength:pieceLength);
	        }
	
	        RDResumeHandler.setupPieces( dm.getDownloadState(), pieces );
	        	        
	        for ( int i=0; i<nbPieces; i++ ){
	        	
	        	pieces[i].calcNeeded();
	        }
	        	        
			return( pieces );
			
		}catch( Throwable e ){
			
			Debug.out( e );
			
			return( null );
		}
	}
	
	private static List<CoreOperationTask>	move_tasks = new ArrayList<>();
	  
	
	private static boolean move_smallest_first;
	
	static{
		COConfigurationManager.addAndFireParameterListener(
			ConfigKeys.File.BCFG_DISKMANAGER_MOVE_SMALLESTFIRST, (n)->{
				move_smallest_first = COConfigurationManager.getBooleanParameter(n);
			});
	}
	
	public static void
	runMoveTask(
		DownloadManager		download_manager,
		File				destination,
		Runnable			target,
		MoveTaskAapter		adapter )
	
		throws DownloadManagerException
	{		
		try{
			DownloadManagerStats stats = download_manager.getStats();

			if ( stats.getTotalDataBytesReceived() == 0 && stats.getPercentDoneExcludingDND() == 0 ){

					// looks like it hasn't run yet

				boolean found_file = false;

				for ( DiskManagerFileInfo info: download_manager.getDiskManagerFileInfoSet().getFiles()){

					if ( !info.getTorrentFile().isPadFile()){

						if ( info.exists()){

							found_file = true;
							
							break;
						}
					}
				}

				if ( !found_file ){

						// going to run quickly, avoid queue and scheduling
					
					target.run();
					
					return;
				}
			}
			
			FileUtil.runAsTask(
				new CoreOperationTask()
				{
					private boolean		added_to_tasks	= false;
					private boolean		queued			= true;

					@Override
					public String 
					getName()
					{
						return( download_manager.getDisplayName());
					}

					@Override
					public DownloadManager 
					getDownload()
					{
						return( download_manager );
					}

					@Override
					public String[] 
					getAffectedFileSystems()
					{
						File save_loc = download_manager.getAbsoluteSaveLocation();
						
						return( FileUtil.getFileStoreNames( save_loc, destination==null?save_loc:destination ));
					}

					private ProgressCallback callback = 
						new ProgressCallbackAdapter( move_smallest_first?ProgressCallbackAdapter.SORT_SIZE:ProgressCallbackAdapter.SORT_ALLOC_ORDER )
						{
							private volatile int	current_state = ProgressCallback.ST_NONE;
	
							@Override
							public int 
							getProgress()
							{
								long[] mp = adapter.getMoveProgress();
	
								return( mp==null?0:(int)mp[0]);
							}
	
							@Override
							public long 
							getSize()
							{
								long[] mp = adapter.getMoveProgress();
	
								return( mp==null?download_manager.getStats().getSizeExcludingDND():mp[1] );
							}
	
							@Override
							public String 
							getSubTaskName()
							{
								return( adapter.getMoveSubTask());
							}
	
							@Override
							public int 
							getSupportedTaskStates()
							{
								return( 
										ProgressCallback.ST_PAUSE |  
										ProgressCallback.ST_RESUME |
										ProgressCallback.ST_CANCEL |
										ProgressCallback.ST_SUBTASKS );
							}
	
							@Override
							public void 
							setTaskState(
									int state )
							{
								if ( current_state == ProgressCallback.ST_CANCEL ){
	
									return;
								}
	
								if ( state == ProgressCallback.ST_PAUSE ){
	
									adapter.setMoveState( ProgressListener.ST_PAUSED );
	
									current_state = state;
	
								}else if ( state == ProgressCallback.ST_RESUME ){
	
									adapter.setMoveState( ProgressListener.ST_NORMAL );
	
									current_state = ProgressCallback.ST_NONE;									
	
								}else if ( state == ProgressCallback.ST_CANCEL ){
	
									adapter.setMoveState( ProgressListener.ST_CANCELLED );
	
									current_state = ProgressCallback.ST_CANCEL;									
								}
							}
	
							public int
							getTaskState()
							{
								if ( current_state == ProgressCallback.ST_NONE && queued ){
	
									return( ProgressCallback.ST_QUEUED );
								}
	
								return( current_state );
							}
						};
	
					@Override
					public boolean
					runOperation(
						CoreOperation operation)
					{
						boolean has_scheduler = DiskManagerOperationScheduler.isEnabled();
						
							// if nothing is scheduling operations then we have to implement
							// the 'recheck before move' ourselves (with a scheduler the move operation
							// is guaranteed to be added after the recheck operation if so configured)
						
						if ( !has_scheduler ){
							
							while( true ){
							
								boolean checkBeforeMove	= COConfigurationManager.getBooleanParameter(ConfigKeys.File.BCFG_CHECK_PIECES_ON_COMPLETION_BEFORE_MOVE);

								if ( checkBeforeMove ){
									
									DiskManager dm = download_manager.getDiskManager();
									
									if ( dm != null && dm.getCompleteRecheckStatus() >= 0 ){
																				
										try{
											Thread.sleep( 500 );
											
										}catch( Throwable e ){
											
										}
										
										if ( callback.getTaskState() == ProgressCallback.ST_CANCEL ){

											throw( new RuntimeException( "Cancelled" ));
										}
										
											// come back later
										
										return( false );
										
									}else{
										
										break;
									}
								}else{
									
									break;
								}
							}
						}
						
						boolean ready;

						synchronized( move_tasks ){

							if ( !added_to_tasks ){
								
								move_tasks.add( this );
								
								added_to_tasks = true;
							}

							ready = move_tasks.size() == 1;
						}

						boolean deferred = false;
						
						try{
							if ( has_scheduler ){

								while( callback.getTaskState() == ProgressCallback.ST_PAUSE ){

									try{
										Thread.sleep( 500 );

									}catch( Throwable e ){

									}
									
										// come back later
									
									deferred = true;
									
									return( false );
								}

								if ( callback.getTaskState() == ProgressCallback.ST_CANCEL ){

									throw( new RuntimeException( "Cancelled" ));
								}

							}else{

								while( !ready ){

									try{
										Thread.sleep( 500 );

									}catch( Throwable e ){

									}

									if ( callback.getTaskState() == ProgressCallback.ST_CANCEL ){

										throw( new RuntimeException( "Cancelled" ));
									}

									synchronized( move_tasks ){

										for ( CoreOperationTask task: move_tasks ){

											int state = task.getProgressCallback().getTaskState();

											if ( state != ProgressCallback.ST_PAUSE ){

												if ( task == this ){

													if ( state == ProgressCallback.ST_QUEUED ){

														ready = true;
													}
												}

												break;
											}
										}
									}
									
									if ( !ready ){
										
											// come back later
										
										deferred = true;
										
										return( false );
									}
								}
							}

							queued = false;

							target.run();

							return( true );
							
						}finally{

							if ( !deferred ){
								
								synchronized( move_tasks ){
	
									move_tasks.remove( this );
								}
							}
						}
					}
				
					@Override
					public ProgressCallback 
					getProgressCallback()
					{
						return( callback );
					}
				});
			
		}catch( RuntimeException e ){

			Throwable cause = e.getCause();

			if ( cause instanceof DownloadManagerException ){

				throw((DownloadManagerException)cause);
			}

			throw( e );
		}
	}

	public interface
	MoveTaskAapter
	{
		public long[]
		getMoveProgress();
		
		public String
		getMoveSubTask();
		
		public void
		setMoveState(
			int	state );
	}
	
	
	protected static File
	setSkippedInternalSupport(
		DownloadManager			dm,
		DiskManagerFileInfo		dm_file,
		CacheFile				cache_file,
		boolean					skipped )
	{
		DownloadManagerState	dm_state = dm.getDownloadState();
		
		int file_index = dm_file.getIndex();
				
		String dnd_sf = dm_state.getAttribute( DownloadManagerState.AT_DND_SUBFOLDER );

 		if ( dnd_sf != null ){

			File	link = dm_file.getLink();				// where the file currently is, null if not amended at all

			File 	base_file = dm_file.getFile( false );	// where the file should be without any renaming/moving

    		if ( skipped ){

				File	base_parent = base_file.getParentFile();
				
				boolean file_ok = base_parent != null;
				
				if ( file_ok ){	
					
	    				// if the file isn't pretty much where we expect it to be then we don't touch it
	    			
	    			file_ok = link == null || link.equals( base_file );
	    			
	    			if ( !file_ok ){
	    				
	    					// not identical but might still be good enough
	    				
	    				if ( base_parent != null && base_parent.equals( link.getParentFile())){
	    					
	    						// that'll do, we don't really care about the name to move it
	    					
	    					file_ok = true;
	    				}
	    			}
				}
				
    			if ( file_ok ){

					String new_name = link==null?base_file.getName():link.getName();

						// just in case add in any expected stuff
					
					String prefix = dm_state.getAttribute( DownloadManagerState.AT_DND_PREFIX );

					String incomp_ext = dm_state.getAttribute( DownloadManagerState.AT_INCOMP_FILE_SUFFIX );

					boolean incomplete = dm_file.getDownloaded() != dm_file.getLength();
					
					if ( prefix != null && incomplete && !new_name.startsWith( prefix )){

						new_name = prefix + new_name;
					}

					if ( incomp_ext != null && incomplete && !new_name.endsWith( incomp_ext )){
												
						new_name = new_name + incomp_ext;
					}
					
					File new_parent = FileUtil.newFile( base_parent, dnd_sf );

					File new_file = FileUtil.newFile( new_parent, new_name );

					if ( !FileUtil.exists( new_file )){

						if ( !FileUtil.exists( new_parent )){

							new_parent.mkdirs();
						}

						if ( new_parent.canWrite()){

							if ( cache_file == null ){
								
								boolean ok;
								
		   						File source = link==null?base_file:link;
	    						
	    						if ( FileUtil.exists( source )){

	    							ok = FileUtil.renameFile( source, new_file );

	    						}else{

	    								// might not be allocated yet
	    							
	    							ok = true;
	    							
	    							File[] files = new_parent.listFiles();
									
									if ( files != null && files.length == 0 ){

										new_parent.delete();
									}
	    						}

	    						if ( ok ){

	    							return( new_file );
	    						}
							}else{
	    						boolean ok;
	
								try{
									dm_state.setFileLink( file_index, base_file, new_file );
	
									cache_file.moveFile( new_file, null );
	
									ok = true;
	
								}catch( Throwable e ){
	
									ok = false;
	
									Debug.out( e );
								}
	
	    						if ( !ok ){
	
	    							dm_state.setFileLink( file_index, base_file, link );
	    						}
							}
						}
					}
    			}
    		}else{

 					// here we are removing the added folder so there must be a link
    			
    			if ( link != null ){

					File link_parent = link.getParentFile();

					if ( link_parent != null && link_parent.getName().equals( dnd_sf )){

						File new_parent = link_parent.getParentFile();

						if ( new_parent != null ){
									
							File new_file = FileUtil.newFile( new_parent, link.getName());
							
							if ( cache_file == null ){
								
									// inactive case
								
								File result = null;
								
								if ( FileUtil.exists( link )){
									
									if ( FileUtil.renameFile( link, new_file )){
		
										result = new_file;
									}
								}else{
									
										// file might not be allocated yet
									
									result = new_file;
								}
								
								if ( FileUtil.exists( link_parent )){
									
									File[] files = link_parent.listFiles();
									
									if ( files != null && files.length == 0 ){

										link_parent.delete();
									}
								}
								
								return( result );
								
							}else{
					
									// active case
								
								boolean	ok;
	
								try{
	
									dm_state.setFileLink( file_index, base_file, new_file );
	
									cache_file.moveFile( new_file, null );
									
									File[] files = link_parent.listFiles();
	
									if ( files != null && files.length == 0 ){
	
										link_parent.delete();
									}
	
									ok = true;
	
								}catch( Throwable e ){
	
									ok = false;
	
									Debug.out( e );
								}
	
								if ( !ok ){
	
									dm_state.setFileLink( file_index, base_file, link );
	    						}
							}
						}
					}
    			}
    		}
    		
    		return( null );
		}
	
		String dnd_al = dm_state.getAttribute( DownloadManagerState.AT_DND_ALT_LOC );

 		if ( dnd_al != null ){

 			File dnd_root	= FileUtil.newFile( dnd_al );
 			 			
 			String save_location_path = dm.getSaveLocation().getAbsolutePath();
 			
			File	link = dm_file.getLink();				// where the file currently is, null if not amended at all

			File 	base_file = dm_file.getFile( false );	// where the file should be without any renaming/moving

    		if ( skipped ){ 
   			
				File	base_parent = base_file.getParentFile();
				
				boolean file_ok = base_parent != null && base_file.getAbsolutePath().startsWith( save_location_path );
				
				if ( file_ok ){	
					
	    				// if the file isn't pretty much where we expect it to be then we don't touch it
	    			
	    			file_ok = link == null || link.equals( base_file );
	    			
	    			if ( !file_ok ){
	    				
	    					// not identical but might still be good enough
	    				
	    				if ( base_parent != null && base_parent.equals( link.getParentFile())){
	    					
	    						// that'll do, we don't really care about the name to move it
	    					
	    					file_ok = true;
	    				}
	    			}
				}
				
    			if ( file_ok ){

					String new_name = link==null?base_file.getName():link.getName();

						// just in case add in any expected stuff
					
					String prefix = dm_state.getAttribute( DownloadManagerState.AT_DND_PREFIX );

					String incomp_ext = dm_state.getAttribute( DownloadManagerState.AT_INCOMP_FILE_SUFFIX );

					boolean incomplete = dm_file.getDownloaded() != dm_file.getLength();
					
					if ( prefix != null && incomplete && !new_name.startsWith( prefix )){

						new_name = prefix + new_name;
					}

					if ( incomp_ext != null && incomplete && !new_name.endsWith( incomp_ext )){
												
						new_name = new_name + incomp_ext;
					}
					
					File new_parent = FileUtil.newFile( dnd_root, base_parent.getAbsolutePath().substring( save_location_path.length()));

					File new_file = FileUtil.newFile( new_parent, new_name );

					if ( !FileUtil.exists( new_file )){

						if ( !FileUtil.exists( new_parent )){

							new_parent.mkdirs();
						}

						if ( new_parent.canWrite()){

							if ( cache_file == null ){
								
								boolean ok;
								
		   						File source = link==null?base_file:link;
	    						
	    						if ( FileUtil.exists( source )){

	    							ok = FileUtil.renameFile( source, new_file );

	    						}else{

	    								// might not be allocated yet
	    							
	    							ok = true;
	    							
	    							File[] files = new_parent.listFiles();
									
									if ( files != null && files.length == 0 ){

										new_parent.delete();
									}
	    						}

	    						if ( ok ){

	    							return( new_file );
	    						}
							}else{
	    						boolean ok;
	
								try{
									dm_state.setFileLink( file_index, base_file, new_file );
	
									cache_file.moveFile( new_file, null );
	
									ok = true;
	
								}catch( Throwable e ){
	
									ok = false;
	
									Debug.out( e );
								}
	
	    						if ( !ok ){
	
	    							dm_state.setFileLink( file_index, base_file, link );
	    						}
							}
						}
					}
    			}
    		}else{

 					// here we are removing the added folder so there must be a link
    			
    			if ( link != null ){

					if ( link.getAbsolutePath().startsWith( dnd_root.getAbsolutePath())){

						File new_parent = base_file.getParentFile();

						if ( new_parent != null ){
									
							File new_file = FileUtil.newFile( new_parent, link.getName());
							
							if ( cache_file == null ){
								
									// inactive case
								
								File result = null;
								
								if ( FileUtil.exists( link )){
									
									if ( FileUtil.renameFile( link, new_file )){
		
										result = new_file;
									}
								}else{
									
										// file might not be allocated yet
									
									result = new_file;
								}
								
								return( result );
								
							}else{
					
									// active case
								
								boolean	ok;
	
								try{
	
									dm_state.setFileLink( file_index, base_file, new_file );
	
									cache_file.moveFile( new_file, null );
	
									ok = true;
	
								}catch( Throwable e ){
	
									ok = false;
	
									Debug.out( e );
								}
	
								if ( !ok ){
	
									dm_state.setFileLink( file_index, base_file, link );
	    						}
							}
						}
					}
    			}
    		}
		}
 		
 		return( null );
	}
}
