/******************************************************************************
 * mod_uploader / UploadItemList.cpp
 ******************************************************************************
 * Copyright (C) 2005 Tetsuya Kimata <kimata@acapulco.dyndns.org>
 *
 * All rights reserved.
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any
 * damages arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any
 * purpose, including commercial applications, and to alter it and
 * redistribute it freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must
 *    not claim that you wrote the original software. If you use this
 *    software in a product, an acknowledgment in the product
 *    documentation would be appreciated but is not bcktuired.
 *
 * 2. Altered source versions must be plainly marked as such, and must
 *    not be misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source
 *    distribution.
 *
 * $Id: UploadItemList.cpp 685 2005-09-15 07:18:18Z svn $
 *****************************************************************************/

#include "UploadItemList.h"
#include "TemplateVariableCreator.h"
#include "Misc.h"

#include "apr_strings.h"
#include "apr_tables.h"
#define APR_WANT_STRFUNC
#include "apr_want.h"

#ifdef DEBUG
#include <iostream>
#endif

const char UploadItemList::PATH_CACHE_FILENAME[]    = ".path_cache";
const char UploadItemList::HEADER_CACHE_FILENAME[]  = ".header_cache";
const char UploadItemList::TMP_SUFFIX[]             = ".tmp.XXXXXX";
const char UploadItemList::PATH_CACHE_DELIMITER     = '\t';


/******************************************************************************
 * public メソッド
 *****************************************************************************/
UploadItemList::UploadItemList(apr_pool_t *pool,
                               const char *file_dir, const char *thumb_dir,
                               apr_size_t total_number_limit,
                               apr_uint64_t total_size_limit)
    : pool_(pool),
      data_pool_(NULL),
      file_dir_(file_dir),
      thumb_dir_(thumb_dir),
      path_cache_path_(create_path(pool, file_dir, PATH_CACHE_FILENAME)),
      header_cache_path_(create_path(pool, file_dir, HEADER_CACHE_FILENAME)),
      total_number_limit_(total_number_limit),
      total_size_limit_(total_size_limit),
      reader_(pool, file_dir),
#ifdef MAKE_THUMBNAIL
      twriter_(pool, file_dir, thumb_dir),
#endif
      total_size_(0),
      mtime_(0)
{

}

void UploadItemList::add(const char *file_path, bool is_new)
{
    UploadItem::header *header;
    item_info *info;
    apr_file_t *file;

    header = reader_.read(file_path, &file);
    apr_file_close(file);

    // pool_ じゃなくて data_pool_ を使うとまずい．
    APR_PALLOC(info, item_info *, pool_, sizeof(item_info));
    info->file_path = static_cast<const char *>(apr_pstrdup(pool_, file_path));
    info->header = header;

    if (is_new) {
        add_new(info);
    } else {
        add(info);
    }
}

void UploadItemList::add(item_info *info)
{
    ilist_.push_front(info);
    total_size_ += info->header->file_size;
}

apr_size_t UploadItemList::size() const
{
    return ilist_.size();
}

bool UploadItemList::empty() const
{
    return ilist_.empty();
}

apr_uint64_t UploadItemList::get_total_file_size() const
{
    return total_size_;
}

apr_size_t UploadItemList::get_thumb_size() const
{
    return tlist_.size();
}

apr_time_t UploadItemList::get_mtime() const
{
    return mtime_;
}

void UploadItemList::update_mtime()
{
    apr_finfo_t info;

    if (UNLIKELY(apr_stat(&info, file_dir_, APR_FINFO_MTIME, pool_) != APR_SUCCESS)) {
        return;
    }

    mtime_ =  info.mtime;
}

bool UploadItemList::is_need_update(apr_pool_t *pool) const
{
    apr_finfo_t info;

    if (UNLIKELY(apr_stat(&info, file_dir_, APR_FINFO_MTIME, pool) != APR_SUCCESS)) {
        return false;
    }

    return mtime_ != info.mtime;
}

void UploadItemList::remove(const char *file_path, const char *password)
{
    item_iterator i;

    load_file();

    for (i = begin(); i != end(); i++) {
        if (strcmp((*i)->file_path, file_path) == 0) {
            break;
        }
    }

    if (i == end()) {
        throw "削除対象のファイルが見つかりませんでした．";
    }

    if (strcmp(password, (*i)->header->remove_pass) != 0) {
        throw "パスワードが違います．";
    }

    remove(*i);

    write_cache();
    update_mtime();
}

