/*
 * iocli.cpp: low-level file and fd access functions for CLI spigot.
 */

#include <stdio.h>
#include <string.h>

#include <string>
#include <sstream>
#include <memory>
using std::string;
using std::ostringstream;

#include "spigot.h"
#include "error.h"
#include "io.h"

class CLIFileAccessContext : public FileAccessContext {
    virtual unique_ptr<FileOpener> file_opener(const string &fname) override;
    virtual unique_ptr<FileOpener> fd_opener(int fd) override;
    virtual unique_ptr<FileOpener> stdin_opener() override;
};

unique_ptr<FileAccessContext> make_cli_file_access_context()
{
    return make_unique<CLIFileAccessContext>();
}

class StdioStreamReaderRaw : public FileReaderRaw {
    string name; // for throwing io_errors
    virtual void close_inner() override;
    virtual size_t read_inner(void *buf, size_t size) override;

  protected:
    FILE *fp;

  public:
    StdioStreamReaderRaw(FILE *fp_, const string &name_);
};

StdioStreamReaderRaw::StdioStreamReaderRaw(FILE *fp_, const string &name_)
    : name(name_), fp(fp_)
{
}

void StdioStreamReaderRaw::close_inner()
{
    if (fp) {
        fclose(fp);
        fp = nullptr;
    }
}

size_t StdioStreamReaderRaw::read_inner(void *buf, size_t size)
{
    if (!fp)
        return 0;
    int c = getc(fp);
    if (c == EOF) {
        if (ferror(fp))
            throw io_error("unable to read from " + name +
                           ": " + strerror(errno));
        return 0;
    }
    *(char *)buf = c;
    return 1;
}

class StdioFileOpener : public FileOpener {
    string filename;

    virtual BufferingReaderCacheKey key() const override;
    virtual string name() const override;
    virtual unique_ptr<FileReaderRaw> open() const override;

  public:
    StdioFileOpener(string filename_);
};

StdioFileOpener::StdioFileOpener(string filename_) 
    : filename(filename_)
{
}

BufferingReaderCacheKey StdioFileOpener::key() const
{
    return BufferingReaderCacheKey(FileType::File, filename);
}

string StdioFileOpener::name() const
{
    return filename;
}

unique_ptr<FileReaderRaw> StdioFileOpener::open() const
{
    FILE *fp = fopen(filename.c_str(), "r");
    string name = "'" + filename + "'";
    if (!fp) {
        throw io_error("unable to open ", name, ": ",
                       strerror(errno));
    }
    return make_unique<StdioStreamReaderRaw>(fp, name);
}

unique_ptr<FileOpener> CLIFileAccessContext::file_opener(const string &name)
{
    return make_unique<StdioFileOpener>(name);
}

#ifdef HAVE_FDS

#include <signal.h>

#include <termios.h>
#include <unistd.h>

bool set_term_modes = false;

class FdReaderRaw : public StdioStreamReaderRaw {
    int fd;
    bool got_orig_term_modes = false;
    bool need_to_restore_term_modes = false;
    struct termios orig_term_modes;

    virtual void setup_inner(SpecialSetup sstype) override;
    virtual void cleanup_inner() override;

  public:
    FdReaderRaw(int fd, const string &name);
};

FdReaderRaw::FdReaderRaw(int fd, const string &name)
    : StdioStreamReaderRaw(nullptr, name)
{
    fp = fdopen(fd, "r");
    if (!fp)
        throw io_error("unable to access fd ", fd,
                       " for reading: ", strerror(errno));

    if (tcgetattr(fd, &orig_term_modes) == 0)
        got_orig_term_modes = true;
}

void FdReaderRaw::setup_inner(SpecialSetup sstype)
{
    if (sstype == SpecialSetup::TermModes &&
        got_orig_term_modes) {
        struct termios new_term_modes = orig_term_modes; // structure copy
        new_term_modes.c_lflag &= ~ICANON;
        new_term_modes.c_lflag &= ~ECHO;
        if (tcsetattr(fd, TCSANOW, &new_term_modes) == 0)
            need_to_restore_term_modes = true;
    }
}

void FdReaderRaw::cleanup_inner()
{
    if (need_to_restore_term_modes) {
        tcsetattr(fd, TCSANOW, &orig_term_modes);
        need_to_restore_term_modes = false;
    }
}

class FdOpener : public FileOpener {
    int fd;
    string fdname;

    virtual BufferingReaderCacheKey key() const override;
    virtual string name() const override;
    virtual unique_ptr<FileReaderRaw> open() const override;

  public:
    FdOpener(int fd_);
};

FdOpener::FdOpener(int fd_)
    : fd(fd_)
{
    ostringstream s;
    s << fd;
    fdname = s.str();
}

BufferingReaderCacheKey FdOpener::key() const
{
    return BufferingReaderCacheKey(FileType::Fd, fdname);
}

string FdOpener::name() const
{
    return "<fd " + fdname + ">";
}

unique_ptr<FileReaderRaw> FdOpener::open() const
{
    return make_unique<FdReaderRaw>(fd, name());
}

static void sighandler(int sig)
{
    io_cleanup();
    signal(sig, SIG_DFL);
    raise(sig);
}

void begin_fd_input()
{
    if (!set_term_modes)
        return;

    signal(SIGINT, sighandler);
    signal(SIGTERM, sighandler);
    atexit(io_cleanup);

    io_setup(SpecialSetup::TermModes);
}

/*
 * The fd functions always pass exact=true, because the whole point of
 * using an fd in place of a file is that fds have a way to actually
 * _distinguish_ 'expansion has terminated' from 'more data not yet
 * available', namely EOF and blocking respectively.
 */

unique_ptr<FileOpener> CLIFileAccessContext::fd_opener(int fd)
{
    return make_unique<FdOpener>(fd);
}

/*
 * If we have fd support, then the 'stdin' input keywords are simply
 * synonyms for fd 0, which means that they still get the termios
 * handling if the user specifies the -T option.
 */
class StdinOpener : public FdOpener {
    virtual string name() const override;

  public:
    StdinOpener();
};

StdinOpener::StdinOpener()
    : FdOpener(0)
{
}

#else

unique_ptr<FileOpener> CLIFileAccessContext::fd_opener(int fd)
{
    throw io_error("fd keywords not supported in this build of spigot");
}

/*
 * If we don't have fd support, we still want to support the 'stdin'
 * input keywords, but they just use the existing FILE *stdin and
 * don't do anything clever.
 */
class StdinOpener : public FileOpener {
    virtual string name() const override;
    virtual BufferingReaderCacheKey key() const override;
    virtual unique_ptr<FileReaderRaw> open() const override;
};

BufferingReaderCacheKey StdinOpener::key() const
{
    return BufferingReaderCacheKey(FileType::Stdin);
}

unique_ptr<FileReaderRaw> StdinOpener::open() const
{
    return make_unique<StdioStreamReaderRaw>(stdin, name());
}

#endif

string StdinOpener::name() const { return "<standard input>"; }

unique_ptr<FileOpener> CLIFileAccessContext::stdin_opener()
{
    return make_unique<StdinOpener>();
}
