/***************************************************************************
                             -------------------
    cvs         :
    begin       : Sat Aug 10 2002
    copyright   : (C) 2002, 2003 by Fabian Kaiser and Christian Stimming
    email       : fabian@openhbci.de

 ***************************************************************************
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Lesser General Public            *
 *   License as published by the Free Software Foundation; either          *
 *   version 2.1 of the License, or (at your option) any later version.    *
 *                                                                         *
 *   This library 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     *
 *   Lesser General Public License for more details.                       *
 *                                                                         *
 *   You should have received a copy of the GNU Lesser General Public      *
 *   License along with this library; if not, write to the Free Software   *
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston,                 *
 *   MA  02111-1307  USA                                                   *
 *                                                                         *
 ***************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "ktoblzcheck.h"
#include <fstream>
#include <iostream>
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring> // for strchr()
#ifdef HAVE_DIRENT_H
# include <sys/types.h>
# include <dirent.h>
# include <errno.h>
#endif
#ifdef HAVE_WINDOWS_H
# include <windows.h>
# include <tchar.h>
#endif

// The actual algorithms for number checking are there
#include "algorithms.h"

// This file has the implementations for the class methods. C wrappers
// are in accnum.cc.

#if defined(__MINGW32__) || defined(_WIN32)
# ifndef OS_WIN32
#  define OS_WIN32
# endif
#endif

#ifdef OS_WIN32
# define DIRSEP "\\"
#else
# define DIRSEP "/"
// asctime() doesn't work on win32 for whatever reason
# define HAVE_ASCTIME
#endif

#ifdef ERROR
# undef ERROR
#endif

using namespace std;

// A hard-coded replacement for strptime() because that one is not
// available on windows/mingw.
static void extract_date(const std::string &d, struct tm *p_tm)
{
    char a[5];

    // Make sure the given struct tm is filled with all zero.
    memset(p_tm, 0, sizeof(struct tm));

    // Year
    a[0] = d[0];
    a[1] = d[1];
    a[2] = d[2];
    a[3] = d[3];
    a[4] = '\0';
    p_tm->tm_year = atol(a) - 1900;
    //  cerr << "tm_year=" << p_tm->tm_year << " ";

    // Month
    a[0] = d[4];
    a[1] = d[5];
    a[2] = '\0';
    p_tm->tm_mon = atol(a) - 1;
    //  cerr << "tm_mon=" << p_tm->tm_mon << " ";

    // Day of month
    a[0] = d[6];
    a[1] = d[7];
    p_tm->tm_mday = atol(a);
    //  cerr << "tm_mday=" << p_tm->tm_mday << " ";
}

/// Returns true if the given string starts with the given beginning
static bool starts_with(const std::string &str, const std::string &beginning)
{
    return (str.size() >= beginning.size())
           && (str.substr(0, beginning.size()) == beginning);
}

/// Returns false if the given string ends with the given ending.
static bool ends_with(const std::string &str, const std::string &ending)
{
    return (str.size() >= ending.size())
           && (str.substr(str.size() - ending.size(), str.size()) == ending);
}

/// Predicate for the file names that we accept
static bool accept_filename(const std::string &f)
{
    return (f.size() == std::string("bankdata_20051234.txt").size())
           && starts_with(f, "bankdata_")
           && ends_with(f, ".txt");
}

#ifndef ONLY_EXTRACT_DATE_TEST
const char *AccountNumberCheck::stringEncoding()
{
    return "ISO-8859-15";
}

const char *AccountNumberCheck::libraryVersion()
{
    return VERSION;
}

std::string AccountNumberCheck::bankdata_dir()
{
    return algorithms_get_bankdata_dir();
}

AccountNumberCheck::Record::Record()
{
}

AccountNumberCheck::Record::Record(unsigned long id, const string &meth, const string &name,
                                   const string &loc)
    : bankId(id)
    , method(meth)
    , bankName(name)
    , location(loc)
{
}

AccountNumberCheck::Record::Record(const char *id, const char *meth, const char *name,
                                   const char *loc)
    : bankId(id ? atol(id) : 0)
    , method(meth ? meth : "")
    , bankName(name ? name : "")
    , location(loc ? loc : "")
{
}

std::string AccountNumberCheck::resultToString(Result r)
{
    switch (r) {
    case AccountNumberCheck::OK:
        return "Ok";
    case AccountNumberCheck::ERROR:
        return "ERROR: account and bank do not match";
    case AccountNumberCheck::BANK_NOT_KNOWN:
        return "Bank is unknown";
    default:
    case AccountNumberCheck::UNKNOWN:
        return "Validation algorithm unknown";
    }
}

// ////////////////////////////////////////

AccountNumberCheck::AccountNumberCheck()
    : data() // std::map doesn't take size as argument
{
    init_datafile_list();

    // New implementation; use dated bankdata files per
    // default. However, this of course only works if we found the
    // files beforehand.
    if (!dated_files.empty()) {
        assert(!dated_files.empty());
        FileDaterange file = find_closest_datafile(time(NULL));
        readDatedFile(file);
    }
}

std::string AccountNumberCheck::getFilenameClosestDateToday() const
{
    if (!dated_files.empty()) {
        assert(!dated_files.empty());
        FileDaterange file = find_closest_datafile(time(NULL));
        return file.first;
    } else {
        return "";
    }
}

AccountNumberCheck::AccountNumberCheck(const string &filename)
    : data() // std::map doesn't take size as argument
{
    init_datafile_list();

    readDatedFile(make_pair(filename, make_pair(0, 0)));
}

void AccountNumberCheck::init_datafile_list()
{
    populate_dated_files(bankdata_dir(), false);

    // No files found? Maybe in the compiled-in installation location?
    if (dated_files.empty()) {
#ifdef BANKDATA_PATH
        populate_dated_files(BANKDATA_PATH, false);
#endif

        // If there are still no files found, try again reading but now
        // print the file-not-found error messages.
        if (dated_files.empty()) {
            populate_dated_files(bankdata_dir(), true);
#ifdef BANKDATA_PATH
            populate_dated_files(BANKDATA_PATH, true);
#endif
        }
    }
}

AccountNumberCheck::~AccountNumberCheck()
{
    deleteList();
}

#define BLZ_SIZE    (8+1)
#define METHOD_SIZE (2+1)
#define NAME_SIZE   (58+1)
#define PLACE_SIZE  (35+1)
// From 2006-06-05 onwards the field "PLACE" has 35 instead of 29
// characters!

//#define ARRAY_BOUNDS_CHECK 1
#undef ARRAY_BOUNDS_CHECK

#ifdef ARRAY_BOUNDS_CHECK
#  define PLUSONE +1
#else
#  define PLUSONE
#endif

void
AccountNumberCheck::readFile(const string &filename)
{
    // First clear existing data
    if (data.size() > 0) {
        deleteList();
    }

    //cerr << "Will now read file: " << filename << std::endl;

    // Now read file
    FILE *istr = fopen(filename.c_str(), "r");
    // FIXME: Do a lot of error checking here.
    if (!istr) {
        int errsv = errno;
        std::string errstr = strerror(errsv);
        std::cerr << "AccountNumberCheck::readFile: File " << filename
                  << " could not be opened: " << errstr
                  << "\nAccountNumberCheck could not obtain bank data." << std::endl;
        return;
    }

    // We better use heap-allocated arrays because some weird stack
    // corruption has been reported with ktoblzcheck-1.8 on Debian.
    char *blz = new char[BLZ_SIZE PLUSONE];
    char *method = new char[METHOD_SIZE PLUSONE];
    char *name = new char[NAME_SIZE PLUSONE];
    char *place = new char[PLACE_SIZE PLUSONE];
#ifdef ARRAY_BOUNDS_CHECK
    blz[BLZ_SIZE] = 17;
    method[METHOD_SIZE] = 17;
    name[NAME_SIZE] = 17;
    place[PLACE_SIZE] = 17;
#endif

    while (fgets(blz, BLZ_SIZE, istr))
    {
        if (fgetc(istr) == EOF) {
            break;                  // remove delimiter
        }
        if (!fgets(method, METHOD_SIZE, istr)) {
            break;                                 // get method
        }
        if (fgetc(istr) == EOF) {
            break;                  // remove delimiter
        }
        if (!fscanf(istr, "%58[^\t]\t%35[^\t\n]", name, place)) {
            break;
        }

        // Create new record object
        Record *newRecord
            = new Record(blz, method, name, place);

        // Insert this always at the end, since the file is sorted by
        // ascending BLZ
        data.insert(data.end(), banklist_type::value_type(newRecord->bankId, newRecord));
        if (fgetc(istr) == EOF) {
            break;                  // remove delimiter
        }
#ifdef ARRAY_BOUNDS_CHECK
        assert(blz[BLZ_SIZE] == 17);
        assert(method[METHOD_SIZE] == 17);
        assert(name[NAME_SIZE] == 17);
        assert(place[PLACE_SIZE] == 17);
#endif
    }
    delete[] blz;
    delete[] method;
    delete[] name;
    delete[] place;
    fclose(istr);
}

void
AccountNumberCheck::deleteList()
{
    for (banklist_type::iterator iter = data.begin(); iter != data.end(); iter++) {
        delete iter->second;
    }
    data.clear();
}

unsigned int AccountNumberCheck::bankCount() const
{
    return data.size();
}

void AccountNumberCheck::createIndex()
{
    // not yet implemented; for std::map this isn't necessary anyway.
}

// Function object for the predicate of matching a job result
#if 0
// currently unused; was only used for std::vector et al
class MatchBlz
{
    unsigned long blz;
public:
    MatchBlz(unsigned long bankId)
        : blz(bankId)
    {
    }

    bool operator()(const AccountNumberCheck::Record *r)
    {
        return r && (r->bankId == blz);
    }
};
#endif

const AccountNumberCheck::Record &
AccountNumberCheck::findBank(const string &bankId) const
{
    unsigned long lbankId = atol(bankId.c_str());
    banklist_type::const_iterator iter;

    // Lookup the object
    iter = data.find(lbankId);

    // Did we find it? Yes, return the reference.
    if (iter != data.end()) {
        return *(iter->second);
    } else {
        throw -1;
    }
}

AccountNumberCheck::Result
AccountNumberCheck::check(const string &bankId, const string &accountId,
                          const string &given_method) const
{
    int account[10] = {9, 1, 3, 0, 0, 0, 0, 2, 0, 1};
    int weight[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

    string method = given_method;

    if (method.empty()) {
        try {
            Record rec = findBank(bankId);
            method = rec.method;
        } catch (int) {
            // bank was not found, return error if not forced to use a spec. method
            return BANK_NOT_KNOWN;
        }
    }

    // Check account-id length
    if (accountId.size() > 10 || bankId.size() > 8) {
        return ERROR;
    }

    // Convert string to array of integers
    number2Array(accountId, account);

    // Note: Originally, the program code for the method was chosen by
    // the "giant if-statement". I thought that this is quite a
    // performance bottleneck, so I changed the code to make one small
    // function for each method, and its lookup is done through a
    // std::map hash table. However, this sped up the number checking
    // only by roughly 15%, so it probably wasn't worth the effort :-(
    // -- cstim, 2005-01-16

    // Initialize method map, if it has been empty
    if (method_map.empty()) {
        const_cast<AccountNumberCheck *>(this)->initMethodMap();
    }

    // Find method function
    method_map_t::const_iterator iter = method_map.find(method);
    // Did we find it? Yes, use the returned function.
    if (iter != method_map.end()) {
        return (iter->second)(account, weight);
    }

    // If it hasnt been found, try the second map for methods that also
    // want the accountId string.
    method_map2_t::const_iterator iter2 = method_map2.find(method);
    // Did we find it? Yes, use the returned function.
    if (iter2 != method_map2.end()) {
        return (iter2->second)(account, weight, accountId, bankId);
    }

    std::cerr << "AccountNumberCheck::check: Specified method '" << method
              << "' is unknown." << std::endl;
    return UNKNOWN;
}

// ////////////////////////////////////////
// Bankdata date validity

typedef std::vector<std::string> StringList;

static StringList all_filenames_from_dir(const std::string &dir, bool print_errormsgs)
{
    StringList result;
#ifdef HAVE_WINDOWS_H
    HANDLE hFind = INVALID_HANDLE_VALUE;
    WIN32_FIND_DATA ffd;
    std::string mydir = dir + "\\*";
    hFind = FindFirstFile(mydir.c_str(), &ffd);
    // List all the files in the directory with some info about them.

    do
    {
        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            continue;
        result.push_back(ffd.cFileName);
    } while (FindNextFile(hFind, &ffd) != 0);

    FindClose(hFind);

#else // HAVE_WINDOWS_H
# ifdef HAVE_DIRENT_H
    DIR *dp = opendir(dir.c_str());
    if (dp) {
        struct dirent *ep;
        while ((ep = readdir(dp)))
        {
            result.push_back(ep->d_name);
            //cerr << "Found dir entry name " << result.back() << std::endl;
        }
        closedir(dp);
    } else {
        int errsv = errno;
        std::string errstr = strerror(errsv);
        if (print_errormsgs) {
            cerr << "Could not read directory \"" << dir << "\": " << errstr << std::endl;
        }
    }
# else // HAVE_DIRENT_H
#  error \
    "OS is not yet supported: Neither POSIX <dirent.h> nor MS windows <windows.h> is available!"
# endif // HAVE_DIRENT_H
#endif // HAVE_WINDOWS_H
    return result;
}

static StringList
lookup_files_from_dir(const std::string &dir, bool (*filename_predicate)(
                          const std::string &), bool print_errormsgs)
{
    // there is surely a STL algorithm for this, but I forgot which
    // one it is (such as copy_if)
    StringList raw = all_filenames_from_dir(dir, print_errormsgs);
    StringList result;
    for (StringList::const_iterator iter = raw.begin();
         iter != raw.end(); ++iter) {
        if (filename_predicate(*iter)) {
            result.push_back(*iter);
            //cerr << "Adding entry name " << result.back() << std::endl;
        }
    }
    return result;
}

void AccountNumberCheck::populate_dated_files(const std::string &dirname, bool print_errormessages)
{
    dated_files.clear();

#if !defined(__MINGW32__) | !defined(_WIN32)
    // Set the timezone information hard to CET (Central European Time)
    // because that's what specifies the (German, after all) bankdata.
    char *old_TZ_p = getenv("TZ");
    std::string old_TZ(old_TZ_p ? old_TZ_p : "");
    //cout << "Old TZ: " << old_TZ << endl;
    setenv("TZ", "CET", 1); // setenv() unavailable of windows/mingw
#endif
    tzset();
    // and reset the env variable TZ again.
#if !defined(__MINGW32__) | !defined(_WIN32)
    if (old_TZ_p) {
        setenv("TZ", old_TZ.c_str(), 1);
    } else {
        unsetenv("TZ");
    }
#endif

    // This method now looks up the available files from the bankdata
    // directory. Previously, we had a hard-coded list of available
    // files.
    StringList file_names = lookup_files_from_dir(dirname, &accept_filename,
                                                  print_errormessages);
    // Also sort alphabetically before using this
    std::sort(file_names.begin(), file_names.end());

    if (file_names.empty() && print_errormessages) {
        std::cerr << "Oops, no bank data file was found in directory \"" << dirname
                  << "\"! The ktoblzcheck library will not work." << std::endl;
    }

    StringList::const_iterator iter = file_names.begin();
    while (iter != file_names.end())
    {
        string filename = *iter;
        ++iter;

        //cerr << "Adding file " << filename << endl;

        // Use the part directly after the '_' character as the date
        struct tm tmp_tm;
        {
            const char *found_char = strchr(filename.c_str(), '_');
            if (!found_char || *found_char == '\0') {
                break;
            }
            extract_date(std::string(found_char+1), &tmp_tm);
        }

        // Point to the beginning of day (don't forget this as tmp_tm is
        // uninitialized so far!)
        tmp_tm.tm_hour = 0;
        tmp_tm.tm_min = 0;
        tmp_tm.tm_sec = 0;
        //cerr << "Result of strptime: " << asctime(&tmp_tm) << endl;
        time_t start_date = mktime(&tmp_tm);
        //cerr << "Result of mktime: " << ctime(&start_date) << endl;
        if (start_date == -1) {
            std::cerr
                << "Error on adding dated file to list: Start date could not be parsed. Filename "
                << filename << endl;
            // This is a die-hard workaround: If the list is empty so
            // far, and this is the last file in the list, we are about
            // to crash because we couldn't find any bankdata. To avoid
            // this crash, we just make up a date interval in order to
            // use at least this very last file.
            if (dated_files.empty() && iter == file_names.end()) {
                time_t now = time(NULL);
                // We make up a date interval on our own: Let's guess the
                // starting date is 1 month in the past.
                start_date = now - 86400 * 30;
                // Simply guess an end date: 3 months in the future
                time_t end_date = start_date + 86400 * 30 * 3;
                std::cerr
                    <<
                    "Falling back to assumption that the last file is valid today; filename "
                    << filename
                    << ". Please report this error of ktoblzcheck "
                    << KTOBLZCHECK_VERSION_MAJOR << "." << KTOBLZCHECK_VERSION_MINOR
                    << " to aqbanking-devel@lists.sourceforge.net" << std::endl;
                dated_files.push_back(make_pair(dirname + DIRSEP + string(filename),
                                                make_pair(start_date, end_date)));
            }
            continue;
        }

        // Simply guess an end date: 3 months in the future
        time_t end_date = start_date + 86400 * 30 * 3;

        if (dated_files.size() > 0) {
            // There is a previous entry. Set our (start date - 1 sec) as
            // end date of the previous entry.
            dated_files.back().second.second = start_date - 1;
        }
        dated_files.push_back(make_pair(dirname + DIRSEP + string(filename),
                                        make_pair(start_date, end_date)));
    }

    // Reset the tset() variables to the original state again.
    tzset();
}

const AccountNumberCheck::FileDaterange &
AccountNumberCheck::find_closest_datafile(time_t date) const
{
    assert(!dated_files.empty());
    DatedFiles::const_iterator iter = dated_files.begin();

    // Is the given date already earlier than first starting date?
    if (date < iter->second.first) {
        return *iter;
    }

    for (; iter != dated_files.end(); ++iter) {
        // For each file, check whether the given date is earlier than
        // the ending date.
        if (date < iter->second.second) {
            return *iter;
        }
    }

    // Still nothing found? Then use the last element.
    return dated_files.back();
}

bool AccountNumberCheck::isDataValidForDate(time_t date) const
{
    return date >= data_valid_start && date <= data_valid_end;
}

bool AccountNumberCheck::isValidDataAvailable(time_t date) const
{
    if (isDataValidForDate(date)) {
        return true;
    }

    if (dated_files.empty()) {
        return false;
    }

    DatedFiles::const_iterator iter = dated_files.begin();

    // Is the given date already earlier than first starting date?
    if (date < iter->second.first) {
        return false;
    }

    for (; iter != dated_files.end(); ++iter) {
        // For each file, check whether the given date is earlier than
        // the ending date.
        if (date < iter->second.second) {
            return true;
        }
    }

    // Still nothing found? Then no matching file is there.
    return false;
}

time_t AccountNumberCheck::closestValidData(time_t date) const
{
    if (dated_files.empty()) {
        return 0;
    }

    DatedFiles::const_iterator iter = dated_files.begin();

    // Is the given date already earlier than first starting date?
    if (date < iter->second.first) {
        return iter->second.first;
    }

    for (; iter != dated_files.end(); ++iter) {
        // For each file, check whether the given date is earlier than
        // the ending date.
        if (date < iter->second.second) {
            return date;
        }
    }

    // Still nothing found? Then no matching file is there and we just
    // use the last date.
    return dated_files.back().second.second;
}

bool AccountNumberCheck::loadDataForDate(time_t date)
{
    if (dated_files.empty()) {
        return false;
    }

    assert(!dated_files.empty());
    FileDaterange file = find_closest_datafile(date);

    // Filename string has already prepended the bankdata directory

    readDatedFile(file);
    return isDataValidForDate(date);
}

void AccountNumberCheck::readDatedFile(const FileDaterange &filename)
{
    readFile(filename.first);
    // Make sure to set the dates here as well!
    data_valid_start = filename.second.first;
    data_valid_end = filename.second.second;
}

// ////////////////////////////////////////////////////////////

#else // ONLY_EXTRACT_DATE_TEST

int rv = 0;

// This is a small unittest for the extract_date() function at the
// top, because this seems to fail on Mac OS X and this should at
// least be discovered by make check.
int main(int argc, char **argv)
{
    std::string filename = "bankdata_20090102.txt";

    if (!starts_with(filename, "bankdata_")) {
        rv = -1;
    }
    if (!ends_with(filename, ".txt")) {
        rv = -1;
    }
    if (!accept_filename(filename)) {
        rv = -1;
    }

    // Use the part directly after the '_' character as the date
    struct tm tmp_tm;
    extract_date(strchr(filename.c_str(), '_')+1, &tmp_tm);

    // Point to the beginning of day (don't forget this as tmp_tm is
    // uninitialized so far!)
    tmp_tm.tm_hour = 0;
    tmp_tm.tm_min = 0;
    tmp_tm.tm_sec = 0;
#ifdef HAVE_ASCTIME
    // asctime() doesn't work on win32 for whatever reason
    cerr << "Result of extract_date: " << asctime(&tmp_tm) << endl;
#endif
    time_t start_date = mktime(&tmp_tm);
    cerr << "Result of mktime: " << ctime(&start_date) << endl;

    if (start_date == -1) {
        cerr << "extract_date and/or mktime() failed. From "
#ifdef HAVE_ASCTIME
        // asctime() doesn't work on win32 for whatever reason
        "extract_date we got \"" << asctime(&tmp_tm) << "\" and from "
#endif
        "mktime() we got " << ctime(&start_date) << endl;
        rv = -1;
    }
    return rv;
}

#endif // ONLY_EXTRACT_DATE_TEST

// The code for the actual methods is now in methods.cc. For adding a
// method, see the comment at the beginning of that file.