TemplateExecutor::variable *UploadItemList::to_varray(apr_pool_t *pool,
                                                      apr_size_t start,
                                                      apr_size_t size) const
{
    apr_array_header_t *array = apr_array_make(pool, static_cast<int>(size),
                                               static_cast<int>(sizeof(TemplateExecutor::variable *)));
    item_iterator i = begin();
    TemplateExecutor::variable **var;
    UploadItem item(pool);

    for (apr_size_t j = 0; (j < start) && (i != end()); j++) {
        i++;
    }

    for (apr_size_t j = 0; (j < size) && (i != end()); j++, i++) {
        item.set_data((*i)->file_path, (*i)->header);

        var = static_cast<TemplateExecutor::variable **>(apr_array_push(array));
        *var = TemplateVariableCreator::create(pool, &item);
    }

    return TemplateVariableCreator::create(pool, array);
}

TemplateExecutor::variable *UploadItemList::to_thumb_varray(apr_pool_t *pool,
                                                            apr_size_t start,
                                                            apr_size_t size) const
{
    apr_array_header_t *array = apr_array_make(pool, static_cast<int>(size),
                                               static_cast<int>(sizeof(TemplateExecutor::variable *)));

    thumb_iterator i = tlist_.begin();
    TemplateExecutor::variable **var;

    for (apr_size_t j = 0; (j < start) && (i != tlist_.end()); j++) {
        i++;
    }

    for (apr_size_t j = 0; (j < size) && (i != tlist_.end()); j++, i++) {
         var = static_cast<TemplateExecutor::variable **>(apr_array_push(array));
         *var = TemplateVariableCreator::create(pool, (*i).c_str());
    }

    return TemplateVariableCreator::create(pool, array);
}

UploadItemList::item_info **UploadItemList::to_array(apr_pool_t *pool,
                                                     apr_size_t start,
                                                     apr_size_t size) const
{
    item_iterator i = begin();
    item_info **item_list;
    item_info **item;

    for (apr_size_t j = 0; (j < start) && (i != end()); j++) {
        i++;
    }

    APR_PCALLOC(item_list, item_info **, pool, sizeof(item_info *)*(size+1));

    item = item_list;
    for (apr_size_t j = 0; (j < size) && (i != end()); j++, i++) {
        *item = *i;

        item++;
    }

    return item_list;
}

UploadItemList *UploadItemList::load(apr_pool_t *pool,
                                     const char *file_dir, const char *thumb_dir,
                                     apr_size_t total_number_limit,
                                     apr_uint64_t total_size_limit)
{
    UploadItemList *item_list = NULL;

    try {
        item_list = new UploadItemList(pool, file_dir, thumb_dir,
                                       total_number_limit, total_size_limit);
        item_list->init();

        return item_list;
    } catch(const char *) {
        if (item_list != NULL) {
            delete item_list;
        }

        throw;
    }
}

#ifdef DEBUG
void UploadItemList::dump_list(UploadItemList *item_list)
{
    item_iterator i;

    for (i = item_list->begin(); i != item_list->end(); i++) {
        UploadItem::dump_header((*i)->header);
    }
}
#endif


/******************************************************************************
 * private メソッド
 *****************************************************************************/
void UploadItemList::init()
{
    load_file();
    load_thumb();

    update_mtime();
}

bool UploadItemList::load_file()
{
    ilist_.clear();
    total_size_ = 0;

    if (data_pool_ != NULL) {
        apr_pool_destroy(data_pool_);
    }
    if (apr_pool_create(&data_pool_, pool_) != APR_SUCCESS) {
        throw "メモリの確保に失敗しました．";
    }

    return load_file_by_any();
}

bool UploadItemList::load_file_by_any()
{
    // 例外をフローの制御に使っちゃいました...
    try {
        load_file_by_cache();

        return true;
    } catch(const char *) {
        try {
            load_file_by_item();

            // キャッシュの読み込みに失敗した場合は生成しておく．
            try {
                write_cache();
            } catch(const char *) {
                // 無視
            }

            return false;
        } catch(const char *) {
            throw;
        }
    }
}

