#include "copyjob.h"
#include "totalsizejob.h"
#include "fileinfo_p.h"

namespace Fm {

CopyJob::CopyJob(const FilePathList& paths, const FilePath& destDirPath, Mode mode):
    FileOperationJob{},
    srcPaths_{paths},
    destDirPath_{destDirPath},
    mode_{mode},
    skip_dir_content{false} {
}

CopyJob::CopyJob(const FilePathList &&paths, const FilePath &&destDirPath, Mode mode):
    FileOperationJob{},
    srcPaths_{paths},
    destDirPath_{destDirPath},
    mode_{mode},
    skip_dir_content{false} {
}

void CopyJob::gfileProgressCallback(goffset current_num_bytes, goffset total_num_bytes, CopyJob* _this) {
    _this->setCurrentFileProgress(total_num_bytes, current_num_bytes);
}

bool CopyJob::copyRegularFile(const FilePath& srcPath, GFileInfoPtr /*srcFile*/, const FilePath& destPath) {
    int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS;
    GErrorPtr err;
_retry_copy:
    if(!g_file_copy(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(),
                    GFileProgressCallback(gfileProgressCallback), this, &err)) {
        flags &= ~G_FILE_COPY_OVERWRITE;
        /* handle existing files or file name conflict */
        if(err.domain() == G_IO_ERROR && (err.code() == G_IO_ERROR_EXISTS ||
                                         err.code() == G_IO_ERROR_INVALID_FILENAME ||
                                         err.code() == G_IO_ERROR_FILENAME_TOO_LONG)) {
#if 0
            GFile* dest_cp = new_dest;
            bool dest_exists = (err->code == G_IO_ERROR_EXISTS);
            FmFileOpOption opt = 0;
            g_error_free(err);
            err = nullptr;

            new_dest = nullptr;
            opt = _fm_file_ops_job_ask_new_name(job, src, dest, &new_dest, dest_exists);
            if(!new_dest) { /* restoring status quo */
                new_dest = dest_cp;
            }
            else if(dest_cp) { /* we got new new_dest, forget old one */
                g_object_unref(dest_cp);
            }
            switch(opt) {
            case FM_FILE_OP_RENAME:
                dest = new_dest;
                goto _retry_copy;
                break;
            case FM_FILE_OP_OVERWRITE:
                flags |= G_FILE_COPY_OVERWRITE;
                goto _retry_copy;
                break;
            case FM_FILE_OP_CANCEL:
                fm_job_cancel(fmjob);
                break;
            case FM_FILE_OP_SKIP:
                ret = true;
                delete_src = false; /* don't delete source file. */
                break;
            case FM_FILE_OP_SKIP_ERROR: ; /* FIXME */
            }
#endif
        }
        else {
            ErrorAction act = emitError( err, ErrorSeverity::MODERATE);
            err.reset();
            if(act == ErrorAction::RETRY) {
                // FIXME: job->current_file_finished = 0;
                goto _retry_copy;
            }
# if 0
            const bool is_no_space = (err.domain() == G_IO_ERROR &&
                                err.code() == G_IO_ERROR_NO_SPACE);
            /* FIXME: ask to leave partial content? */
            if(is_no_space) {
                g_file_delete(dest, fm_job_get_cancellable(fmjob), nullptr);
            }
            ret = false;
            delete_src = false;
#endif
        }
        err.reset();
    }
    else {
        return true;
    }
    return false;
}

bool CopyJob::copySpecialFile(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& destPath) {
    bool ret = false;
    GError* err = nullptr;
    /* only handle FIFO for local files */
    if(srcPath.isNative() && destPath.isNative()) {
        auto src_path = srcPath.localPath();
        struct stat src_st;
        int r;
        r = lstat(src_path.get(), &src_st);
        if(r == 0) {
            /* Handle FIFO on native file systems. */
            if(S_ISFIFO(src_st.st_mode)) {
                auto dest_path = destPath.localPath();
                if(mkfifo(dest_path.get(), src_st.st_mode) == 0) {
                    ret = true;
                }
            }
            /* FIXME: how about block device, char device, and socket? */
        }
    }
    if(!ret) {
        g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED,
                    ("Cannot copy file '%s': not supported"),
                    g_file_info_get_display_name(srcFile.get()));
        // emitError( err, ErrorSeverity::MODERATE);
        g_clear_error(&err);
    }
    return ret;
}

bool CopyJob::copyDir(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& destPath) {
    bool ret = false;
    if(makeDir(srcPath, srcFile, destPath)) {
        GError* err = nullptr;
        auto enu = GFileEnumeratorPtr{
                g_file_enumerate_children(srcPath.gfile().get(),
                                          gfile_info_query_attribs,
                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                          cancellable().get(), &err),
                false};
        if(enu) {
            int n_children = 0;
            int n_copied = 0;
            ret = true;
            while(!isCancelled()) {
                auto inf = GFileInfoPtr{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false};
                if(inf) {
                    ++n_children;
                    /* don't overwrite dir content, only calculate progress. */
                    if(Q_UNLIKELY(skip_dir_content)) {
                        /* FIXME: this is incorrect as we don't do the calculation recursively. */
                        addFinishedAmount(g_file_info_get_size(inf.get()), 1);
                    }
                    else {
                        const char* name = g_file_info_get_name(inf.get());
                        FilePath childPath = srcPath.child(name);
                        bool child_ret = copyPath(childPath, inf, destPath, name);
                        if(child_ret) {
                            ++n_copied;
                        }
                        else {
                            ret = false;
                        }
                    }
                }
                else {
                    if(err) {
                        // FIXME: emitError( err, ErrorSeverity::MODERATE);
                        g_error_free(err);
                        err = nullptr;
                        /* ErrorAction::RETRY is not supported here */
                        ret = false;
                    }
                    else { /* EOF is reached */
                        /* all files are successfully copied. */
                        if(isCancelled()) {
                            ret = false;
                        }
                        else {
                            /* some files are not copied */
                            if(n_children != n_copied) {
                                /* if the copy actions are skipped deliberately, it's ok */
                                if(!skip_dir_content) {
                                    ret = false;
                                }
                            }
                            /* else job->skip_dir_content is true */
                        }
                        break;
                    }
                }
            }
            g_file_enumerator_close(enu.get(), nullptr, &err);
        }
    }
    return false;
}

bool CopyJob::makeDir(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& dirPath) {
    GError* err = nullptr;
    if(isCancelled())
        return false;

    FilePath destPath = dirPath;
    bool mkdir_done = false;
    do {
        mkdir_done = g_file_make_directory(destPath.gfile().get(), cancellable().get(), &err);
        if(err->domain == G_IO_ERROR && (err->code == G_IO_ERROR_EXISTS ||
                                         err->code == G_IO_ERROR_INVALID_FILENAME ||
                                         err->code == G_IO_ERROR_FILENAME_TOO_LONG)) {
            GFileInfoPtr destFile;
            // FIXME: query its info
            FilePath newDestPath;
            FileExistsAction opt = askRename(FileInfo{srcFile, srcPath.parent()}, FileInfo{destFile, dirPath.parent()}, newDestPath);
            g_error_free(err);
            err = nullptr;

            switch(opt) {
            case FileOperationJob::RENAME:
                destPath = newDestPath;
                break;
            case FileOperationJob::SKIP:
                /* when a dir is skipped, we need to know its total size to calculate correct progress */
                // job->finished += size;
                // fm_file_ops_job_emit_percent(job);
                // job->skip_dir_content = skip_dir_content = true;
                mkdir_done = true; /* pretend that dir creation succeeded */
                break;
            case FileOperationJob::OVERWRITE:
                mkdir_done = true; /* pretend that dir creation succeeded */
                break;
            case FileOperationJob::CANCEL:
                cancel();
                break;
            case FileOperationJob::SKIP_ERROR: ; /* FIXME */
            }
        }
        else {
#if 0
            ErrorAction act = emitError( err, ErrorSeverity::MODERATE);
            g_error_free(err);
            err = nullptr;
            if(act == ErrorAction::RETRY) {
                goto _retry_mkdir;
            }
#endif
            break;
        }
        // job->finished += size;
    } while(!mkdir_done && !isCancelled());

    if(mkdir_done && !isCancelled()) {
        bool chmod_done = false;
        mode_t mode = g_file_info_get_attribute_uint32(srcFile.get(), G_FILE_ATTRIBUTE_UNIX_MODE);
        if(mode) {
            mode |= (S_IRUSR | S_IWUSR); /* ensure we have rw permission to this file. */
            do {
                /* chmod the newly created dir properly */
                // if(!fm_job_is_cancelled(fmjob) && !job->skip_dir_content)
                chmod_done = g_file_set_attribute_uint32(destPath.gfile().get(),
                                                         G_FILE_ATTRIBUTE_UNIX_MODE,
                                                         mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                                         cancellable().get(), &err);
                if(!chmod_done) {
/*
                    ErrorAction act = emitError( err, ErrorSeverity::MODERATE);
                    g_error_free(err);
                    err = nullptr;
                    if(act == ErrorAction::RETRY) {
                        goto _retry_chmod_for_dir;
                    }
*/
                    /* FIXME: some filesystems may not support this. */
                }
            } while(!chmod_done && !isCancelled());
            // finished += size;
            // fm_file_ops_job_emit_percent(job);
        }
    }
    return false;
}

bool CopyJob::copyPath(const FilePath& srcPath, const FilePath& destDirPath, const char* destFileName) {
    GErrorPtr err;
    GFileInfoPtr srcInfo = GFileInfoPtr {
        g_file_query_info(srcPath.gfile().get(),
        gfile_info_query_attribs,
        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
        cancellable().get(), &err),
        false
    };
    if(!srcInfo || isCancelled()) {
        return false;
    }
    return copyPath(srcPath, srcInfo, destDirPath, destFileName);
}

