/***************************************************************************
                             -------------------
    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);
   }
}

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()
{
   algorithms_init_binreloc();

   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;
    hFind = FindFirstFile(dir.c_str(), &ffd);
    // List all the files in the directory with some info about them.

    do
    {
        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            _tprintf(TEXT("  %s   <DIR>\n"), ffd.cFileName);
        }
        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.
