// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/input/readers/binary/Binary.h"

#include <sys/stat.h>

#include "zeek/input/readers/binary/binary.bif.h"
#include "zeek/threading/SerialTypes.h"

using namespace std;
using zeek::threading::Field;
using zeek::threading::Value;

namespace zeek::input::reader::detail {

streamsize Binary::chunk_size = 0;

Binary::Binary(ReaderFrontend* frontend) : ReaderBackend(frontend), in(nullptr), mtime(0), ino(0), firstrun(true) {
    if ( ! chunk_size ) {
        chunk_size = BifConst::InputBinary::chunk_size;

        if ( ! chunk_size )
            chunk_size = 1024;
    }
}

Binary::~Binary() { DoClose(); }

void Binary::DoClose() {
    if ( in )
        CloseInput();
}

bool Binary::OpenInput() {
    in = new ifstream(fname.c_str(), ios_base::in | ios_base::binary);

    if ( in->fail() ) {
        Error(Fmt("Init: cannot open %s", fname.c_str()));
        return false;
    }

    return true;
}

bool Binary::CloseInput() {
    if ( ! in || ! in->is_open() ) {
        InternalWarning(Fmt("Trying to close closed file for stream %s", fname.c_str()));
        return false;
    }

#ifdef DEBUG
    Debug(DBG_INPUT, "Binary reader starting close");
#endif

    in->close();
    delete in;
    in = nullptr;

#ifdef DEBUG
    Debug(DBG_INPUT, "Binary reader finished close");
#endif

    return true;
}

bool Binary::DoInit(const ReaderInfo& info, int num_fields, const Field* const* fields) {
    in = nullptr;
    mtime = 0;
    ino = 0;
    firstrun = true;

    path_prefix.assign((const char*)BifConst::InputBinary::path_prefix->Bytes(),
                       BifConst::InputBinary::path_prefix->Len());

    if ( ! info.source || strlen(info.source) == 0 ) {
        Error("No source path provided");
        return false;
    }

    if ( num_fields != 1 ) {
        Error(
            "Filter for binary reader contains more than one field. Filters "
            "for binary reader must contain exactly one string field. "
            "Filter ignored.");
        return false;
    }

    if ( fields[0]->type != TYPE_STRING ) {
        Error("Filter for binary reader contains a non-string field.");
        return false;
    }

    // do initialization
    fname = info.source;

    // Handle path-prefixing. See similar logic in Ascii::OpenFile().
    if ( fname.front() != '/' && ! path_prefix.empty() ) {
        string path = path_prefix;
        std::size_t last = path.find_last_not_of('/');

        if ( last == string::npos ) // Nothing but slashes -- weird but ok...
            path = "/";
        else
            path.erase(last + 1);

        fname = path + "/" + fname;
    }

    if ( ! OpenInput() )
        return false;

    if ( UpdateModificationTime() == -1 )
        return false;

#ifdef DEBUG
    Debug(DBG_INPUT, "Binary reader created, will perform first update");
#endif

    // after initialization - do update
    DoUpdate();

#ifdef DEBUG
    Debug(DBG_INPUT, "Binary reader did first update");
#endif

    return true;
}

streamsize Binary::GetChunk(char** chunk) {
    if ( in->peek() == std::iostream::traits_type::eof() )
        return 0;

    if ( in->eof() == true || in->fail() == true )
        return 0;

    *chunk = new char[chunk_size];

    in->read(*chunk, chunk_size);

    streamsize bytes_read = in->gcount();

    if ( ! bytes_read ) {
        delete[] *chunk;
        *chunk = nullptr;
        return 0;
    }

    // probably faster to just not resize if bytes_read < chunk_size, since
    // length of valid data is known

    return bytes_read;
}

int Binary::UpdateModificationTime() {
    struct stat sb;

    if ( stat(fname.c_str(), &sb) == -1 ) {
        Error(Fmt("Could not get stat for %s", fname.c_str()));
        return -1;
    }

    if ( sb.st_ino == ino && sb.st_mtime == mtime )
        // no change
        return 0;

    mtime = sb.st_mtime;
    ino = sb.st_ino;
    return 1;
}

// read the entire file and send appropriate thingies back to InputMgr
bool Binary::DoUpdate() {
    if ( firstrun )
        firstrun = false;

    else {
        switch ( Info().mode ) {
            case MODE_REREAD: {
                switch ( UpdateModificationTime() ) {
                    case -1: return false; // error
                    case 0: return true;   // no change
                    case 1: break;         // file changed. reread.
                    default: assert(false);
                }
                // fallthrough
            }

            case MODE_MANUAL:
            case MODE_STREAM:
                if ( Info().mode == MODE_STREAM && in ) {
                    in->clear(); // remove end of file evil bits
                    break;
                }

                CloseInput();

                if ( ! OpenInput() )
                    return false;

                break;

            default: assert(false);
        }
    }

    char* chunk = nullptr;
    streamsize size = 0;
    while ( (size = GetChunk(&chunk)) ) {
        assert(NumFields() == 1);

        Value** fields = new Value*[1];

        // filter has exactly one text field. convert to it.
        Value* val = new Value(TYPE_STRING, true);
        val->val.string_val.data = chunk;
        val->val.string_val.length = size;
        fields[0] = val;

        if ( Info().mode == MODE_STREAM )
            Put(fields);
        else
            SendEntry(fields);
    }

    if ( Info().mode != MODE_STREAM )
        EndCurrentSend();

#ifdef DEBUG
    Debug(DBG_INPUT, "DoUpdate finished successfully");
#endif

    return true;
}

bool Binary::DoHeartbeat(double network_time, double current_time) {
    switch ( Info().mode ) {
        case MODE_MANUAL:
            // yay, we do nothing :)
            break;

        case MODE_REREAD:
        case MODE_STREAM:
#ifdef DEBUG
            Debug(DBG_INPUT, "Starting Heartbeat update");
#endif
            Update(); // call update and not DoUpdate, because update
                      // checks disabled.
#ifdef DEBUG
            Debug(DBG_INPUT, "Finished with heartbeat update");
#endif
            break;
        default: assert(false);
    }

    return true;
}

} // namespace zeek::input::reader::detail