bool CopyJob::copyPath(const FilePath& srcPath, const GFileInfoPtr& srcInfo, const FilePath& destDirPath, const char* destFileName) {
    setCurrentFile(srcPath);
    GErrorPtr err;
    GFileInfoPtr destDirInfo = GFileInfoPtr {
        g_file_query_info(destDirPath.gfile().get(),
        "id::filesystem",
        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
        cancellable().get(), &err),
        false
    };

    if(!destDirInfo || isCancelled()) {
        return false;
    }

    auto size = g_file_info_get_size(srcInfo.get());
    setCurrentFileProgress(size, 0);

    auto destPath = destDirPath.child(destFileName);
    bool success = false;
    switch(g_file_info_get_file_type(srcInfo.get())) {
    case G_FILE_TYPE_DIRECTORY:
        success = copyDir(srcPath, srcInfo, destPath);
        break;
    case G_FILE_TYPE_SPECIAL:
        success = copySpecialFile(srcPath, srcInfo, destPath);
        break;
    default:
        success = copyRegularFile(srcPath, srcInfo, destPath);
        break;
    }

    if(success) {
        addFinishedAmount(size, 1);
#if 0

        if(ret && dest_folder) {
            fm_dest = fm_path_new_for_gfile(dest);
            if(!_fm_folder_event_file_added(dest_folder, fm_dest)) {
                fm_path_unref(fm_dest);
            }
        }
#endif
    }

    return false;
}

#if 0

bool _fm_file_ops_job_copy_run(FmFileOpsJob* job) {
    bool ret = true;
    GFile* dest_dir;
    GList* l;
    FmJob* fmjob = FM_JOB(job);
    /* prepare the job, count total work needed with FmDeepCountJob */
    FmDeepCountJob* dc = fm_deep_count_job_new(job->srcs, FM_DC_JOB_DEFAULT);
    FmFolder* df;

    /* let the deep count job share the same cancellable object. */
    fm_job_set_cancellable(FM_JOB(dc), fm_job_get_cancellable(fmjob));
    fm_job_run_sync(FM_JOB(dc));
    job->total = dc->total_size;
    if(fm_job_is_cancelled(fmjob)) {
        g_object_unref(dc);
        return false;
    }
    g_object_unref(dc);
    g_debug("total size to copy: %llu", (long long unsigned int)job->total);

    dest_dir = fm_path_to_gfile(job->dest);
    /* suspend updates for destination */
    df = fm_folder_find_by_path(job->dest);
    if(df) {
        fm_folder_block_updates(df);
    }

    fm_file_ops_job_emit_prepared(job);

    for(l = fm_path_list_peek_head_link(job->srcs); !fm_job_is_cancelled(fmjob) && l; l = l->next) {
        FmPath* path = FM_PATH(l->data);
        GFile* src = fm_path_to_gfile(path);
        GFile* dest;
        char* tmp_basename;

        if(g_file_is_native(src) && g_file_is_native(dest_dir))
            /* both are native */
        {
            tmp_basename = nullptr;
        }
        else if(g_file_is_native(src)) /* copy from native to virtual */
            tmp_basename = g_filename_to_utf8(fm_path_get_basename(path),
                                              -1, nullptr, nullptr, nullptr);
        /* gvfs escapes it itself */
        else { /* copy from virtual to native/virtual */
            /* if we drop URI query onto native filesystem, omit query part */
            const char* basename = fm_path_get_basename(path);
            char* sub_name;

            sub_name = strchr(basename, '?');
            if(sub_name) {
                sub_name = g_strndup(basename, sub_name - basename);
                basename = strrchr(sub_name, G_DIR_SEPARATOR);
                if(basename) {
                    basename++;
                }
                else {
                    basename = sub_name;
                }
            }
            tmp_basename = fm_uri_subpath_to_native_subpath(basename, nullptr);
            g_free(sub_name);
        }
        dest = g_file_get_child(dest_dir,
                                tmp_basename ? tmp_basename : fm_path_get_basename(path));
        g_free(tmp_basename);
        if(!_fm_file_ops_job_copy_file(job, src, nullptr, dest, nullptr, df)) {
            ret = false;
        }
        g_object_unref(src);
        g_object_unref(dest);
    }

    /* g_debug("finished: %llu, total: %llu", job->finished, job->total); */
    fm_file_ops_job_emit_percent(job);

    /* restore updates for destination */
    if(df) {
        fm_folder_unblock_updates(df);
        g_object_unref(df);
    }
    g_object_unref(dest_dir);
    return ret;
}
#endif

void CopyJob::exec() {
    TotalSizeJob totalSizeJob{srcPaths_};
    connect(&totalSizeJob, &TotalSizeJob::error, this, &CopyJob::error);
    connect(this, &CopyJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel);
    totalSizeJob.run();
    if(isCancelled()) {
        return;
    }

    setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount());
    Q_EMIT preparedToRun();

    for(auto& srcPath : srcPaths_) {
        if(isCancelled()) {
            break;
        }
        copyPath(srcPath, destDirPath_, srcPath.baseName().get());
    }
}


} // namespace Fm