void UploadItemList::load_file_by_item()
{
    apr_dir_t *dir;
    apr_finfo_t info;

    if (apr_dir_open(&dir, file_dir_, pool_) != APR_SUCCESS) {
        throw "アップロードファイルのリストを読み込めませんでした．";
    }

    try {
        while (apr_dir_read(&info, APR_FINFO_NAME, dir) == APR_SUCCESS) {
            if (info.name[0] == '.') {
                continue;
            }
            add(info.name);
        }

        apr_dir_close(dir);

        sort();
    } catch(const char *) {
        apr_dir_close(dir);

        throw;
    }
}

void UploadItemList::load_file_by_cache()
{
    char *path;
    UploadItem::header *header;
    item_info *info;
    apr_size_t number;
    apr_size_t i;
    apr_pool_t *tmp_pool;

    if (apr_pool_create(&tmp_pool, pool_) != APR_SUCCESS) {
        throw "メモリの確保に失敗しました．";
    }

    try {
        read_cache(tmp_pool, &path, &header, &number);

        apr_pool_destroy(tmp_pool);
    } catch(const char *) {
        apr_pool_destroy(tmp_pool);

        throw;
    }

    for (i = 0; i < number; i++) {
        APR_PALLOC(info, item_info *, data_pool_, sizeof(item_info));

        info->file_path = path;
        info->header = header;

        add(info);

        path += strlen(path) + 1; // うーん
        header++;
    }

    sort();
}

void UploadItemList::load_thumb()
{
#ifdef MAKE_THUMBNAIL
    apr_dir_t *dir;
    apr_finfo_t info;
    char *name;

    if (apr_dir_open(&dir, thumb_dir_, pool_) != APR_SUCCESS) {
        throw "サムネイルのリストを読み込めませんでした．";
    }

    while (apr_dir_read(&info, APR_FINFO_NAME, dir) == APR_SUCCESS) {
        if ((info.name[0] == '.') ||
            (strlen(info.name) <= strlen(ThumbnailWriter::FILE_SUFFIX))) {
            continue;
        }

        if (strcmp(info.name+strlen(info.name)-strlen(ThumbnailWriter::FILE_SUFFIX),
                   ThumbnailWriter::FILE_SUFFIX) != 0) {
            continue;
        }

        name = apr_pstrndup(pool_, info.name,
                            strlen(info.name)-strlen(ThumbnailWriter::FILE_SUFFIX));

        tlist_.push_front(name);
    }

    apr_dir_close(dir);

    if (!tlist_.empty()) {
        tlist_.sort(ThumbnailCompare());
    }
#endif
}

void UploadItemList::add_new(item_info *info)
{
    bool is_load_by_cache;

    is_load_by_cache = load_file();

#ifdef MAKE_THUMBNAIL
    try {
        twriter_.write(info->file_path);

        tlist_.push_front(info->file_path);
    } catch(const char *) {
        // 無視
    }
#endif

    if (is_load_by_cache) {
        add(info);
    }

    while (ilist_.size() > total_number_limit_) {
        remove(ilist_.back());
    }
    while (total_size_ > total_size_limit_) {
        remove(ilist_.back());
    }

    write_cache();
    update_mtime();
}

UploadItemList::item_iterator UploadItemList::begin() const
{
    return ilist_.begin();
}

UploadItemList::item_iterator UploadItemList::end() const
{
    return ilist_.end();
}

void UploadItemList::remove(item_info *info)
{
    char *file_path;

    if (apr_filepath_merge(&file_path, file_dir_, info->file_path,
                           APR_FILEPATH_NOTABOVEROOT, pool_) != APR_SUCCESS) {
        throw "アップロードファイルの名前を構成できませんでした．";
    }

    if (apr_file_remove(file_path, pool_) != APR_SUCCESS) {
        throw "アップロードファイルを削除できませんでした．";
    }

#ifdef MAKE_THUMBNAIL
    tlist_.remove(info->file_path);
    apr_file_remove(ThumbnailWriter::create_thumb_path(pool_, thumb_dir_, info->file_path),
                    pool_);
#endif

    total_size_ -= info->header->file_size;
    ilist_.remove(info);
}


