package randomfilecopy;

import java.nio.file.*;
import java.util.*;
import java.io.IOException;
import javax.swing.*;

/** Handles the actual execution of the program's copying features */
public class CopyEngine extends SwingWorker<Void,Integer>
{

    public CopySettings settings;
    private Path srcDirPath  = null;    // path of source directory
    private Path destDirPath = null;    // path of destination directory
    private int  numSrcFiles = 0;       // total number of valid files in source directories
    private long totalSize;             // total size in bytes of all files copied
    private int  totalFiles;            // total number of files copied
    private String[] filter;            // array of file extensions used by file filter
    private EventLogger log = null;     // log being used by the main window

    private CopyEngine() { }

    public CopyEngine(EventLogger log) {
        super();
        settings = new CopySettings();
        this.log = log;
    }

    @Override protected void doInBackground() {

        srcDirPath  = Paths.get( settings.src );
        destDirPath = Paths.get( settings.dest );

        log.write( "Execution begun...");
        log.write( "Source directory = " + settings.src );
        log.write( "Destination directory = " + settings.dest );

        // create file extension filters
        filter = settings.filters.split( "[ .,]+" );

        // dirFiles is a list of lists, the inner lists are all the files in a directory, the outer list is of all directories
        List<List> dirFiles = new ArrayList<List>();
        createMasterFileList(srcDirPath, dirFiles);

        log.write( "Number of directories searched = " + dirFiles.size() );

        if (isCancelled() == true) {
            firePropertyChange("status", null, "Cancelled");
            return null; }

        copyFilesToDest( createRandomList(dirFiles) );

        return null;
    }

    @Override protected void done() {

        String status = "Done : Copied " + totalFiles + " files, total size "
                        + String.format("%.2f", totalSize / 1048576.0) + "(MB)";
        firePropertyChange("done", null, status);
    }

    /** Creates a list of all files in the sent directory, adds it to the master list, then calls itself on all subdirectories */
    private void createMasterFileList(Path currentDir, List<List> masterList) {

        // user wants to cancel?
        if (isCancelled() == true) return;

        DirectoryStream<Path> stream = null;
        List<Path> currDirFiles = null;
        int numFiles = 0;

        try {

            // iterate over all subdirectories

            stream = Files.newDirectoryStream(currentDir);
            for (Path currentPath: stream) {
                if ( Files.isDirectory( currentPath ) )
                    createMasterFileList(currentPath, masterList);
                else
                    numFiles++;
            }
            stream.close();

            firePropertyChange("status", null, "Scanning : " +  currentDir.toString());

            // iterate over all files in current directory

            currDirFiles = new ArrayList<Path>(numFiles);
            stream = Files.newDirectoryStream(currentDir);

            for (Path currentPath: stream)
                if ( !Files.isDirectory( currentPath ) )
                    currDirFiles.add( currentPath );
            stream.close();

        }
        catch (NoSuchFileException e) {
            JOptionPane.showMessageDialog( null, "Source directory does not exist.", "Error", JOptionPane.WARNING_MESSAGE ); }
        catch (Exception e) {
            System.err.println("Error searching source directory : " + e); }
        finally {
            try{ stream.close(); }        // close the stream
            catch(IOException e) {
                System.err.println("Error searching source directory : " + e );
            }
        }

        // if file list for this directory is not empty, add it to master file list
        if (!currDirFiles.isEmpty()) {
            masterList.add( currDirFiles );
            numSrcFiles += currDirFiles.size();
        }

    }    // end createMasterFileList()

    /** Randomly selects files from sent list in accordance with our parameters and returns list of chosen files */
    private List createRandomList(List<List> dirFiles) {

        List<Path> copyList = new ArrayList<Path>(settings.maxFiles);
        totalSize      = 0;
        totalFiles    = 0;

        int dirIndex, fileIndex;
        long fileSize;
        Path tempPath;

        firePropertyChange("status", null, "Picking Random Files...");

        while (totalSize < settings.maxTotalSize && totalFiles < settings.maxFiles && numSrcFiles > 0) {

            if (!settings.normalize) {
                dirIndex = 0;
                fileIndex = (int)(Math.random() * numSrcFiles);

                while ( fileIndex >= dirFiles.get(dirIndex).size() )
                    fileIndex -= dirFiles.get(dirIndex++).size();
            }
            else {

                // pick a random directory and random file in that directory

                do {
                    dirIndex = (int)(Math.random() * dirFiles.size() );
                } while ( dirFiles.get(dirIndex).isEmpty() );                // if directory has been emptied of files, find a new one

                fileIndex = (int)(Math.random() * dirFiles.get(dirIndex).size());
            }

            tempPath  = (Path)dirFiles.get(dirIndex).get( fileIndex );
            fileSize  = getFileSizeInBytes( tempPath );

            if ( isValidFile(tempPath, fileSize) ) {
                copyList.add( tempPath );
                totalSize += fileSize;
                totalFiles++;
            }

            // remove file from list, either we used it, or it's invalid for some reason
            dirFiles.get(dirIndex).remove( fileIndex );
            numSrcFiles--;
        }

        log.write("Number of files selected to copy = " + copyList.size() );
        return copyList;

    }    // end createRandomList()

    private boolean isValidFile(Path file, long fileSize) {

        if (fileSize + totalSize < settings.maxTotalSize &&
            fileSize < settings.maxFileSize &&
            fileSize > settings.minFileSize) {

            if (settings.useFilter) {

                // get file extension
                String name = file.getFileName().toString();
                String ext = name.substring( name.lastIndexOf('.')  + 1);

                //check to see if extension matches a file filter
                boolean matchedFilter = false;
                for (int i = 0; i < filter.length; i++)
                    if (ext.equalsIgnoreCase( filter[i]) )
                        matchedFilter = true;

                // file is invalid if user wants only certain extensions, and we have no match
                    // or if user does not want certain extensions, and we do have a match
                if ( (settings.filterInclude && matchedFilter == false) || (!settings.filterInclude && matchedFilter == true) )
                    return false;
            }

            return true;
        }

        return false;
    }

    /** Copies all files in the sent list into the destination directory */
    private void copyFilesToDest(List<Path> copyList) {

        Path srcFilePath, destFilePath;

        if (copyList.isEmpty() ) {
            JOptionPane.showMessageDialog( null, "No valid files found.", "Error", JOptionPane.WARNING_MESSAGE );
            return;
        }

        // create new destination directory if needed, clear if needed
        try {

        if ( !Files.exists( destDirPath ) )
            Files.createDirectories( destDirPath );
        else if (settings.clearDestDir) {

            Object o = "Are you sure you want to delete all files in the destination directory?";
            int result = JOptionPane.showConfirmDialog( null, o, "Delete All Files", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
            if (result == JOptionPane.YES_OPTION)
                deleteFilesInDir( destDirPath );
        }

        } catch (IOException e) {
            System.err.println(e); }

        // keep track of how many files/bytes have been copied
        totalSize = 0;
        totalFiles = 0;

        // copy files to destination directory
        for (int i = 0; i < copyList.size(); i++) {

            if (isCancelled() == true)  return;            // user wants to cancel?

            srcFilePath  = copyList.get( i );
            destFilePath = destDirPath.resolve( srcFilePath.getFileName() );

            firePropertyChange("status", null, "Copying file : " + srcFilePath.getFileName());

            try {
                Files.copy( srcFilePath, destFilePath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
            }
            catch (IOException e) {
                System.err.println(e + "\nsrcPath  = " + srcFilePath.toString() );
                System.err.println(         "destPath = " + destFilePath.toString() );
            }

            totalFiles++;
            totalSize += getFileSizeInBytes(srcFilePath);
            setProgress(  (int)( ((double)(i+1) / copyList.size()) * 100.0  )  );
        }

    }    // end copyFilesToDest()

    /** Deletes all files in the sent directory, leaves sub-directories alone */
    private void deleteFilesInDir(Path dir) {

        DirectoryStream<Path> stream = null;

        firePropertyChange("status", null, "Clearing Destination Directory...");

        try {

        stream = Files.newDirectoryStream(dir);
        for (Path currentPath: stream)
            if ( !Files.isDirectory( currentPath ) )    // dont attempt to delete directories
                Files.delete( currentPath );
        stream.close();
        }
        catch (IOException e) {
            System.err.println( "Error deleting files.\n" + e); }
        finally {
            try{ stream.close(); }        // close the stream
            catch(IOException e) {
                System.err.println( e ); }
        }

    }    // end deleteFilesInDir()

    /** Returns the file size of the sent file in bytes */
    private long getFileSizeInBytes(Path path) {

        try {   return Files.size(path);  }
        catch (IOException e) {
            System.err.println( "Error getting file size.\n" + e ); }

        return 0;
    }

}    // end CopyEngine