const char *UploadItemList::create_path(apr_pool_t *tmp_pool, const char *dir_path,
                                        const char *file_name)
{
    char *file_path;

    if (apr_filepath_merge(&file_path, dir_path, file_name,
                           APR_FILEPATH_NOTABOVEROOT,tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの名前を作成できませんでした．";
    }

    return file_path;
}

const char *UploadItemList::create_tmp_path(apr_pool_t *tmp_pool, const char *name,
                                            bool is_buffered, apr_file_t **tmp_file)
{
    char *tmp_path;

    tmp_path = const_cast<char *>(create_path(tmp_pool, file_dir_,
                                              apr_pstrcat(tmp_pool, name, TMP_SUFFIX, NULL)));

    if (apr_file_mktemp(tmp_file, tmp_path,
                        APR_READ|APR_WRITE|APR_CREATE|APR_EXCL|APR_BINARY|
                        (is_buffered ? APR_BUFFERED : 0),
                        tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルを作成できませんでした．";
    }

    return tmp_path;
}

const char *UploadItemList::get_tmp_path_cache_path(apr_pool_t *tmp_pool, apr_file_t **cache_file)
{
    return create_tmp_path(tmp_pool, PATH_CACHE_FILENAME, true, cache_file);
}

const char *UploadItemList::get_tmp_header_cache_path(apr_pool_t *tmp_pool, apr_file_t **cache_file)
{
    return create_tmp_path(tmp_pool, HEADER_CACHE_FILENAME, false, cache_file);
}

void UploadItemList::read_cache(apr_pool_t *tmp_pool,
                                char **paths, UploadItem::header **headers,
                                apr_size_t *number)
{
    apr_size_t path_number;
    apr_size_t header_number;

    *number = 0;

    read_path_cache(tmp_pool, paths, &path_number);
    read_header_cache(tmp_pool, headers, &header_number);

    if (path_number != header_number) {
        throw "アイテムのキャッシュに不整合が見つかりました．";
    }

    *number = path_number;
}

void UploadItemList::read_path_cache(apr_pool_t *tmp_pool, char **paths,
                                     apr_size_t *number)
{
    apr_file_t *cache_file;
    apr_mmap_t *file_map;
    apr_finfo_t info;
    char *data;

    *number = 0;

    if (apr_file_open(&cache_file, path_cache_path_,
                      APR_READ|APR_BINARY,
                      APR_OS_DEFAULT, tmp_pool) != APR_SUCCESS) {
        throw "アイテムのキャッシュファイルを開けませんでした．";
    }

    if (apr_file_info_get(&info, APR_FINFO_SIZE, cache_file) != APR_SUCCESS) {
        throw "キャッシュファイルのサイズを取得できませんでした．";
    }

    if (apr_mmap_create(&file_map, cache_file,
                        0, static_cast<apr_size_t>(info.size),
                        APR_MMAP_READ, tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの map に失敗しました．";
    }

    APR_PCALLOC(*paths, char *, data_pool_, static_cast<apr_size_t>(info.size+1));

    memcpy(*paths, file_map->mm, static_cast<apr_size_t>(info.size));

    data = *paths;
    while (*data != '\0') {
        if (*data == PATH_CACHE_DELIMITER) {
            *data = '\0';

            (*number)++;
        }

        data++;
    }

    if (apr_mmap_delete(file_map) != APR_SUCCESS) {
        throw "キャッシュファイルの unmap に失敗しました．";
    }

    apr_file_close(cache_file);
}

void UploadItemList::read_header_cache(apr_pool_t *tmp_pool,
                                       UploadItem::header **headers,
                                       apr_size_t *number)
{
    apr_file_t *cache_file;
    apr_mmap_t *file_map;
    apr_finfo_t info;

    *number = 0;

    if (apr_file_open(&cache_file, header_cache_path_,
                      APR_READ|APR_BINARY,
                      APR_OS_DEFAULT, tmp_pool) != APR_SUCCESS) {
        throw "アイテムのキャッシュファイルを開けませんでした．";
    }

    if (apr_file_info_get(&info, APR_FINFO_SIZE, cache_file) != APR_SUCCESS) {
        throw "キャッシュファイルのサイズを取得できませんでした．";
    }

    if ((info.size % sizeof(UploadItem::header)) != 0) {
        throw "キャッシュファイルのサイズが不正です．";
    }

    *number = static_cast<apr_size_t>(info.size) / sizeof(UploadItem::header);

    if (apr_mmap_create(&file_map, cache_file,
                        0, static_cast<apr_size_t>(info.size),
                        APR_MMAP_READ, tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの map に失敗しました．";
    }

    APR_PALLOC(*headers, UploadItem::header *, data_pool_, static_cast<apr_size_t>(info.size));

    memcpy(*headers, file_map->mm, static_cast<apr_size_t>(info.size));

    if (apr_mmap_delete(file_map) != APR_SUCCESS) {
        throw "キャッシュファイルの unmap に失敗しました．";
    }

    apr_file_close(cache_file);
}

void UploadItemList::write_cache()
{
    apr_pool_t *tmp_pool;

    if (apr_pool_create(&tmp_pool, pool_) != APR_SUCCESS) {
        throw "メモリの確保に失敗しました．";
    }

    write_header_cache(tmp_pool);
    write_path_cache(tmp_pool);

    apr_pool_destroy(tmp_pool);
}

void UploadItemList::write_path_cache(apr_pool_t *tmp_pool)
{
    const char *tmp_cache_path;
    apr_file_t *cache_file;
    item_iterator i;

    tmp_cache_path = get_tmp_path_cache_path(tmp_pool, &cache_file);

    for (i = begin(); i != end(); i++) {
        apr_file_printf(cache_file, "%s%c",
                        (*i)->file_path, PATH_CACHE_DELIMITER);
    }

    apr_file_close(cache_file);

    if (apr_file_rename(tmp_cache_path, path_cache_path_, tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの rename に失敗しました．";
    }
}

void UploadItemList::write_header_cache(apr_pool_t *tmp_pool)
{
    const char *tmp_cache_path;
    apr_file_t *cache_file;
    apr_off_t offset;
    apr_mmap_t *file_map;
    UploadItem::header *header;
    item_iterator i;

    tmp_cache_path = get_tmp_header_cache_path(tmp_pool, &cache_file);

    if (!empty()) {
        offset = sizeof(UploadItem::header)*size() - 1;
        if (apr_file_seek(cache_file, APR_SET, &offset) != APR_SUCCESS) {
            throw "キャッシュファイルの seek に失敗しました．";
        }

        if (apr_file_putc('*', cache_file) != APR_SUCCESS) {
            throw "キャッシュファイルの write に失敗しました．";
        }

        if (apr_mmap_create(&file_map, cache_file,
                            0, sizeof(UploadItem::header)*size(),
                            APR_MMAP_READ|APR_MMAP_WRITE, tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの map に失敗しました．";
        }

        header = static_cast<UploadItem::header *>(file_map->mm);
        for (i = begin(); i != end(); i++) {
            memcpy(header++, (*i)->header, sizeof(UploadItem::header));
        }

        if (apr_mmap_delete(file_map) != APR_SUCCESS) {
            throw "キャッシュファイルの unmap に失敗しました．";
        }
    }

    apr_file_close(cache_file);

    if (apr_file_rename(tmp_cache_path, header_cache_path_, tmp_pool) != APR_SUCCESS) {
        throw "キャッシュファイルの rename に失敗しました．";
    }
}

void UploadItemList::sort()
{
    if (!ilist_.empty()) {
        ilist_.sort(ItemCompare());
    }
}


/******************************************************************************
 * テスト
 *****************************************************************************/
#ifdef DEBUG_UploadItemList
#include "apr_general.h"

const apr_size_t TOTAL_NUMBER_LIMIT = 100;
const apr_size_t TOTAL_SIZE_LIMIT   = 1024*1024*1024;

void usage(const char *prog_name)
{
    cerr << "Usage: " << prog_name << " <DIRECTORY>" << endl;
}

int main(int argc, const char * const *argv)
{
    apr_pool_t *pool;

    apr_app_initialize(&argc, &argv, NULL);
    apr_pool_create(&pool, NULL);

    try {
        if (argc != 2) {
            throw "ファイル用ディレクトリが指定されていません．";
        }

        auto_ptr<UploadItemList> item_list(UploadItemList::load(pool, argv[1], argv[1],
                                                                TOTAL_NUMBER_LIMIT, TOTAL_SIZE_LIMIT));

        UploadItemList::dump_list(item_list.get());
        item_list->to_array(pool, 0, item_list->size());
    } catch(const char *message) {
        cerr << "Error: " << message << endl;
        usage(argv[0]);

        return EXIT_FAILURE;
    }

    apr_terminate();

    return EXIT_SUCCESS;
}
#endif

// Local Variables:
// mode: c++
// buffer-file-coding-system: euc-japan-dos
// End:
